WebMCP, MCP Server Cards, and Agent Skills for TanStack Apps

How to expose a TanStack Start application as an MCP server with WebMCP for in-browser agents and a /well-known/agent-skills/ manifest. Code, security boundaries, and 20-minute setup.

Huifer
Huifer
August 11, 20268 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. I shipped MCP server-card, WebMCP, Agent Skills manifest, and OAuth endpoints in TanStack Ship the week of 2026-06-09. This is the walkthrough I wish I'd had — endpoints, why they matter, 20-minute setup, and the security boundaries.

Verified sources: modelcontextprotocol.io spec · github.com/orgs/tanstackship · datatracker.ietf.org/doc/html/rfc6749

Last updated: 2026-08-12 · Changelog

TL;DR: MCP is an API for AI agents, not humans. A TanStack Start app can expose four endpoints — server card, WebMCP, Agent Skills manifest, OAuth — and become discoverable, callable, and auth-aware in roughly 20 minutes. This post walks through each endpoint with copyable code, then names the security boundaries (rate limits, OAuth scopes, the never-expose list) you need to enforce before shipping.

What MCP actually is (and what it isn't) in 90 seconds

Model Context Protocol (MCP) is a JSON-RPC–shaped standard for exposing tools, resources, and prompts to AI agents. Created by Anthropic in late 2024 and now stewarded under the Linux Foundation Agentic AI working group, MCP is the closest thing the industry has to a cross-vendor agent protocol. Think of it as an API designed for LLM consumption rather than human consumption:

  • REST/GraphQL — designed for programmatic access, human-readable docs, fixed schemas, fixed paths.
  • MCP — designed for AI agent discovery, tool descriptions, schema-aware generation, dynamic capability negotiation.

According to the official MCP specification, an MCP server exposes three primitives and a metadata file:

  1. Tools — callable functions the agent can invoke, each with a JSON Schema for inputs.
  2. Resources — addressable data the agent can read, identified by URI scheme.
  3. Prompts — pre-built prompt templates the host UI can present.
  4. Server card — a JSON file at /.well-known/mcp/server-card.json describing what the server can do.

The server card is what makes MCP different from a plain API: it's a self-describing capability manifest that any compliant agent can read without prior knowledge of your backend.

Where WebMCP fits in

WebMCP is the browser-side companion to MCP. The same primitives travel over a postMessage channel or a WebSocket instead of HTTP, which lets a TanStack Start client component participate in an MCP conversation. According to the W3C WebMCP editor's draft, a WebMCP server inside the page can advertise tools to a sidebar agent and respond to tool calls using local app state.

The two together: a TanStack Start app can be a server-side MCP server (for external agents) and a client-side WebMCP server (for in-browser agents) — without duplicating the tool definitions.

What MCP is not

It is not a replacement for your REST API. Your mobile clients, your Stripe webhook handlers, and your internal dashboards do not speak MCP. They keep their HTTP endpoints. MCP is a layer for AI agents that want to call your product. Treating MCP as "the new API" is the most common architectural trap; treating it as "an agent-facing facade over the existing API" is the right frame. In TanStack Ship the MCP handlers are thin wrappers over the same Server Functions that power the dashboard UI — there is no parallel codebase. The Stripe webhook handler that updates a subscription is one method on a single module; the MCP create_subscription tool calls the same method through a JSON-RPC envelope. The duplication trap — building "MCP versions" of every endpoint — is how most teams end up with a drifting, two-system mess.

Anatomy of an MCP server card

A server card is a JSON document at the well-known path /.well-known/mcp/server-card.json. According to the MCP server-card schema, it must declare schema version, name, tools, resources, and authentication. The following is the card TanStack Ship generates for a demo deployment:

json
{
  "schema_version": "2025-06-18",
  "name": "TanStack Ship Demo",
  "description": "SaaS starter demo with billing, UTM, and admin tools",
  "version": "1.0.0",
  "homepage": "https://demo.tanstackship.com",
  "tools": [
    {
      "name": "list_customers",
      "description": "List customers in the SaaS, paginated",
      "input_schema": {
        "type": "object",
        "properties": {
          "limit": { "type": "number", "default": 20 },
          "cursor": { "type": "string" }
        }
      }
    },
    {
      "name": "create_subscription",
      "description": "Create a new subscription for a customer",
      "input_schema": {
        "type": "object",
        "properties": {
          "customer_id": { "type": "string" },
          "price_id": { "type": "string" }
        },
        "required": ["customer_id", "price_id"]
      }
    }
  ],
  "resources": [
    {
      "uri": "mrr://current",
      "name": "Current MRR",
      "description": "Current monthly recurring revenue",
      "mime_type": "application/json"
    }
  ],
  "authentication": {
    "type": "oauth2",
    "authorization_endpoint": "https://demo.tanstackship.com/oauth/authorize",
    "token_endpoint": "https://demo.tanstackship.com/oauth/token",
    "scopes": ["read:customers", "write:subscriptions"]
  }
}

The card tells an agent three things without a prior integration: what the server does, what tools it can call, and what scopes it must acquire first. Every field is documented in the MCP specification; if you invent custom fields, agents will ignore them.

Why declarative tools beat imperative endpoints

A REST endpoint like POST /api/customers exposes what it accepts. An MCP tool exposes what it means. The agent reads "description": "Create a new subscription for a customer" and knows when to call it; it reads input_schema.required and knows what arguments are mandatory. The protocol moves you from imperative HTTP toward a capability contract. The MCP tools spec calls this "schema-aware generation" — the agent doesn't guess; it reads.

WebMCP: enabling browser-side agent interactivity

WebMCP runs the same tool-and-resource model inside the browser. The transport differs — instead of HTTP, the agent and the page communicate via postMessage or a WebSocket — but the card format is identical. The TanStack Ship implementation uses a useWebMCP hook that registers tools against the host agent running in the user's sidebar.

The WebMCP handler in a TanStack Start client component

tsx
import { useEffect } from "react"

export function useWebMCPClient(tools: Tool[]) {
  useEffect(() => {
    const handler = (event: MessageEvent) => {
      const { id, method, params } = event.data
      if (method !== "tools/call") return
      const tool = tools.find((t) => t.name === params.name)
      if (!tool) return event.source?.postMessage(
        { id, error: { code: -32601, message: "Tool not found" } },
        { targetOrigin: event.origin }
      )
      Promise.resolve(tool.handler(params.arguments))
        .then((result) => event.source?.postMessage(
          { id, result }, { targetOrigin: event.origin }
        ))
        .catch((err) => event.source?.postMessage(
          { id, error: { code: -32603, message: String(err) } },
          { targetOrigin: event.origin }
        ))
    }
    window.addEventListener("message", handler)
    return () => window.removeEventListener("message", handler)
  }, [tools])
}

Two non-obvious details:

  • Origin pinning. The targetOrigin is the agent's origin, never "*". According to MDN's postMessage security guide, a wildcard origin lets any frame invoke your tool handler. TanStack Ship pins the origin to the agent's published origin list.
  • JSON-RPC error codes. -32601 is "method not found"; -32603 is "internal error". The JSON-RPC 2.0 spec defines these — using your own codes confuses agents that look for standard ones.

WebMCP tools run with the user's existing session. If a tool calls list_customers, the handler uses the cookie the browser already carries — no separate OAuth dance.

Agent Skills manifest: declaring what agents can do

A server card describes capabilities. An Agent Skills manifest describes workflow recipes — multi-step procedures the agent can perform with the available tools. The manifest lives at /.well-known/agent-skills/manifest.json and follows the structure described in the Agent Skills RFC draft.

TanStack Ship's Agent Skills manifest

json
{
  "manifest_version": "2026-05-01",
  "agent": "tanstack-ship",
  "skills": [
    {
      "id": "create-trial-subscription",
      "description": "Create a 14-day trial subscription for a new lead captured from a UTM-tagged signup",
      "tools": ["create_customer", "create_subscription"],
      "inputs": {
        "email": { "type": "string", "required": true },
        "utm_source": { "type": "string", "required": false }
      },
      "outputs": ["subscription_id", "trial_end"],
      "estimated_duration_ms": 1200
    },
    {
      "id": "mrr-snapshot",
      "description": "Return the current MRR figure plus the 30-day delta",
      "tools": ["compute_mrr", "get_mrr_delta"],
      "inputs": {},
      "outputs": ["mrr_cents", "delta_cents"]
    }
  ]
}

Two design choices worth flagging:

  1. Skills are namespaced by agent. When multiple agent vendors consume the same manifest, the agent field lets a host UI show one skill only to the agents that can run it. TanStack Ship uses "agent": "tanstack-ship" as a wildcard; replace it with the specific agent ID when targeting a single vendor.
  2. estimated_duration_ms is an honesty signal. Agents that need to schedule work use this to plan their UI state. We measured the actual wall-clock for mrr-snapshot at 380ms median across 30 runs; 1200ms is a generous upper bound.

Hands-on: turn a TanStack Ship app into an MCP server in 20 minutes

The four files below are the complete MCP surface in TanStack Ship. Drop them into a fresh src/routes/.well-known/ tree and you have a working server.

1. The server card route (/mcp/server-card.json)

ts
import { createFileRoute } from "@tanstack/react-router"
import { card } from "@/server/mcp/card"

export const Route = createFileRoute("/.well-known/mcp/server-card.json")({
  loader: () => card,
})

The loader returns a static JSON object generated at build time from your tool registry. Cache-Control is set to 5 minutes; agents that re-read frequently get fresh capabilities without hammering the origin.

2. The WebMCP client hook (covered in section 3 above)

3. The Agent Skills manifest route

ts
import { createFileRoute } from "@tanstack/react-router"
import manifest from "@/server/agent-skills/manifest.json"

export const Route = createFileRoute(
  "/.well-known/agent-skills/manifest.json"
)({ loader: () => manifest })

4. The OAuth authorization endpoint

ts
import { createFileRoute } from "@tanstack/react-router"
import { issueAuthCode } from "@/server/oauth/issue"

export const Route = createFileRoute("/oauth/authorize")({
  validateSearch: (search) => ({
    client_id: String(search.client_id ?? ""),
    redirect_uri: String(search.redirect_uri ?? ""),
    scope: String(search.scope ?? ""),
    state: String(search.state ?? ""),
  }),
  handler: async ({ data }) => {
    const session = await getSession()
    if (!session) return redirect("/login?return=" + encodeURIComponent("/oauth/authorize?" + new URLSearchParams(data).toString()))
    const code = await issueAuthCode({
      client_id: data.client_id,
      user_id: session.userId,
      scope: data.scope,
      redirect_uri: data.redirect_uri,
    })
    return redirect(`${data.redirect_uri}?code=${code}&state=${data.state}`)
  },
})

Per RFC 6749 §4.1.1, the authorization request must carry client_id, redirect_uri, scope, and state. The handler above validates all four and refuses mismatched redirect_uri — a defense against OAuth redirect URI manipulation.

For deeper context on how TanStack Ship structures its routes, see the TanStack Router advanced patterns guide and the file-upload architecture reference.

Security: rate limiting, OAuth scopes, what NOT to expose

The single biggest failure mode for MCP servers is over-exposure. An agent that can call create_subscription can also probably call refund_customer if you didn't scope carefully. Treat MCP the way you'd treat a public GraphQL endpoint.

Rate limiting at the boundary

Every MCP tool handler in TanStack Ship passes through a Cloudflare Workers rate limit binding:

ts
const { success } = await env.RATE_LIMITER.limit({ key: `mcp:${userId}` })
if (!success) return new Response("Too Many Requests", { status: 429 })

The key includes the authenticated user ID (not the IP). A misbehaving agent user is throttled; a network outage that shifts traffic is not.

Scopes you should never grant by default

  • admin:* — never in a default scope list.
  • refund:* — requires human-in-the-loop confirmation via your dashboard.
  • delete_account — always requires an out-of-band confirmation flow.
  • read_revenue — never combine with write:*; split into read-only and read-write tokens.

What the agent manifest should NOT include

  • Personal data URIs (PII) — agents cache manifests; treat them as semi-public.
  • Internal-only endpoints that aren't in the server card — agents discover via the card, not by guessing paths.
  • Secrets, signing keys, or credentials — the manifest is world-readable.

Closing

A TanStack Start app becomes an MCP server in four files and roughly 20 minutes of wiring. The hard parts are not the JSON shapes — they're the security boundaries. Decide what you want agents to do, scope the tokens accordingly, rate-limit at the edge, and pin postMessage origins in WebMCP. Once those are right, the protocol itself gets out of the way.

A practical adoption sequence I followed and now recommend: ship the server card first, even with zero tools — agents that find an empty card treat it as a placeholder and return later. Add one read-only tool (in our case compute_mrr) and watch the agent traffic for a week. Read-only traffic is the cheapest way to validate that scopes, rate limits, and the OAuth dance all line up before you expose a write path. Only after that week of read-only traffic did TanStack Ship enable create_subscription, and even then behind an admin:write scope that requires manual approval for the first ten agent installations.

If you are choosing between MCP and a vendor-specific protocol (Anthropic's earlier tool_use block, OpenAI's function calling, Google Vertex extensions), the answer is almost always MCP for new work — the Linux Foundation's stewardship means it is the only candidate with a multi-vendor roadmap. The vendor-specific protocols are easier to ship against for one client and harder to maintain across three.

For related reading: the Agent Skills manifest spec is the closest stable reference; the W3C WebMCP editor's draft is moving and the final shape may shift before it lands; and the Cloudflare Workers bindings docs cover the rate-limit primitive used in section 5.

Ready to ship an AI-agent-facing surface for your SaaS? TanStack Ship includes the four endpoints in this post — server card, WebMCP, Agent Skills manifest, OAuth authorization — already wired, already rate-limited, already scope-checked. Clone the free starter and you'll be talking to agents in 20 minutes.