Custom Methods

Purpose and Scope

Use custom methods when an integration needs JSON-RPC behavior that is outside the MCP specification but still travels over the same MCP connection. The repository documentation defines a custom method as a JSON-RPC method outside the spec and requires a vendor namespace, such as a search provider prefix, instead of a bare generic name. That naming rule is the first compatibility boundary: it prevents a local extension from colliding with current or future MCP methods while preserving the same request, response, and notification mechanics used by the rest of the protocol.

Sources: docs/advanced/custom-methods.md

The custom-methods guide is intentionally a how-to, not a new transport or framework layer. You keep using the high-level server object for normal tools, resources, and prompts, then reach the embedded low-level server only for the method that MCP does not define. On the client side, you keep the same connected client and use the generic request and notification handler APIs when the SDK cannot infer schemas from a spec method name. This makes custom methods useful for private host features, experimental protocol extensions, or organization-specific control messages without forking the SDK.

Sources: docs/advanced/custom-methods.md, docs/advanced/custom-transports.md

Relevant Source Files

  • docs/advanced/custom-methods.md — Primary how-to for vendor-prefixed custom requests, custom notification sending, client-side calls, schemas, and validation behavior.
  • docs/advanced/custom-transports.md — Adjacent advanced guide showing that the same MCP request and notification messages can travel over any Transport implementation, including custom channels.
  • docs/_meta/CONVENTIONS.md — Documentation authoring contract that explains why guide pages are task-oriented, code-led, and written around observable results.
  • docs/.vitepress/theme/custom.css — Shared VitePress styling for the docs site, including wider code columns and admonition styling used by advanced guides.
  • docs/.vitepress/theme/index.ts — v2 docs theme entry that extends the default VitePress theme and installs the shared banner and CSS.
  • docs/v1/.vitepress/theme/index.ts — v1 docs theme entry that imports the same shared CSS, showing that presentation conventions are shared across documentation versions.

Sources: docs/advanced/custom-methods.md, docs/advanced/custom-transports.md, docs/_meta/CONVENTIONS.md, docs/.vitepress/theme/custom.css, docs/.vitepress/theme/index.ts, docs/v1/.vitepress/theme/index.ts

Core Primitives

A custom method has three important primitives: the method name, the schemas, and the connection context. The method name is a JSON-RPC string with a vendor prefix. The schemas describe incoming parameters and the outgoing result for requests, or incoming parameters only for notifications. The connection context matters because a handler can send a notification back to the same peer that made the request. This keeps progress, status, or side-channel updates scoped to the active request path instead of broadcasting them to unrelated clients.

Sources: docs/advanced/custom-methods.md

The guide uses Zod v4 schemas for the example, but the conceptual contract is broader: a non-spec method must give the SDK enough information to validate the wire payload and to type the handler or response. For spec methods, the SDK resolves schemas from the method name. For custom methods, there is no spec registry to consult, so the application supplies the schema bundle explicitly. That distinction is the main rule to remember when moving between built-in MCP operations and extension operations.

Sources: docs/advanced/custom-methods.md

Server-Side Flow

Register a vendor-prefixed request on the low-level server reached from an MCP server instance. The custom-methods guide shows a search method whose parameters include a string query and an integer limit with a default. The result is an object containing string items. When the handler runs, it receives parsed parameters rather than raw JSON. If the client sends the wrong shape, validation fails before handler code executes and the caller receives an invalid-parameters JSON-RPC error. That makes custom extensions safe to expose because malformed requests do not leak into business logic.

Sources: docs/advanced/custom-methods.md

Add the request handler with explicit parameter and result schemas.

import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
 
const SearchParams = z.object({ query: z.string(), limit: z.number().int().default(10) });
const SearchResult = z.object({ items: z.array(z.string()) });
 
const mcp = new McpServer({ name: 'acme-search', version: '1.0.0' });
 
mcp.server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async ({ query, limit }) => {
    return { items: Array.from({ length: limit }, (_, index) => `${query}-${index}`) };
});

The same method name can also become a progress channel by sending custom notifications from inside the request handler. The guide replaces the search handler with one that calls the request context notification helper before and after computing the result. Those notifications use another vendor-prefixed method name and contain their own parameters. Because the helper sends to the peer whose request is being handled, it fits request-scoped progress reporting: the client that initiated the search can observe start and completion without every connected client receiving the update.

Sources: docs/advanced/custom-methods.md

Send request-scoped progress notifications from the handler context.

mcp.server.setRequestHandler('acme/search', { params: SearchParams, result: SearchResult }, async ({ query, limit }, ctx) => {
    await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'start', pct: 0 } });
    const items = Array.from({ length: limit }, (_, index) => `${query}-${index}`);
    await ctx.mcpReq.notify({ method: 'acme/searchProgress', params: { stage: 'done', pct: 1 } });
    return { items };
});

Client-Side Flow

Call a custom request with the connected client’s generic request API. Unlike built-in MCP methods, the client cannot infer the result schema from the custom method name, so the caller passes the request object and the expected result schema together. The guide’s example calls the search method with a query and limit, then receives a validated object containing the generated items. This validation step protects the client as well as the server: a custom server response must match the declared result schema before the promise resolves to application code.

Sources: docs/advanced/custom-methods.md

Call the custom method and validate the response shape.

const result = await client.request({ method: 'acme/search', params: { query: 'mcp', limit: 3 } }, SearchResult);
console.log(result);

The expected output is a normal JavaScript object whose fields match the declared result schema.

{ items: [ 'mcp-0', 'mcp-1', 'mcp-2' ] }

Register notification handlers with the same schema discipline. A custom notification is the one-way mirror of a custom request: it has a vendor-prefixed method name and parameters, but no result. The receiving side installs a notification handler with a parameter schema, and the handler receives parsed parameters. In the progress example, the client defines fields such as stage and percent complete, then logs or reacts to those updates while the original custom request is still in flight.

Sources: docs/advanced/custom-methods.md

Handle progress notifications with an explicit parameter schema.

const SearchProgressParams = z.object({ stage: z.string(), pct: z.number() });
 
client.setNotificationHandler('acme/searchProgress', { params: SearchProgressParams }, params => {
    console.log(params);
});

Extension Negotiation and Transport Boundaries

Treat capability negotiation as the contract that tells a peer whether it may rely on an extension. A vendor-prefixed method prevents name collisions, but it does not by itself prove that the other side implements the extension. In practice, advertise extension support in the same initialization and capability-discovery style used elsewhere in MCP, then call the custom method only when the peer has declared compatible behavior. Keep the advertised capability name in the same vendor namespace as the method so operators can trace support, failure reports, and versioned extension behavior together.

Sources: docs/advanced/custom-methods.md

Custom methods are independent of the transport. The neighboring custom-transport guide defines a transport as the layer that moves JSON-RPC messages in both directions, with the SDK calling send for outbound messages and receiving inbound messages through callbacks. That means the same custom request can run over stdio, Streamable HTTP, an in-memory loopback, or a private byte stream if the transport obeys the SDK callback contract. Do not put method semantics into the transport; keep framing, delivery, close, and error signaling separate from the vendor extension.

Sources: docs/advanced/custom-transports.md

API Reference

TaskPublic entry pointSchema requirementResult behavior
Handle a custom requestmcp.server.setRequestHandler('vendor/name', { params, result }, handler)Parameter and result schemas are supplied by the applicationHandler receives parsed params and returns a validated result shape
Handle a spec requestsetRequestHandler('tools/call', handler)The SDK resolves schemas from the spec method nameUse the built-in method contract
Call a custom requestclient.request({ method, params }, ResultSchema)Caller supplies the expected result schemaPromise resolves after response validation
Send a custom notificationctx.mcpReq.notify({ method, params })Notification params should follow the vendor extension contractSends one-way message to the peer for the active request
Receive a custom notificationclient.setNotificationHandler('vendor/event', { params }, handler)Parameter schema is supplied by the applicationHandler receives parsed params and returns no result

Sources: docs/advanced/custom-methods.md

Edge Cases and Next Steps

The sharp edge is schema ownership. For built-in MCP methods, do not pass a custom schema bundle; the SDK already resolves the schema from the method name. For non-spec methods, always pass the schema bundle because the SDK has no built-in definition to use. If a caller sends a value with the wrong type, such as a number where the search query expects a string, the request fails before handler logic runs and the caller sees an invalid-parameters JSON-RPC error. Use that behavior as a test signal for every extension.

Sources: docs/advanced/custom-methods.md

Use this page with the low-level server and custom transport guides. The low-level server API is where custom request handlers live, while custom transports explain how those same JSON-RPC messages move over a channel the SDK does not know about. If the extension becomes broadly useful, document its vendor prefix, parameter schema, result schema, notification names, advertised capability, and versioning policy before depending on it from multiple hosts. Keep the normal MCP surface for portable features and reserve custom methods for private or experimental behavior that truly needs an extension point.

Sources: docs/advanced/custom-methods.md, docs/advanced/custom-transports.md