mCP 2.0

mCP 2.0

Demonstrates stateful application patterns on the stateless MCP 2026-07-28 protocol, enabling request-scoped state, multi round-trip confirmations, and streaming progress updates across independent tool calls.

Category
Visit Server

README

Stateless MCP, stateful application

A minimal JavaScript project demonstrating the main MCP 2026-07-28 request/response patterns with the MCP TypeScript SDK v2.

The demo covers:

  • Stateless HTTP requests with no Mcp-Session-Id.
  • Application-managed state addressed by explicit handles.
  • Multi round-trip requests for user confirmation.
  • Progress updates scoped to an individual operation.
  • Cacheable tool discovery with TTL and sharing scope.

Run it

Requires Node.js 20 or newer.

npm install
npm run demo

The command starts a server on an available local port, exercises every scenario, prints the results, and shuts the server down. The deletion example uses an in-memory virtual file set and never touches files on disk.

Run the integration tests with:

npm test

To leave the server running for another MCP client:

npm run server

The endpoint is http://127.0.0.1:3000/mcp. Set PORT to override the port.

1. Stateless requests

MCP 2026-07-28 removes protocol-level HTTP sessions. This project creates a fresh McpServer for each request, so any request can reach any server instance:

const mcpHandler = createMcpHandler(
  () => createStateServer(demoFiles),
  {
    legacy: "reject",
    onerror: (error) => console.error("MCP error:", error),
  },
);

The client explicitly selects the modern protocol revision:

const client = new Client(
  { name: "state-demo-client", version: "0.1.0" },
  {
    capabilities: { elicitation: { form: {} } },
    versionNegotiation: {
      mode: { pin: "2026-07-28" },
    },
  },
);

State stored inside one McpServer instance therefore disappears after that request. Calling the demo's ephemeral counter twice produces 1 from two different server instances.

2. Stateful application data

Stateless MCP does not require a stateless application. Durable state belongs outside the per-request MCP server and is selected using an explicit handle:

const countersById = new Map();

server.registerTool(
  "create-counter",
  {
    outputSchema: z.object({ counterId: z.uuid(), value: z.number() }),
  },
  async () => {
    const counterId = randomUUID();
    countersById.set(counterId, 0);
    const structuredContent = { counterId, value: 0 };

    return {
      content: [{ type: "text", text: JSON.stringify(structuredContent) }],
      structuredContent,
    };
  },
);

The client carries the handle between otherwise independent calls:

const created = await client.callTool({ name: "create-counter" });
const { counterId } = created.structuredContent;

await client.callTool({
  name: "increment-counter",
  arguments: { counterId },
});

The in-memory Map is only a stand-in. A production server should use a database or shared store, bind handles to the authenticated principal, and enforce authorization and expiry on every lookup.

3. Multi round-trip confirmation

A tool that needs more input returns input_required. The client obtains the answer and retries the original request with inputResponses, so the server does not initiate a second JSON-RPC request on the operation stream.

server.registerTool(
  "delete-files",
  {
    inputSchema: z.object({ files: z.array(z.string()).min(1) }),
    annotations: { destructiveHint: true },
  },
  async ({ files }, ctx) => {
    const confirmation = acceptedContent(
      ctx.mcpReq.inputResponses,
      "confirm",
      confirmationSchema,
    );

    if (confirmation === undefined) {
      return inputRequired({
        inputRequests: {
          confirm: inputRequired.elicit({
            message: `Delete ${files.length} virtual files?`,
            requestedSchema: confirmationSchema,
          }),
        },
      });
    }

    if (!confirmation.confirm) {
      return {
        content: [{ type: "text", text: "Cancelled" }],
        structuredContent: { status: "cancelled", deleted: [] },
      };
    }

    const deleted = files.filter((file) => demoFiles.delete(file));
    return {
      content: [{ type: "text", text: `Deleted: ${deleted.join(", ")}` }],
      structuredContent: { status: "deleted", deleted },
    };
  },
);

The SDK can fulfil the request and retry automatically using the normal elicitation handler:

client.setRequestHandler("elicitation/create", async (request) => {
  const confirm = await askUser(request.params.message);
  return {
    action: "accept",
    content: { confirm },
  };
});

Confirmation is user experience, not authorization. The server must still authenticate the caller and independently enforce permission to delete files.

4. Request-scoped progress

Progress remains attached to the request that started the work. Concurrent operations receive only their own updates instead of sharing one global event stream.

server.registerTool(
  "run-work",
  { inputSchema: z.object({ job: z.string() }) },
  async ({ job }, ctx) => {
    const progressToken = ctx.mcpReq._meta?.progressToken;

    for (const progress of [10, 30, 70]) {
      if (progressToken !== undefined) {
        await ctx.mcpReq.notify({
          method: "notifications/progress",
          params: {
            progressToken,
            progress,
            total: 100,
            message: `${job}: ${progress}%`,
          },
        });
      }
    }

    return {
      content: [{ type: "text", text: `${job}: complete` }],
      structuredContent: { job, status: "complete" },
    };
  },
);

Each client call supplies its own progress callback:

await Promise.all([
  client.callTool(
    { name: "run-work", arguments: { job: "alpha" } },
    { onprogress: (update) => alphaProgress.push(update) },
  ),
  client.callTool(
    { name: "run-work", arguments: { job: "beta" } },
    { onprogress: (update) => betaProgress.push(update) },
  ),
]);

5. Cacheability

Cacheable responses include a freshness lifetime and sharing policy. This reduces repeated discovery traffic when one agent connects to many MCP servers.

This server marks its tool catalog as reusable for five minutes:

const server = new McpServer(
  { name: "mcp-state-demo", version: "0.1.0" },
  {
    cacheHints: {
      "tools/list": {
        ttlMs: 300_000,
        cacheScope: "public",
      },
    },
  },
);

The client uses fresh entries automatically:

await client.listTools(); // network request; stores the result
await client.listTools(); // cache hit; no network request

await client.listTools(undefined, {
  cacheMode: "refresh",
}); // forces a network request and updates the cache

The fields apply to tools/list, prompts/list, resources/list, resources/templates/list, and resources/read results.

  • public allows clients and shared intermediaries to reuse the result across users.
  • private restricts reuse to the requesting authorization context. When a cache store is shared, set the client's cachePartition to a stable principal identifier.

A TTL is a freshness estimate. List-change notifications can invalidate cached catalogs before their TTL expires.

Project structure

src/server.js       MCP server and tool implementations
src/demo.js         Client exercising all five scenarios
test/state.test.js  Integration tests for every scenario

The MCP packages are pinned to 2.0.0, including the inputRequired, acceptedContent, caching, and request-scoped progress APIs used here.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured