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: 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 Case | Recommendation |
|---|---|
| Expose backend tools to any MCP-compatible agent | MCP server |
| Agent needs custom server-side logic during a conversation | Use AI functions |
| Agent needs to call tools on an external MCP server | Use an MCP connector |
| Agent needs to access a connected service (CRM, API, etc.) | Use a connected integration |
How it works
- You decorate a class with
@mcpServerin a service that extendsSquidService - You add tools to the server using
@mcpToolon methods within that class - Squid registers the MCP server at deploy time and exposes it as a JSON-RPC endpoint
- MCP-compatible clients connect, discover available tools via
tools/list, and call them viatools/call - Optionally, you add an
@mcpAuthorizermethod to control access
Quick Start
Prerequisites
- A Squid backend project initialized with
squid init-backend - The
@squidcloud/backendpackage 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:
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:
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:
- Add an MCP connector in the Squid Console, pointing it to your deployed MCP server
- 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:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique identifier used in the MCP endpoint URL |
name | string | Yes | Server name exposed in the MCP manifest |
description | string | Yes | Describes the server's purpose |
version | string | Yes | Server version exposed in the MCP manifest |
Each id must be unique across all MCP servers in your application. Duplicate IDs cause a deployment error.
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:
| Field | Type | Required | Description |
|---|---|---|---|
description | string | Yes | Tells the agent what the tool does and when to call it |
inputSchema | JSONSchema | Yes | JSON Schema defining the tool's input parameters |
outputSchema | JSONSchema | No | JSON 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.
@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:
@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:
@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.
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
| Field | Type | Description |
|---|---|---|
body | any | The parsed JSON-RPC request body |
queryParams | Record<string, string> | Query parameters from the request URL |
headers | Record<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:
@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 Code | Meaning | Cause |
|---|---|---|
-32001 | Unauthorized | The @mcpAuthorizer method returned false |
-32601 | Method not found | The requested JSON-RPC method or tool name does not exist |
-32000 | Server error | The MCP server ID was not found, or an internal error occurred |
Common issues
| Issue | Cause | Solution |
|---|---|---|
| Deployment fails with duplicate ID error | Two @mcpServer classes use the same id | Use a unique id for each MCP server |
| Deployment fails with duplicate tool name | Two @mcpTool methods have the same name in one server | Rename one of the methods |
| Tool is never called by the agent | Tool description does not match user prompts | Rewrite the description to clearly state what the tool does |
| Authorization always fails | Token or header check is incorrect | Log 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:
// 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
requiredonly when the tool cannot function without them - Use
enumto constrain values to a known set - Write clear
descriptionfields 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
@mcpAuthorizerwhen 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:
@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
- MCP connectors - Connect your agent to an MCP server through the Squid Console
- Abilities - Attach MCP connectors and other tools to your agent
- AI functions - An alternative way to expose backend logic to agents
- AI agent documentation - Build and configure AI agents