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.
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.
publicallows clients and shared intermediaries to reuse the result across users.privaterestricts reuse to the requesting authorization context. When a cache store is shared, set the client'scachePartitionto 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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。