MCP

A file that exports tools instead of a request handler becomes a live MCP server at the same /f/<name> URL. You write handlers; the platform speaks the protocol.

An MCP function

export const mcp = {
  name: "slugger",
  tools: {
    slugify: {
      description: "Turn a title into a URL slug",
      inputSchema: { type: "object", properties: { text: { type: "string" } }, required: ["text"] },
      async handler({ text }) { return text.toLowerCase().replace(/[^a-z0-9]+/g, "-"); },
    },
  },
};

rusted push slugger.js and it's serving. initialize and tools/list are answered from deploy-time metadata without booting a sandbox; tools/call validates arguments against the tool's inputSchema first, then runs the handler under your plan's limits like any invocation. A thrown error comes back as an isError tool result the model can read and correct — never a protocol error. Return a string for text content; any other value travels as JSON (objects also mirrored in structuredContent).

The push prints the block to paste into an MCP client:

{ "mcpServers": { "slugger": {
    "url": "https://rusted.sh/f/slugger",
    "headers": { "Authorization": "Bearer <your rusted api key>" } } } }

Who may call it

Three modes, declared in the file:

  • Owner key (default) — callers present one of your rusted API keys.
  • public: true — no key at all.
  • External OAuth — delegate caller authentication to your own authorization server:
export const mcp = {
  name: "notes",
  auth: {
    type: "oauth",
    issuer: "https://app.example.com",          // an https origin — no path, no trailing slash
    audience: "https://rusted.sh/f/notes",       // tokens must carry exactly this aud
    scopes: ["folders:read"],
    introspectionClientIdSecret: "AS_CLIENT_ID",       // vault names for RFC 7662 client auth;
    introspectionClientSecretSecret: "AS_CLIENT_SECRET", // the function itself can never read them
  },
  tools: { whoami: {
    description: "Show the verified caller",
    inputSchema: { type: "object" },
    handler(_args, context) { return context.auth; },  // { subject, clientId, scopes, connectionId? }
  } },
};

Rusted publishes RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource/f/<name>, challenges unauthenticated clients toward it, discovers your server's metadata, and introspects each bearer token — issuer, exact audience, expiry, scopes, and revocation must all hold before a sandbox starts. Tool code sees only the sanitized context.auth; the bearer token never reaches JavaScript. Rejected tokens are negatively cached briefly, an unreachable issuer answers 503 (distinct from an invalid token's 401), and refusals appear in your logs.

Environments get their own resource identities: /f/@stage/notes derives its audience (…/f/@stage/notes), publishes its own discovery metadata, and validates tokens against exactly that — a stage token never opens prod. Introspection credentials resolve from the stage vault.

What an MCP function is not

  • No SSE or streaming — every request gets one JSON response.
  • No sessions — Mcp-Session-Id is echoed, not stored.
  • No listChanged — the tool list changes only when you push.
  • No unbounded work — a tool call runs under your plan's execution budget.

The platform's own MCP server

Distinct from functions you deploy: https://rusted.sh/mcp is the server an AI agent connects to in order to build on rusted — its tools are execute, deploy, list, delete, inbox_create, and inbox_read. See AI agents.