Build your integrationTargetsMCP

Generated MCP server

Generate an MCP server that lets coding agents discover and call your API with typed inputs and explicit safety metadata.

Typeship generates an MCP server for your API so coding agents can discover operations, inspect their contracts, and make authenticated calls. The server ships as its own zero-dependency npm package and can also run at a hosted URL.

Smaller APIs expose one typed tool per operation. Larger APIs use search_docs, read_docs, and execute so tool discovery does not consume the agent's context window. Supported operations remain callable in either mode; plan caps and transport exclusions still apply. The Parcel API produces the package and executable parcel-mcp by default.

You can generate MCP without generating a CLI or SDK. When you also generate a CLI with the same command identity, both can use the same saved credentials.

Generated servers support MCP 2025-11-25 and 2026-07-28. Clients select a supported protocol with their default settings. See Protocol notes for the differences that affect clients.

The server reaches agents two ways: as its own package, which a client spawns or you deploy as a Worker, or as the hosted endpoint Typeship runs for you at a stable URL. To connect Claude Code, Claude Desktop, or Cursor, see Connect MCP clients.

Transports

Start with Authentication to choose how customers connect and which application owns their credentials.

The same compiled server speaks two transports. The transport decides where credentials come from.

stdio

node dist/mcp.js speaks newline-delimited JSON-RPC 2.0 over stdin and stdout. This is the transport local MCP clients spawn themselves. Build the package once first:

cd parcel
npm install
npm run build   # clients run dist/mcp.js

Credentials and the base URL resolve the same way as in the CLI. Environment variables win, then whatever parcel login and parcel config saved:

SettingEnvironment variableSaved by
Bearer tokenPARCEL_TOKENparcel login
API keyPARCEL_API_KEY, or one variable per headerparcel login
Basic authPARCEL_USERNAME / PARCEL_PASSWORDparcel login
Named credentialsPARCEL_CREDENTIALS (JSON)parcel login --credentials @credentials.json
Extra headersPARCEL_HEADERS (JSON object, or one Name: value per line), or --header "Name: value" in the server's argumentsNot saved
Base URLPARCEL_BASE_URLparcel config set base-url
Environmentparcel config set environment

Variable names follow the same rules as the CLI, including auth.credential_variables. When the spec declares no security, the server offers PARCEL_TOKEN as an optional bearer token and its instructions say authentication is not declared, rather than that none is needed. Tool input never asks for a parameter that repeats the configured credential.

For APIs with multiple security schemes, named credentials distinguish schemes that share a header and supply credentials required together. Local MCP merges them per scheme, with environment values taking precedence over the saved profile. It sends only a complete combination accepted by the endpoint. Named environment credentials and saved profiles are local-MCP inputs; they are not accepted from remote callers.

One login covers the CLI and every agent client. No secrets need to live in client config files. If no base URL can be resolved, the first tool call returns an isError result naming the fix (set PARCEL_BASE_URL or parcel config set base-url); the server stays up.

With identity fields configured, local MCP verifies the same mapped subject, account, and organization after refreshing a saved CLI session. It rejects a changed identity before replacing the saved token. Run the CLI login again to establish the intended identity. This protection applies to saved local sessions; explicit runtime credentials remain the caller’s responsibility.

The package also declares the server as a bin, so npx parcel-mcp (or parcel-mcp after a global install) starts it the same way node dist/mcp.js does.

Nothing but JSON-RPC is written to stdout. Startup errors and PARCEL_DEBUG=1 request lines go to stderr.

Requests from the generated MCP server to your API carry a User-Agent beginning with the server's command and version, such as parcel-mcp/1.0.0. If the connected MCP client announces a name, the header adds (client=<name>). Your API can use this client-reported name to distinguish MCP traffic; it is not an authenticated identity. The server adds no credentials or generator marker to the header.

Streamable HTTP

Use createMcpHandler from your generated package's /mcp export to serve remote requests. It validates a token issued for your MCP endpoint, then asks your application for that caller's API credentials. Keep credential storage, account linking, and authorization in the application that owns the deployment. Package functionality may live in a reviewed linked Draft with checks; independently released application adapters may remain separate.

Set mcp.access.issuer, mcp.access.resource, and mcp.access.scopes in the Project configuration. The resource is the public URL of the self-hosted endpoint, including its path. This connection policy is separate from the OAuth applications used by the generated CLI.

import { createMcpHandler, McpAccountLinkRequired } from "parcel-client/mcp";
import { apiCredentialsFor } from "./api-accounts.js";

const handle = createMcpHandler({
  async credentialsFor({ issuer, subject }) {
    // Look up the API account linked to this verified MCP identity.
    const credentials = await apiCredentialsFor({ issuer, subject });
    if (!credentials) {
      throw new McpAccountLinkRequired("https://accounts.parcel.example/connect");
    }
    return { bearerToken: credentials.accessToken };
  },
});

export default { fetch: handle };

apiCredentialsFor is application-owned code: it must resolve an account the caller is allowed to use, or fail. It can look up a linked account or use a credential your application stores for that user. It receives a verified issuer, subject, MCP resource, scopes and verified provider claims for your account/organization policy; it receives no incoming token or request headers. Return the same credential options your generated TypeScript SDK accepts, including named credentials. Client instances and reference lookups stay within one remote request. A credential resolver failure does not reveal its error details to callers.

When the account is not linked, McpAccountLinkRequired asks a compatible client to open your account page using MCP's URL-mode elicitation. The requested API operation stays paused. Your page signs the user in, verifies that they are the MCP user, obtains their API authorization and stores the grant in your application. Bind the linking attempt to the verified user and client. The URL must contain no credentials or personal information and must not grant access by itself. Your application owns this page and its grant storage; the generated handler does not create them.

After the browser interaction, the client retries the tool and credentialsFor runs again. A client acknowledgment cannot substitute for a linked grant. Declining or cancelling stops that attempt; clients without URL-mode support receive an ACCOUNT_LINK_REQUIRED error explaining that they need a compatible client. API credentials stay outside the MCP conversation. Use a dedicated callback and validate OAuth state/PKCE and the returned identity before saving a link.

By default, your provider must issue signed JWT access tokens for the MCP resource. The generated validator supports RS256, ES256 and Ed25519, checks signature, exact issuer, audience, expiry and required scopes (from scope, or from scp as a space-separated string or an array, as Okta and Microsoft Entra issue), and discovers signing keys from the issuer unless you set mcp.access.jwks_url. For opaque tokens, use introspection below. Review the provider requirements before selecting an authorization server.

The unconfigured handleHttp entry and --http command return 503; adding an API token to the server environment does not open remote access. Use the configured handler above. MCP connection tokens cannot be forwarded as API credentials. Your API independently enforces its own permissions.

Browser origins must match the configured MCP origin or appear in PARCEL_MCP_ALLOWED_ORIGINS. Loopback origins are accepted automatically only for a loopback MCP resource. Requests without Origin still require a valid MCP token. CORS exposes the OAuth challenge header for allowed browser clients.

The HTTP server is stateless, as the 2026-07-28 transport specifies. It accepts one JSON-RPC message per POST, never mints or reads session ids, and does not open a server-to-client stream. Notifications return 202. The metadata endpoint accepts GET and browser preflight accepts OPTIONS; other methods return 405.

Every request must carry the transport's mirrored headers, and the server checks them against the body as the spec requires: MCP-Protocol-Version (must equal the version in _meta), Mcp-Method (must equal method), and Mcp-Name on tools/call (must equal params.name; the =?base64?…?= encoding is decoded first). A missing or mismatched header is 400 with JSON-RPC error -32020. An unknown method is 404 with -32601. A JSON-RPC batch or a malformed message is 400 with -32600.

Opaque access tokens

An opaque token does not contain claims your server can verify locally. Your provider's introspection endpoint checks the token and returns its active state, identity, audience and permissions.

Register a confidential resource-server client with permission to introspect tokens for your MCP endpoint. Keep its credentials in your server's secret store. Add an introspection option to your application-owned createMcpHandler wrapper. This example uses an illustrative parcel-client package for the fictional Parcel API:

import type { McpTokenIntrospectionConfiguration } from "parcel-client/mcp";

const introspection: McpTokenIntrospectionConfiguration = {
  url: process.env.PARCEL_INTROSPECTION_URL!,
  clientId: process.env.PARCEL_INTROSPECTION_CLIENT_ID!,
  clientSecret: process.env.PARCEL_INTROSPECTION_CLIENT_SECRET!,
  authMethod: "client_secret_basic",
};

Use your generated package's /mcp import path in place of parcel-client/mcp. Pass introspection and your existing credentialsFor resolver to createMcpHandler. The issuer, MCP resource and required scopes still come from your shared Authentication settings. Introspection credentials belong to the server performing the check; they are separate from the public client that signs users in and from credentials used to call your API. Do not put the secret in Typeship configuration or commit it with your generated package.

The endpoint must be a fixed HTTPS URL, or a loopback URL for local development. The handler supports client_secret_basic and client_secret_post, rejects redirects, and bounds the entire response to 10 seconds and 1 MiB. Set timeoutMs to change the deadline, up to 30 seconds.

Your provider must return active: true, a nonempty sub, an aud containing the exact MCP resource, and a future numeric exp. If the response omits aud, its resource must contain the exact MCP resource instead; a present aud is always authoritative. Required scopes must appear in scope or scp. If iss is present, it must match your configured issuer; otherwise the fixed authenticated endpoint establishes the issuer. A client ID alone does not establish the resource audience. Sender-constrained tokens such as DPoP are not supported by this bearer handler.

Introspection checks every request, including JWTs when this option is set. It does not cache successful token checks or fall back to local signature validation. A revoked token is denied when the provider reports it inactive. Provider errors return 503 and stop API execution; invalid tokens return 401, and insufficient connection scopes return 403. This adds provider latency and availability requirements, so rate-limit your public endpoint for your expected traffic.

credentialsFor continues to receive the verified identity and resolve a separate API grant. Introspection does not create an account-linking page or manage your customers' API credentials. See provider requirements before selecting a deployment.

Connect it

When the CLI Target is selected too, its mcp command is the fastest way to wire the server into an agent client:

parcel mcp                    # print the server entry as JSON
parcel mcp install --claude           # write ./.mcp.json (Claude Code)
parcel mcp install --claude-desktop   # write the Claude Desktop config
parcel mcp install --url https://typeship.dev/mcp/<slug> --claude   # a remote endpoint instead

Local entries are { "command": "node", "args": ["/abs/path/parcel/dist/mcp.js"] }. Remote entries are { "type": "http", "url": "..." }. Existing servers in the target file are preserved. Only the parcel entry is added or replaced. Claude Desktop's config file only launches local servers, so --claude-desktop with --url stops with the instruction to add the URL as a connector in the app instead.

The written entry contains no tokens. Supply the environment credentials documented by the generated MCP package. HTTP and worker callers supply their own authentication context.

Configure each local server through its MCP client’s environment or secret settings. Your API determines account identity and permissions.

For an MCP-only package, use the README's generated Claude Code/Codex commands or VS Code install links. They run npx -y --package <package-name> parcel-mcp; put the API credential named in the Connect block in the MCP client's environment settings. No CLI command is assumed. The package also ships a registry-valid server.json; see Connect and distribute a generated MCP server.

Use a service account

Select a confidential OAuth application for the MCP Target and supply PARCEL_CLIENT_SECRET in the server process's environment. The generated server uses the selected client ID, token endpoint, scopes and client authentication method. PARCEL_CLIENT_ID, PARCEL_TOKEN_URL, and PARCEL_CLIENT_AUTH_METHOD=post|basic can override those defaults. A client ID without a secret continues to use the compatible CLI profile or other supplied credentials.

With environment auth overrides, set PARCEL_ENVIRONMENT=sandbox to select its API URL, public client ID, token endpoint and grant settings together. Supply the secret for that selected client. An explicit PARCEL_BASE_URL must match the environment.

Machine tokens are acquired only for operations whose declared security accepts that grant. They do not substitute for a separate user credential. Permission to connect to a remote MCP server remains separate from permission to call your API. See Configure a machine client.

Tools

In operations mode, each operation is a tool named resource_method in lower snake case: shipments_list, shipments_get, shipments_cancel. GraphQL root fields follow the same shape: query_shipment, mutation_create_shipment. Names are limited to lowercase letters, digits, and underscores, and deduplicated. A name longer than 60 characters is cut at a word boundary and ends with a four-character digest of the full name, so it stays distinct and does not change when other operations are added.

  • Descriptions give agents the operation summary, the first sentence of its description when that adds something, and the method and path as ground truth: Create a shipment. A shipment moves packages from a sender to a recipient. POST /shipments. Paginated tools add (paginated: returns one page plus hasMore and nextPage arguments). The full description stays available through read_docs.
  • Guardrails the spec already states are folded in, so the agent reads them before the call instead of in the error. A deprecated operation's description leads with Deprecated.; an operation whose security names credentials and scopes describes its alternatives, such as Authentication: bearer or oauth (scopes: shipments:write).; an operation that works without credentials in an API that otherwise has them says No credential needed. Argument descriptions gain enum meanings from x-enumDescriptions (or x-enum-descriptions / x-enum-varnames) as Values: pending (label not yet scanned), in_transit (with the carrier), the schema default, Markdown; use literal newlines. for text/markdown content, Deprecated. for deprecated parameters, and, when strict reference inference succeeds, Accepts an ID or an exact name (resolved via recipients_list). The same notes appear in read_docs and CLI help.
  • When the spec cannot say it (a three-step upload, a slow endpoint), write the tool description yourself: mcp.tool_descriptions in the project's config, keyed by operationId or "METHOD /path", replaces the derived text for that operation in the package and on the hosted endpoint. Keys that match nothing are generation warnings.
  • Each tool carries a title (the operation summary) and spec annotations: readOnlyHint for GET and HEAD, for GraphQL queries, and for POST operations the spec names as reads (search, query, find, count); destructiveHint for DELETE and operations named like remove, cancel, archive, and revoke; idempotentHint for reads, PUT, and DELETE; openWorldHint false, since the server talks to one known API. Ordinary creates and updates are writes without being mislabeled destructive.
  • Input schemas are JSON Schema 2020-12 derived from your spec: real types, enums, formats, descriptions, defaults, and required fields for path, query, header, and body parameters. allOf compositions are merged into one flat argument list, readOnly properties are left out of inputs, and nullable fields keep their nullability. Nested schemas are inlined to a fixed depth so models can read them without resolving references.
  • Tools carry an outputSchema when your spec documents a success body (paginated tools describe items, hasMore, nextPage), and successful results carry the JSON as structuredContent alongside the text block. Error results keep their JSON in the text only, since structuredContent must match the schema.
  • Operations whose body is a plain object take its fields as top-level arguments. Other body shapes take a single body argument. Argument names are your wire names.
  • Every tool also takes fields, an array of dotted paths that keeps only those keys of the result (["id", "status", "recipient.email"]), applied per item on paginated tools. It is the same projection the CLI's --fields does, and the way to keep large responses small. A path the operation's response schema declares may be absent: an optional key that no item has is left out of the result. Any other path that matches nothing in the result (in any item, on a paginated tool) returns FIELDS_UNMATCHED with the keys that do exist, instead of an empty object. The call has already run by then, so the error also carries the unprojected result in result (the leading items of a page when the whole page is over half the size cap), and for a write it says not to repeat the call. An operation that already has a fields parameter keeps its own.
  • GraphQL operations that return an object accept a select argument, a raw selection set such as { id status }, that replaces the default depth-2 selection. It is declared in the tool's input schema with the default spelled out.

Every server also ships two documentation tools:

  • search_docs searches your reference (operations, parameters) and, when a docs site is configured, your guides. Reference matches are ranked for the way agents describe a task: plural and other word forms match, rare words count for more than common ones, and the query's verb is matched against what each operation does (its HTTP method and whether its path names one item or a collection, or its GraphQL root field). GraphQL mutations are also found through their input fields, so "assign issue" finds issueUpdate through assigneeId. Deprecated operations follow current ones. Matches come 10 at a time through page.
  • read_docs returns an operation's reference by tool name or dotted resource.method: its description, authentication requirement and safety classification, every argument in prose (type, enum values, whether it is required, and nested fields), one example, and the shape of the result. An object argument shows its named input type, such as IssueFilter. Pass path to read one nested argument: {"page": "issues_list", "path": "filter.team.key"} returns that field's type and description, followed by the named type's fields or enum values. Pass a type name as page, such as {"page": "StringComparator"}, to read the type itself. Each named type is described once, and every type it mentions can be read the same way. Pass schema: true to add the complete input and output JSON Schemas, which are several times larger. It can also read a guide page from your docs site.

The reference comes from the spec and is baked into the package. Guides come from your documentation site through the llms.txt convention. See Docs for agents.

Tool mode for large APIs

Hundreds of per-operation tools flood an agent's context. The meta tool mode collapses the server to exactly three tools:

  • search_docs and read_docs, as above.
  • execute, which runs any operation by name with a JSON arguments object. It accepts tool names (shipments_create) or dotted names (shipments.create). Operation arguments, fields included, go inside arguments; any other top-level key returns INVALID_ARGUMENTS without calling the API. Operations classified as destructive stop with CONFIRMATION_REQUIRED until the call is repeated with confirm: true; reads and ordinary writes do not need it.

Set it with mcp.tool_mode in the project's config: auto, operations, or meta. auto is the default and switches to meta when there are more than 100 operations or the per-operation tool list would exceed 40,000 characters (about 10,000 tokens). The hosted endpoint honors the same setting.

Argument validation

Before anything reaches your API, the arguments of a tool call are checked against the tool's input schema, and every problem comes back at once as one isError result, so a single round trip fixes the call:

  • An unknown argument name is an error with a suggestion ("servce": did you mean "service"?). When only case or punctuation differs (shipmentId for shipment_id), the value is matched to the accepted name and the call proceeds.
  • Values are coerced where the intent is clear ("true" to a boolean, "3" to an integer, a JSON string to an object, a lone scalar to a one-item array, US to the enum member us) and rejected where it is not ("abc" for an integer), naming what was expected.
  • Missing required arguments are listed with their descriptions.
  • Date and date-time arguments take relative forms as well as ISO 8601: -P7D and -7d (seven days ago), +PT1H and +2h (ahead), 7 days ago, in 2 weeks, today, yesterday, now. They resolve against the server's clock before the request, so your API sees an absolute value (YYYY-MM-DD for date, an ISO instant for date-time). An unsigned duration (7d) is rejected as ambiguous, naming the signed forms. The argument's description says which forms it takes.
{
  "error": "InvalidArguments",
  "code": "INVALID_ARGUMENTS",
  "message": "2 problems with the arguments to shipments_create; nothing was sent to the API.",
  "issues": [
    { "code": "UNKNOWN_ARGUMENT", "argument": "servce", "message": "Unknown argument \"servce\"; did you mean \"service\"? Accepted: recipient, service, packages, metadata, fields." },
    { "code": "MISSING_ARGUMENT", "argument": "recipient", "message": "Missing required argument \"recipient\"." }
  ],
  "next_steps": ["Fix the arguments listed in issues and call shipments_create again.", "read_docs {\"page\": \"shipments_create\"} lists every argument with its type and whether it is required."]
}

Correct the reported arguments before retrying. Unknown arguments are not silently removed.

Human-readable references

An eligible <resource>_id argument accepts a human name as well as an opaque ID. Typeship enables this automatically only when there is exactly one unambiguous read collection for the resource, that operation takes no required arguments, and its item schema exposes id plus exactly one of name, slug, key, or email. The server performs an exact, case-insensitive match, never a fuzzy or partial one, and uses the collection's matching query filter when it has one.

  • One match substitutes its id before calling the requested operation.
  • No matches stop before the requested operation and return REFERENCE_NOT_FOUND.
  • Multiple matches stop before the requested operation and return REFERENCE_AMBIGUOUS with each match's ID and fields.
  • An unfiltered collection scan stops after five pages. If more results remain, REFERENCE_SCAN_LIMIT asks for an exact ID or a more selective resolver.
  • Resolved values are cached for the local server process. Remote HTTP requests resolve references again through your API, so separate Targets and later requests do not reuse another request's name-to-ID mapping. Compact-mode execute and direct operation tools use the same path. Your API must enforce access to the resolved record on every operation.

User-shaped references such as user_id, owner, and assignees also accept "me" when the callable surface has both that argument and a known identity operation. This applies to a string argument, an array of strings, a union with a string member, and the same names inside an object argument such as a GraphQL input. An ID-named argument (user_id, assigneeIds) receives the caller's id. Any other receives the caller's login, username, or handle, or id when the identity has none of these. The value "me" in any other argument is sent unchanged. Declare the identity operation with x-typeship-me: true, or select it with auth.identity_verification.operation. The generated server instructions list each argument that accepts "me", and only when both pieces are present.

Inference is optional and reversible. Use mcp.reference_resolvers in Configuration reference to declare a resolver or set one argument to false without changing the Spec. OpenAPI authors can instead put x-typeship-resolve on the parameter; see Spec compatibility.

Results and errors

Tool results are compact JSON text content. Paginated tools return one page plus a continuation signal and, when there is another page, the arguments that fetch it, whatever the API's pagination style:

{ "items": [ ... ], "hasMore": true, "nextPage": { "cursor": "cur_2", "limit": 20 } }

For a list that pages by URL (a Link header, or a next-page URL field such as next_page_uri), nextPage holds page_url, the URL the API named, with the operation's required arguments (such as path parameters) and fields repeated, so it can be passed back as-is. The server requests that URL as given, on the API's origin only.

Results are capped at 64,000 characters (about 16k tokens) so the agent sees an explanation instead of a mid-JSON cut. Over the cap, a page is cut to whole items and gains a truncated note saying how many were omitted and how to continue: offset and last-id pagination styles resume exactly at the cut through nextPage; cursor and page styles say to call again with a smaller page size. A plain object loses its largest keys, each replaced by a marker that names the fields call that fetches it alone. The package's server takes the cap from <PREFIX>_MCP_MAX_RESULT_CHARS.

API failures come back as tool results with isError: true, carrying the typed error, a stable code, your API's own error payload, and what to do next, so the agent can branch without parsing prose:

{
  "error": "NotFoundError",
  "code": "NOT_FOUND",
  "message": "...",
  "status": 404,
  "body": { ... },
  "docs_url": "https://docs.parcel.example",
  "next_steps": ["Check the id; list the resource first to find the right one."]
}

The codes are the generated CLI's: NO_AUTH, AUTH_INVALID, PLAN_LIMIT, NOT_FOUND, INVALID_REQUEST, RATE_LIMITED, SERVER_ERROR, NETWORK_ERROR, VALIDATION_FAILED, plus INVALID_ARGUMENTS for the checks above, REFERENCE_NOT_FOUND, REFERENCE_AMBIGUOUS, and REFERENCE_SCAN_LIMIT for human-reference resolution, FIELDS_UNMATCHED when a fields path matches nothing in the result, and CONFIRMATION_REQUIRED when compact-mode execution needs explicit destructive confirmation. A 401 names where credentials come from on the transport in use (the environment variable and login on stdio, the Authorization header over HTTP); a rate limit (a 429, or a 403 whose headers say the quota is spent) returns RATE_LIMITED with the reset time in retry_at and the next step; and a failure the API reports inside a successful response, such as Slack's ok: false, returns an error rather than data. A GraphQL error inside a 200 response keeps the vendor's code in vendor_code beside the normalized code, classified as the CLI does; an unrecognized code is CALL_FAILED with no next step, and body keeps the original errors. When the response also carries data, the tool returns that data with the errors instead of failing. On stdio, a tool whose operation requires credentials that are not fully configured returns NO_AUTH without calling the API, naming the missing security schemes and the environment variables that supply them. See Output and exit codes for what each code means. JSON-RPC protocol errors are reserved for unknown tools, unknown methods, and malformed requests.

Files: binary responses and uploads

A binary response (an image, a PDF, an archive: anything the API returns that is not JSON or text) never comes back as {}. Images up to 4 MB come back as an image content block the model can look at, with a text summary (media_type, bytes) beside it. Other binaries depend on where the server runs:

  • The package's server, run as a process on the agent's machine (stdio, or --http launched there), saves the bytes to a per-server directory under the OS temp dir and returns { saved_to, media_type, bytes }, the same way the CLI's --out materializes files.
  • A remote server (the hosted endpoint, or the Cloudflare worker) has no disk of the agent's. Binaries up to 1 MB are embedded as a base64 resource block; larger ones return a description with the size and the advice to fetch through the SDK, the CLI, or the package's local server.

Uploads go the other way. Operations with a multipart/form-data body or a raw binary body are tools on the package's local server, where each file argument is a local path the server reads and sends ("image": "/Users/me/avatar.png"), and a field that holds several files takes an array of paths, one part each; an unreadable path is an argument error before anything is sent. On a remote server they are not tools, since the server cannot see the agent's files; the argument description and the server instructions say so. Generation reports which operations are affected.

Read-only and narrower servers

Use read-only mode when an agent should only inspect data. --read-only (or <PREFIX>_MCP_READ_ONLY=1) keeps only read operations: GET and HEAD, GraphQL queries, and POST operations the spec names as reads. Writes are out of the callable set, not merely unlisted: a write called by name, through execute, or in read_docs returns NOT_AVAILABLE saying the server is read-only, and nothing is sent. The server's instructions say it is read-only and how many operations that hides.

parcel mcp install --claude --read-only     # registers node dist/mcp.js --read-only

--tools shipments,labels.purchase (or <PREFIX>_MCP_TOOLS) narrows the server to named resources, tool names, or dotted resource.method, for agents that need a corner of a large API without the meta tool shape. An operation outside the list returns NOT_AVAILABLE naming the switch. A value that names nothing in the API stops the server at startup with exit code 2 and lists the resources, so a typo cannot start a server with no operations. Any other argument the server does not read, such as a misspelled --readonly, stops it the same way and lists the flags it takes. Both switches compose with mcp.tool_mode. The hosted endpoint serves its read-only twin at /mcp/<slug>/readonly.

Instructions for agents

server/discover returns instructions that agents read once when they connect: what the tools are, how arguments and results behave, where credentials come from on this transport, whether the server is read-only, and, when the API has an operation that returns the caller (the CLI's whoami target, or auth.identity_verification.operation in config), which tool to call first to learn whose credential this is. Add your own with mcp.instructions in the project's config (up to 2,000 characters): what to call first, conventions the spec does not state, what not to do. The package's server and the hosted endpoint both carry it. serverInfo names your docs site as websiteUrl when one is configured.

Generation warns when tools/list exceeds 80,000 characters, since every client loads the list once per session.

OAuth discovery

The configured handler serves RFC 9728 metadata at /.well-known/oauth-protected-resource followed by the MCP endpoint's path. For https://mcp.parcel.example/mcp, an invalid or missing connection token receives:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.parcel.example/.well-known/oauth-protected-resource/mcp", scope="mcp:connect", error="invalid_token"

The metadata names the configured MCP issuer and resource, independent of the incoming Host header and your API's OAuth settings. The MCP client signs in through that issuer. An expired, incorrectly signed, wrong-issuer or wrong-audience token receives 401; a valid token lacking connection scopes receives 403. Provider discovery or signing-key failures return 503 and do not fall back to server-held API credentials.

Docs for agents

Two layers back search_docs and read_docs, and the CLI's docs command:

  1. Reference. Every operation's summary, description, and argument table, generated from your spec and shipped in the package. Works offline.
  2. Guides. Your documentation site, read through llms.txt and llms-full.txt. Most docs hosts publish these automatically. The URL resolves in this order: parcel config set docs-url, then docs_url in the project's config, then your spec's externalDocs.url.

Nothing is fetched unless a docs tool or command runs. Fetches time out after 10 seconds.

Follow Connect your API guides to CLI and MCP to publish the index, configure its URLs, and verify search and reading end to end.

Host it yourself

The package includes a Worker entry and wrangler.toml. Before deploying, create an application-owned entry with createMcpHandler, as shown above, and point main in your Wrangler configuration to it. Supply API credentials from your application's account storage or provider integration. The generated default entry stays closed until you configure that mapping.

npx wrangler deploy

A self-hosted deployment serves the Spec it was generated from. The hosted endpoint below uses a different credential path. It forwards the caller's API credential and does not separate MCP connection authorization from API access.

Hosted endpoint

FreeAvailable on every planThe hosted MCP Delivery is included on every plan.

The server in your package is something your users install or you deploy. Typeship also runs a Project endpoint at a URL your users paste into their agent client:

https://typeship.dev/mcp/<slug>

Hosted URLs forward the API credential each caller supplies. They do not validate a separate MCP connection token or resolve a user's API grant. The shared MCP issuer, resource and scope settings configure the generated handler; saving them does not add that authorization boundary to a hosted Delivery. For separate MCP and API authorization, deploy the configured package described in Host it yourself.

The hosted MCP Delivery is on by default when you select the MCP Target, on every plan. Its URL appears under Targets and on the Target's deliveries. Remove or disable that Delivery when you only want to distribute or self-host the generated package. Disabling the MCP Target stops its hosted endpoint too.

  • The endpoint serves the MCP Target's last accepted Generation after its reviewed Draft merges. It uses that Generation's Spec Revision, configuration, and operation selection. Before the first merge, it serves the latest successful Generation. Once a Draft has merged, newer unmerged or failed Generations do not change either URL. Generate and merge the Draft to update an accepted endpoint.
  • Free serves up to the 25 operations selected by the served Generation on its one active Project. Pro and Enterprise serve the Generation's selected operations under their existing plan limits. This applies to both the normal and read-only URLs, including compact-mode execute calls.
  • Upgrading retains the Generation's operation selection until you generate again. Downgrading to Free limits the active URL to 25 operations and pauses the other Projects' URLs.
  • Tool calls run through the same request runtime every generated SDK ships: the same query and body encoding, retries with Retry-After, per-operation retry policies, idempotency keys, and page walking. A tool behaves the same on the hosted URL and in the package.
  • Each caller supplies the credential in the scheme your spec or GraphQL config declares. Authorization passes through for bearer, OAuth, and Basic auth. Declared API-key headers pass through by name; an API key declared in the query is supplied as a same-named MCP request header and moved into the upstream query. server/discover names the exact header. Typeship receives the credential to make the request; this path does not persist an API credential store.
  • Tool calls are rate limited per caller (by the credential the caller sends, else by IP) at 120 per minute and per endpoint at 1,200 per minute; over the limit is 429 with Retry-After. tools/list and server/discover are not limited.
  • The same search_docs and read_docs tools ship, backed by your spec and your docs site.
  • The served Generation's mcp.tool_mode and mcp.instructions config apply, so large APIs can serve the three-tool meta shape and agents read your guidance on connect.
  • /mcp/<slug>/readonly is the same endpoint with every write left out of the callable set, for agents that should only read; it serves its own protected-resource metadata at /mcp/<slug>/readonly/oauth-protected-resource.
  • Arguments are checked and coerced, results are projected with fields and capped in size, and errors carry codes and next steps, as in the package.
  • When your spec declares OAuth2, Typeship can determine its issuer, and you have verified your origins, a request with no declared credential gets the standard discovery challenge. A caller that sends a declared API key is not challenged. Protected-resource metadata is served at /mcp/<slug>/oauth-protected-resource and at /.well-known/oauth-protected-resource/mcp/<slug>, and lists the scopes your operations require.
  • When your API rejects a forwarded OAuth access token with 401, the endpoint answers the MCP request with HTTP 401 and an invalid_token challenge, so the client refreshes or signs in again. A 403 with insufficient_scope becomes an HTTP 403 challenge naming the scope. Other API errors, and rejected API keys, are tool errors.

A client that supports Streamable HTTP and the protocol revision described below can add the URL. Your CLI can write the entry for the common ones:

parcel mcp --url https://typeship.dev/mcp/<slug> --claude

The entry uses the credential your spec declares: an Authorization bearer token or an API-key header, as a reference to the CLI's environment variable. For an OAuth-only API it adds no header, and the client signs in when it connects. See Connect MCP clients for the manual configuration.

Verify your origins for OAuth

The hosted endpoint offers OAuth sign-in under typeship.dev only after your Project proves it controls the API origin that receives the forwarded token. If your issuer is on a different site from your API, verify the issuer origin too. Until then, the endpoint serves no OAuth challenge or protected-resource metadata, and callers send a credential header instead. Public operations and header credentials work without verification.

Open Console → Project → Settings → MCP server to see each origin and its verification value. Prove each one in either of two ways:

  • Add a DNS TXT record named _typeship-verification.<host> with the value.
  • Serve the value as a line of https://<origin>/.well-known/typeship-verification.txt.

Then select Verify. Typeship checks verified origins again every day and turns OAuth sign-in off when the record and file are both gone. An issuer on a shared identity-provider domain, such as a default Auth0 or Okta tenant domain, cannot be verified; use a custom domain for your provider.

Which clients can connect

Your API acceptsClients that can set headers (Claude Code, Cursor, VS Code, Codex, Windsurf)Connectors in Claude.ai, Claude Desktop, and ChatGPT
Bearer token or API keyYes. Send the credential header.No. Connectors sign in with OAuth and cannot send a credential header.
Basic authenticationYes. Send Authorization: Basic with the encoded username and password.No.
OAuthYes, when the client supports MCP OAuth.Yes, when your identity provider accepts the client's registration and the endpoint as a resource.

The hosted endpoint forwards the OAuth access token your identity provider issues to your API. Your API must accept a token whose audience or resource is the hosted endpoint URL. For connectors, your provider must also allow the client to register, through dynamic client registration or a client ID metadata document.

Hosted or self-hosted

Hosted endpointYour package's server
Runs onTypeshipYour users' machines, or your Cloudflare account
SpecThe last accepted Generation's Spec Revision, or the latest successful Generation before the first mergeFrozen at generation
CredentialsAPI credentials received and forwarded per requestLocal: environment or CLI profile. Remote: verified MCP token plus application-owned API credentials.
Rate limit120/min per caller, 1,200/min per endpointNone built in
SetupConfigure the URL and its caller-supplied API credentialsLocal: build the package. Remote: configure createMcpHandler, then deploy.

Both share tool names, input schemas, argument checking, result shaping, the error contract, and the docs tools. Choose your deployment based on its authentication boundary and where API credentials may be processed. Upstream requests from the hosted endpoint time out after 30 seconds.

Protocol notes

  • Protocols 2026-07-28 and 2025-11-25 share the same tools, validation, authorization, pagination, and result limits. Clients select their protocol automatically; no environment flags are required.
  • Modern requests carry params._meta["io.modelcontextprotocol/protocolVersion"] and ["io.modelcontextprotocol/clientCapabilities"]. HTTP routing headers must match the body. Results carry resultType: "complete" and the server identity in _meta; discovery and tool lists include cache metadata.
  • Clients using 2025-11-25 send initialize, then notifications/initialized. Subsequent HTTP requests include MCP-Protocol-Version: 2025-11-25. HTTP is stateless: no session ID, SSE stream, resumability, or server-initiated requests. Tool results use the same content and structured content without the modern envelope. Browser account-linking prompts require a modern client; this compatibility path returns ACCOUNT_LINK_REQUIRED without making an API request.
  • An initialize that requests another revision, such as 2025-06-18, is answered with 2025-11-25, and the client decides whether to continue. Other requests with an unsupported revision return -32022 with supported versions in error.data. Mixing modern metadata with a legacy handshake is rejected.
  • Cancellation: on stdio, notifications/cancelled for an in-flight request suppresses its response; on HTTP, closing the connection is the signal.
  • The server exits when its stdin closes.
  • On stdio, one JSON-RPC message per line on stdin and stdout.
  • Operations that stream server-sent events are not exposed as tools (a tool result is one value, not a stream). For an operation that answers JSON by default and streams on request, the tool returns the complete JSON response, and setting its stream field (stream: true) is an INVALID_ARGUMENTS error, reported before anything is sent. Streams are still SDK methods and CLI commands, and generation reports which ones. Operations with binary or multipart request bodies are tools on the package's local server only; see Files.

On this page