agentcloudflare-workersmodel-context-protocol

How to pass API key to tool calls in Cloudflare remote MCP server


I would like to pass a client secret/API key to a remote MCP server inside the Authorization header. I know that remote MCPs are meant to be used with OAuth, but in my case I'd rather just use the existing API keys.

mcp remote package allows to do so

  "mcpServers": {
    "remote-example": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://remote.mcp.server/sse",
        "--header",
        "Authorization: Bearer ${AUTH_TOKEN}"
      ]
    }
  }
}

I'm using npm create cloudflare@latest -- my-mcp-server --template=cloudflare/ai/demos/remote-mcp-authless template to set up a Remote MCP without Oauth. Now, all I want is to pass a token from the headers inside the tool call, so it's available inside the extra variable. Is there a way to do it?

import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

// Define our MCP agent with tools
export class MyMCP extends McpAgent {
  server = new McpServer({
    name: "Authless Calculator",
    version: "1.0.0",
  });

  async init() {
    // Simple addition tool
    this.server.tool(
      "add",
      { a: z.number(), b: z.number() },
      async ({ a, b }, extra) => {
        // I want API key/token to be available here
        console.log({ extra });
        return {
          content: [{ type: "text", text: String(a + b) }],
        };
      }
    );

    // Calculator tool with multiple operations
    this.server.tool(
      "calculate",
      {
        operation: z.enum(["add", "subtract", "multiply", "divide"]),
        a: z.number(),
        b: z.number(),
      },
      async ({ operation, a, b }) => {
        let result: number;
        switch (operation) {
          case "add":
            result = a + b;
            break;
          case "subtract":
            result = a - b;
            break;
          case "multiply":
            result = a * b;
            break;
          case "divide":
            if (b === 0)
              return {
                content: [
                  {
                    type: "text",
                    text: "Error: Cannot divide by zero",
                  },
                ],
              };
            result = a / b;
            break;
        }
        return { content: [{ type: "text", text: String(result) }] };
      }
    );
  }
}

export default {
  fetch(request: Request, env: Env, ctx: ExecutionContext) {
    const url = new URL(request.url);
    const authHeader = request.headers.get("authorization");

    if (!authHeader?.startsWith("Bearer ")) {
      return new Response("Forbidden", { status: 403 });
    }

    console.log({authHeader})

    const token = authHeader.split(/\s+/)[1] ?? "";

    // I can get a token from the headers
    // But how do I pass it to the tools?
    console.log({ token });

    if (url.pathname === "/sse" || url.pathname === "/sse/message") {
      // @ts-ignore
      return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
    }

    if (url.pathname === "/mcp") {
      // @ts-ignore
      return MyMCP.serve("/mcp").fetch(request, env, ctx);
    }

    return new Response("Not found", { status: 404 });
  },
};

Solution

  • You can set ctx.props.XYZ_FIELD in your outer fetch function and then access that as this.props.XYZ_FIELD within your server tool handler.

    So with your example that would become something like:

    import { McpAgent } from "agents/mcp";
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { z } from "zod";
    
    // Define our MCP agent with tools
    export class MyMCP extends McpAgent {
      server = new McpServer({
        name: "Authless Calculator",
        version: "1.0.0",
      });
    
      async init() {
        // Simple addition tool
        this.server.tool(
          "add",
          { a: z.number(), b: z.number() },
          async ({ a, b }) => {
            token = this.props.myToken as string; // <-- this is how you can access it
            return {
              content: [{ type: "text", text: String(a + b) }],
            };
          }
        );
    
        // ...
      }
    }
    
    export default {
      fetch(request: Request, env: Env, ctx: ExecutionContext) {
        const url = new URL(request.url);
        const authHeader = request.headers.get("authorization");
    
        if (!authHeader?.startsWith("Bearer ")) {
          return new Response("Forbidden", { status: 403 });
        }
    
        console.log({authHeader})
    
        const token = authHeader.split(/\s+/)[1] ?? "";
    
        ctx.props.myToken = token; // <-- this is how you can set it
    
        if (url.pathname === "/sse" || url.pathname === "/sse/message") {
          // @ts-ignore
          return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
        }
    
        if (url.pathname === "/mcp") {
          // @ts-ignore
          return MyMCP.serve("/mcp").fetch(request, env, ctx);
        }
    
        return new Response("Not found", { status: 404 });
      },
    };