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 the weather tool 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 enable OAuth or add an @mcpAuthorizer method to control access

Quick Start

Prerequisites

  • A Squid backend project initialized with squid init
  • The @squidcloud/backend package installed from NPM
  • 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: Register 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
oauthMcpOAuthOptionsNoRequire OAuth 2.0 bearer tokens on every request. See OAuth authentication.

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> {
// Look up the product using your own data layer.
const product = await this.findProduct(sku);
if (!product) {
throw new Error(`Product with SKU ${sku} not found`);
}
return product.stockLevel;
}

Input schema

The inputSchema follows JSON Schema format. In TypeScript the tool method receives a single destructured object whose properties match the schema; in Python the method receives a single args: dict and reads each property by key:

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
authMcpAuthContext | undefinedValidated auth context, present only when the request passed OAuth validation

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

OAuth authentication

For MCP clients used by end users, such as Claude or other MCP-compatible assistants, a shared secret token is impractical: each user needs to sign in with their own identity. Adding the oauth option to @mcpServer requires a valid OAuth 2.0 bearer token on every request:

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

@mcpServer({
id: 'crm',
name: 'crmServer',
description: 'Provides CRM lookup tools',
version: '1.0.0',
oauth: {
integrationId: 'my-auth0',
},
})
export class CrmMcpService extends SquidService {
// Tools go here
}

McpOAuthOptions has a single field:

FieldTypeRequiredDescription
integrationIdstringYesThe ID of an auth integration registered for this app
note

MCP OAuth currently supports Auth0 auth integrations and is available in the TypeScript Backend SDK only. The Python @mcp_server decorator does not accept an oauth option.

When oauth is set, Squid handles the resource-server side of the OAuth flow automatically:

  1. Requests without a valid bearer token receive an HTTP 401 Unauthorized response with a WWW-Authenticate challenge pointing at the server's protected resource metadata.
  2. Squid exposes an OAuth 2.0 Protected Resource Metadata document at <mcp-server-url>/.well-known/oauth-protected-resource, listing your Auth0 tenant as the authorization server. MCP clients discover it and walk the user through the Auth0 login automatically.
  3. Squid validates the bearer token on every request against the referenced integration: the token must be RS256-signed by your Auth0 tenant (verified via its JWKS), with the tenant domain as issuer and the integration's client ID as audience.

The requested scopes are openid, profile, email, and offline_access.

With oauth enabled and no @mcpAuthorizer, any request carrying a valid token from the integration is allowed.

Combining OAuth with an authorizer

To apply your own rules on top of token validation, such as restricting access to specific users or domains, add an @mcpAuthorizer. The authorizer runs after OAuth validation succeeds, and the validated context is available on request.auth:

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

@mcpServer({
id: 'crm',
name: 'crmServer',
description: 'Provides CRM lookup tools',
version: '1.0.0',
oauth: {
integrationId: 'my-auth0',
},
})
export class CrmMcpService extends SquidService {
@mcpAuthorizer()
async authorize(request: McpAuthorizationRequest): Promise<boolean> {
const email = request.auth?.claims['email'] as string | undefined;
return !!email && email.endsWith('@my-company.com');
}

// Tools go here
}

McpAuthContext fields:

FieldTypeDescription
type'oauth'The auth mechanism that produced this context
integrationIdstringThe ID of the auth integration used to validate the request
claimsRecord<string, unknown>Validated JWT claims (sub, aud, iss, exp, custom claims)

Validated claims are available only inside the @mcpAuthorizer; @mcpTool methods receive only their input arguments. Enforce identity-based access centrally in the authorizer rather than expecting per-user context inside tools.

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> {
// Look up the order using your own data layer.
const order = await this.findOrder(orderId);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
if (order.status === 'shipped') {
throw new Error(`Order ${orderId} has already shipped and cannot be cancelled`);
}
await this.updateOrderStatus(orderId, '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

When OAuth is enabled, requests with a missing or invalid bearer token are rejected before any JSON-RPC processing with an HTTP 401 Unauthorized response carrying a WWW-Authenticate challenge.

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
OAuth returns 401 invalid_token on every requestToken issuer or audience doesn't match the integrationVerify the token comes from the integration's Auth0 tenant; the audience must be the integration's client ID
OAuth-enabled server rejects all requestsoauth.integrationId doesn't reference a registered Auth0 integrationRegister the Auth0 integration for the app and use its ID

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: 'Returns the current stock level for a product. Use when asked about inventory or availability.'

Bad: '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)
  • For user-facing MCP clients such as Claude, prefer OAuth authentication over shared tokens, and apply per-user rules in the authorizer via request.auth?.claims

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