Search docs

Jump between documentation pages.

Browse docs

Model Context Protocol (MCP)

DaloyJS can host a dedicated Model Context Protocol server for AI clients that need tools, resources, and prompts. The core helper implements MCP Streamable HTTP with JSON-RPC 2.0, so a company that already runs a DaloyJS REST API can run a second DaloyJS service at/mcp with a different auth policy and a smaller, agent-safe surface area.

Keep the REST API and the MCP server separate when the callers, permissions, or rate limits differ. MCP tools are model-callable operations, so they deserve the same care as any production API route, plus tighter descriptions and schemas because the caller may be an AI client acting on a user's behalf.

Dedicated MCP boundary
  1. AI clientClaude, Cursor, VS Code
  2. DaloyJS MCP appPOST /mcp JSON-RPC
  3. Tools and contexttools, resources, prompts
  4. Existing systemsdatabase, REST API, queues
Run MCP as its own DaloyJS service when it has a different trust boundary than your REST API. The app still gets body limits, request timeouts, rate limits, auth middleware, and problem+json errors.

Install

bash
# MCP support ships in @daloyjs/core.
# No @modelcontextprotocol/sdk dependency is required.
pnpm add @daloyjs/core

Create an MCP server

Use createMcpHandler() for the MCP protocol layer and mcpRoutes() to mount POST, GET, and OPTIONS on a DaloyJS app. The POST route is the actual MCP transport. GET returns a JSON hint instead of opening a server-initiated SSE stream, and OPTIONS supports browser-based clients when CORS middleware is installed.

ts
import {
  App,
  McpToolError,
  bearerAuth,
  createMcpHandler,
  mcpRoutes,
  rateLimit,
} from "@daloyjs/core";
import { serve } from "@daloyjs/core/node";

const mcp = createMcpHandler({
  serverInfo: {
    name: "inventory-mcp",
    title: "Inventory MCP",
    version: "1.0.0",
  },
  instructions:
    "Use this server to inspect inventory and prepare stock reports.",
  tools: [
    {
      name: "inventory_lookup",
      title: "Inventory lookup",
      description: "Look up available inventory units by SKU.",
      inputSchema: {
        type: "object",
        properties: { sku: { type: "string", minLength: 1 } },
        required: ["sku"],
        additionalProperties: false,
      },
      handler: async (args) => {
        const sku = typeof args.sku === "string" ? args.sku : "";
        if (!sku) {
          throw new McpToolError("sku is required.");
        }

        const units = await inventory.countAvailable(sku);
        return {
          content: [{ type: "text", text: `${sku}: ${units} units` }],
          structuredContent: { sku, units },
        };
      },
    },
  ],
  resources: [
    {
      uri: "daloy://schemas/inventory",
      name: "inventory_schema",
      title: "Inventory schema",
      mimeType: "application/json",
      read: () => ({
        uri: "daloy://schemas/inventory",
        mimeType: "application/json",
        text: JSON.stringify({
          sku: "string",
          units: "number",
          warehouseId: "string",
        }),
      }),
    },
  ],
  prompts: [
    {
      name: "stock_report",
      title: "Stock report",
      description: "Draft a stock report for one SKU.",
      arguments: [{ name: "sku", required: true }],
      get: (args) => ({
        messages: [
          {
            role: "user",
            content: {
              type: "text",
              text: `Prepare a stock report for SKU ${String(args.sku)}.`,
            },
          },
        ],
      }),
    },
  ],
});

const app = new App({
  bodyLimitBytes: 64 * 1024,
  requestTimeoutMs: 10_000,
});

app.use(rateLimit({ windowMs: 60_000, max: 120 }));
app.use(
  bearerAuth({
    realm: "inventory-mcp",
    validate: (token) => token === process.env.MCP_TOKEN,
  })
);

for (const route of mcpRoutes("/mcp", mcp)) {
  app.route(route);
}

serve(app, { port: 3001 });

Client config

Point an MCP-compatible client at the deployed endpoint. The exact config file differs by client, but remote Streamable HTTP servers use a URL and whatever headers your auth middleware requires.

json
{
  "mcpServers": {
    "inventory": {
      "url": "https://mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}

What core supports

  • initialize, ping, tools/list, tools/call, resources/list, resources/read, prompts/list, and prompts/get.
  • Protocol-version negotiation, MCP-Protocol-Version rejection for unsupported versions, JSON-RPC parse errors, accepted notifications, and bounded request bodies.
  • Dependency-free TypeScript types for tools, resources, prompts, JSON schemas, content blocks, structured tool output, and handler context.

What stays out of core

DaloyJS does not bundle the official MCP SDK, stdio process management, OAuth server metadata, persistent MCP sessions, server-initiated SSE, or experimental tasks. Those pieces either add dependency weight or need a product-specific security model. Keep them in your application or a separate integration package until your use case needs them.

Error handling

Throw McpToolError when the model can fix the call, for example missing arguments or a domain object that does not exist. The client receives an MCP tool result with isError: true. Unexpected errors become JSON-RPC internal errors and are redacted in production.

ts
import { McpToolError, createMcpHandler } from "@daloyjs/core/mcp";

const mcp = createMcpHandler({
  serverInfo: { name: "inventory-mcp", version: "1.0.0" },
  tools: [
    {
      name: "inventory_lookup",
      description: "Look up inventory by SKU.",
      inputSchema: {
        type: "object",
        properties: { sku: { type: "string" } },
        required: ["sku"],
        additionalProperties: false,
      },
      handler: async (args) => {
        const sku = typeof args.sku === "string" ? args.sku.trim() : "";
        if (!sku) {
          throw new McpToolError("sku is required.");
        }

        const row = await inventory.findBySku(sku);
        if (!row) {
          throw new McpToolError(`No inventory record found for ${sku}.`);
        }

        return `${row.sku}: ${row.units} units`;
      },
    },
  ],
});

Security checklist

  • Put auth in DaloyJS middleware before the MCP route. Bearer tokens, mTLS, IP restrictions, and per-client rate limits all work normally.
  • Validate tool arguments inside handlers. The advertised JSON Schema helps clients, but it is not a substitute for server-side validation.
  • Keep tool descriptions precise. A vague tool is easier for a model to misuse and harder for a human to approve.
  • Route outbound calls through fetchGuard() when a tool fetches URLs influenced by users, prompts, or external content.