Skip to main content

Model Context Protocol (MCP)

Create custom MCP servers so AI agents can access your backend tools over the standard MCP protocol.

Why Use MCP

Your AI agent needs to call tools hosted on an external server, or you want to expose your own backend logic as tools that any MCP-compatible client can discover and invoke.

Without MCP, you would have to build custom integration logic for every agent-to-tool connection. With MCP, you define tools on a server and any compatible agent can discover and call them through a standard protocol:

Backend code
// Backend: define an MCP server with a tool
@mcpServer({
name: 'weather',
id: 'weather',
description: 'Provides weather data',
version: '1.0.0',
})
export class WeatherMcpService extends SquidService {
@mcpTool({
description: 'Returns the current weather for a city',
inputSchema: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
},
required: ['city'],
},
})
async getWeather({ city }: { city: string }): Promise<string> {
return `The weather in ${city} is sunny, 25°C.`;
}
}

Any MCP-compatible AI agent can now discover and call getWeather through the standard protocol.

Overview

MCP (Model Context Protocol) is an open protocol that standardizes how AI agents discover and invoke tools on external servers. Squid provides built-in support for creating MCP servers in your backend, letting you define tools as decorated methods that agents can call over JSON-RPC.

When to use MCP

Use CaseRecommendation
Expose backend tools to any MCP-compatible agentMCP server
Agent needs custom server-side logic during a conversationUse AI functions
Agent needs to call tools on an external MCP serverUse an MCP connector
Agent needs to access a connected service (CRM, API, etc.)Use a connected integration

How it works

  1. You decorate a class with @mcpServer in a service that extends SquidService
  2. You add tools to the server using @mcpTool on methods within that class
  3. Squid registers the MCP server at deploy time and exposes it as a JSON-RPC endpoint
  4. MCP-compatible clients connect, discover available tools via tools/list, and call them via tools/call
  5. Optionally, you add an @mcpAuthorizer method to control access

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init-backend
  • The @squidcloud/backend package installed
  • An AI agent created (if you want to connect the MCP server to a Squid agent)

Step 1: Create an MCP server with a tool

Create a service class that extends SquidService, decorate it with @mcpServer, and add a tool:

Backend code
import { mcpServer, mcpTool, SquidService } from '@squidcloud/backend';

@mcpServer({
name: 'greetingServer',
id: 'greetingServer',
description: 'A simple MCP server that greets users',
version: '1.0.0',
})
export class McpService extends SquidService {
@mcpTool({
description: 'Returns a greeting for the given name',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'The name to greet' },
},
required: ['name'],
},
})
async greet({ name }: { name: string }): Promise<string> {
return `Hello, ${name}!`;
}
}

Step 2: Export the service

Ensure the service is exported from the service index file:

service/index.ts
export * from './mcp-service';

Step 3: Deploy the backend

For local development, run the backend locally using the Squid CLI:

squid start

To deploy to the cloud, see deploying your backend.

Step 4: Connect the MCP server to an agent

After deployment, add your MCP server as a connector and attach it to an agent's abilities:

  1. Add an MCP connector in the Squid Console, pointing it to your deployed MCP server
  2. Add the connector to your agent's abilities in the Agent Studio

The agent can now discover and call your MCP tools during conversations.

Core Concepts

The @mcpServer decorator

The @mcpServer decorator marks a SquidService class as an MCP server. It accepts a configuration object with these fields:

FieldTypeRequiredDescription
idstringYesUnique identifier used in the MCP endpoint URL
namestringYesServer name exposed in the MCP manifest
descriptionstringYesDescribes the server's purpose
versionstringYesServer version exposed in the MCP manifest

Each id must be unique across all MCP servers in your application. Duplicate IDs cause a deployment error.

Backend code
import { mcpServer, SquidService } from '@squidcloud/backend';

@mcpServer({
id: 'inventory',
name: 'inventoryServer',
description: 'Provides product inventory lookup and management tools',
version: '1.0.0',
})
export class InventoryMcpService extends SquidService {
// Tools go here
}

The @mcpTool decorator

The @mcpTool decorator exposes a method as a tool that MCP clients can discover and invoke. It accepts a configuration object with these fields:

FieldTypeRequiredDescription
descriptionstringYesTells the agent what the tool does and when to call it
inputSchemaJSONSchemaYesJSON Schema defining the tool's input parameters
outputSchemaJSONSchemaNoJSON Schema describing the tool's output format

The method name becomes the tool name in the MCP manifest. Each tool name must be unique within a server.

Backend code
@mcpTool({
description: 'Looks up the current stock level for a product by SKU',
inputSchema: {
type: 'object',
properties: {
sku: {
type: 'string',
description: 'The product SKU code',
},
},
required: ['sku'],
},
})
async getStockLevel({ sku }: { sku: string }): Promise<number> {
const product = await this.squid.collection('products').doc(sku).snapshot();
if (!product) {
throw new Error(`Product with SKU ${sku} not found`);
}
return product.data.stockLevel;
}

Input schema

The inputSchema follows JSON Schema format. The tool method receives a single object parameter with properties matching the schema:

Backend code
@mcpTool({
description: 'Searches products by category and price range',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'Product category to search',
enum: ['electronics', 'clothing', 'home', 'sports'],
},
maxPrice: {
type: 'number',
description: 'Maximum price in USD',
},
inStockOnly: {
type: 'boolean',
description: 'If true, only return items currently in stock',
},
},
required: ['category'],
},
})
async searchProducts({
category,
maxPrice,
inStockOnly,
}: {
category: string;
maxPrice?: number;
inStockOnly?: boolean;
}): Promise<string> {
// Query logic here
return JSON.stringify(results);
}

Output schema

The optional outputSchema describes the structure of the tool's return value, helping clients understand the response format:

Backend code
@mcpTool({
description: 'Returns product details for a given SKU',
inputSchema: {
type: 'object',
properties: {
sku: { type: 'string', description: 'Product SKU' },
},
required: ['sku'],
},
outputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
price: { type: 'number' },
inStock: { type: 'boolean' },
},
},
})
async getProduct({ sku }: { sku: string }) {
return { name: 'Widget', price: 9.99, inStock: true };
}

The @mcpAuthorizer decorator

The @mcpAuthorizer decorator designates a method that runs before every request to the MCP server. Use it to validate incoming requests and reject unauthorized callers.

The authorizer method receives an McpAuthorizationRequest object and must return a boolean (or Promise<boolean>). Return true to allow the request, or false to reject it with an "Unauthorized" error.

Backend code
import { mcpAuthorizer, McpAuthorizationRequest, mcpServer, mcpTool, SquidService } from '@squidcloud/backend';

@mcpServer({
name: 'secureMcp',
id: 'secureMcp',
description: 'An MCP server with authorization',
version: '1.0.0',
})
export class SecureMcpService extends SquidService {
@mcpAuthorizer()
async authorize(request: McpAuthorizationRequest): Promise<boolean> {
const token = request.headers['authorization'];
return token === `Bearer ${this.secrets['MCP_AUTH_TOKEN']}`;
}

@mcpTool({
description: 'Returns sensitive data',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'The data query' },
},
required: ['query'],
},
})
async getSensitiveData({ query }: { query: string }): Promise<string> {
// This tool is only accessible if the authorizer returns true
return `Results for: ${query}`;
}
}

McpAuthorizationRequest fields

FieldTypeDescription
bodyanyThe parsed JSON-RPC request body
queryParamsRecord<string, string>Query parameters from the request URL
headersRecord<string, string>HTTP headers from the request

If no @mcpAuthorizer method is defined, all requests to the MCP server are allowed.

Error Handling

Tool errors

When a tool method throws an error, the MCP server catches it and returns it as a tool response with isError: true. The calling agent receives the error message and can relay it or decide how to proceed.

Throw clear, descriptive errors so the agent can provide useful feedback:

Backend code
@mcpTool({
description: 'Cancels an order by ID',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'The order ID to cancel' },
},
required: ['orderId'],
},
})
async cancelOrder({ orderId }: { orderId: string }): Promise<string> {
const order = await this.squid.collection('orders').doc(orderId).snapshot();
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
if (order.data.status === 'shipped') {
throw new Error(`Order ${orderId} has already shipped and cannot be cancelled`);
}
await this.squid.collection('orders').doc(orderId).update({ status: 'cancelled' });
return `Order ${orderId} has been cancelled`;
}

Protocol-level errors

The MCP server uses standard JSON-RPC error codes for protocol-level issues:

Error CodeMeaningCause
-32001UnauthorizedThe @mcpAuthorizer method returned false
-32601Method not foundThe requested JSON-RPC method or tool name does not exist
-32000Server errorThe MCP server ID was not found, or an internal error occurred

Common issues

IssueCauseSolution
Deployment fails with duplicate ID errorTwo @mcpServer classes use the same idUse a unique id for each MCP server
Deployment fails with duplicate tool nameTwo @mcpTool methods have the same name in one serverRename one of the methods
Tool is never called by the agentTool description does not match user promptsRewrite the description to clearly state what the tool does
Authorization always failsToken or header check is incorrectLog the McpAuthorizationRequest fields to debug

Best Practices

Write clear tool descriptions

The description is the primary way agents determine when to call a tool. Be specific about what the tool does and what information it returns:

Backend code
// Good: specific about capability and when to use
description: 'Returns the current stock level for a product. Use when asked about inventory or availability.';

// Bad: vague
description: 'Gets product info';

Design input schemas carefully

  • Mark parameters as required only when the tool cannot function without them
  • Use enum to constrain values to a known set
  • Write clear description fields for each property so the agent knows what format to provide
  • Use appropriate JSON Schema types (string, number, boolean, array, object)

Secure your MCP servers

  • Always add an @mcpAuthorizer when the server exposes sensitive operations
  • Validate authorization tokens against stored secrets rather than hardcoded values
  • Check both authentication (who is calling) and authorization (what they can do)

Validate tool inputs

Even though the input schema provides type constraints, validate inputs in your tool methods to handle edge cases:

Backend code
@mcpTool({
description: 'Transfers funds between accounts',
inputSchema: {
type: 'object',
properties: {
fromAccount: { type: 'string', description: 'Source account ID' },
toAccount: { type: 'string', description: 'Destination account ID' },
amount: { type: 'number', description: 'Amount to transfer in USD' },
},
required: ['fromAccount', 'toAccount', 'amount'],
},
})
async transferFunds({
fromAccount,
toAccount,
amount,
}: {
fromAccount: string;
toAccount: string;
amount: number;
}): Promise<string> {
if (amount <= 0) {
throw new Error('Transfer amount must be positive');
}
if (fromAccount === toAccount) {
throw new Error('Source and destination accounts must be different');
}
// Process transfer...
return `Transferred $${amount} from ${fromAccount} to ${toAccount}`;
}

Keep tools focused

Each tool should do one thing well. Prefer multiple small, focused tools over a single tool that handles many operations. This helps the agent select the right tool for the task.

Next Steps