Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
159 changed files with 69131 additions and 9001 deletions

94
examples/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,94 @@
# Contributing an example
Each `examples/<story>/` directory is a tiny `@mcp-examples/<story>` workspace
package containing a `server.ts` / `client.ts` pair. The pair is a
self-verifying e2e test: the client connects, asserts results, and exits
non-zero on any mismatch. `pnpm run:examples` runs every story over its
configured transport × era legs and is part of the per-PR CI gate.
## Typical shape
Examples are **compiled documentation**. Every story shows the SDK transport
setup **inline** — no helper hides `serveStdio`, `createMcpHandler`, `Client`,
or transport construction. The duplication is the feature: when the public API
changes, 25 compile errors flag 25 doc pages.
Only the part a reader is _not_ here to learn — argv parsing — is shared, via
`parseExampleArgs` / `check` / `siblingPath` from `@mcp-examples/shared` (a
workspace package, so it reads as scaffolding, not part of the example). The
demo OAuth provider and `InMemoryEventStore` live at the
`@mcp-examples/shared/auth` subpath so the args-only root barrel does not pull
better-auth/express/better-sqlite3 into every story.
Most stories follow the skeleton below; deviate freely when the story calls for
it (HTTP-only auth, sessionful transports, framework adapters, etc.).
### `server.ts`
```ts
import { createServer } from 'node:http';
import { parseExampleArgs } from '@mcp-examples/shared';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
function buildServer(): McpServer {
const server = new McpServer({ name: '<story>-example', version: '1.0.0' });
// … register tools / resources / prompts here …
return server;
}
const { transport, port } = parseExampleArgs();
if (transport === 'stdio') {
void serveStdio(buildServer);
console.error('[server] serving over stdio');
} else {
const handler = createMcpHandler(buildServer);
createServer(toNodeHandler(handler)).listen(port, () => {
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
});
}
```
### `client.ts`
```ts
import { check, parseExampleArgs, siblingPath } from '@mcp-examples/shared';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
const { transport, url, era } = parseExampleArgs();
const client = new Client({ name: '<story>-example-client', version: '1.0.0' }, { versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' } });
await client.connect(transport === 'stdio' ? new StdioClientTransport({ command: 'npx', args: ['-y', 'tsx', siblingPath(import.meta.url, 'server.ts')] }) : new StreamableHTTPClientTransport(new URL(url)));
// … example body — drive the server and assert with `check.*` …
await client.close();
```
The body uses top-level `await`. A `check.*` failure throws, Node prints the
error and exits 1; on success `client.close()` releases the last handle and
Node exits 0. `pnpm run:examples` reports PASS/FAIL from the exit code (a
timeout is reported as a hang — investigate it as a possible unclosed handle).
## Import rules (lint-enforced)
Stories may import from:
- `@modelcontextprotocol/{server,client,core,node,express,hono}` and their published
subpath exports (e.g. `@modelcontextprotocol/server/stdio`)
- `@mcp-examples/shared` (args/assert) and `@mcp-examples/shared/auth` (demo OAuth + `InMemoryEventStore`)
- third-party packages a consumer would `npm install`
Stories may **not** import from:
- `@modelcontextprotocol/core-internal` or `@modelcontextprotocol/core-internal/*` (internal barrel)
- `@modelcontextprotocol/*/src/*` or `@modelcontextprotocol/*/dist/*` (deep paths)
- `@modelcontextprotocol/test-helpers`
- any relative path that hides the SDK transport setup behind a shared helper
`@mcp-examples/shared` itself must never import from a story package (one-way).

172
examples/README.md Normal file
View file

@ -0,0 +1,172 @@
# MCP TypeScript SDK examples
One **story** per directory. Every story is a runnable, self-verifying client/server pair: `server.ts` is what you would deploy, `client.ts` is what a host would write — it connects, exercises the feature with the public client API, asserts results, and exits 0. CI runs every
pair over every transport it supports (`scripts/examples/run-examples.ts`); a non-zero exit fails the build.
Each story is its own private workspace package (`@mcp-examples/<story>`). Run any pair from the repo root:
```bash
# stdio (the client spawns the server itself):
pnpm --filter @mcp-examples/<story> client
# Streamable HTTP (two terminals):
pnpm --filter @mcp-examples/<story> server -- --http --port 3000
pnpm --filter @mcp-examples/<story> client -- --http http://127.0.0.1:3000/mcp
```
Add `-- --legacy` to the client command for the 2025-era handshake.
## Start here
| Story | What it teaches |
| ------------------------------------- | ------------------------------------------------------------------------ |
| [`tools/`](./tools/README.md) | Register tools, infer input/output schemas, call them, structured output |
| [`prompts/`](./prompts/README.md) | Prompts + argument completion |
| [`resources/`](./resources/README.md) | Static + templated resources, list/read |
| [`dual-era/`](./dual-era/README.md) | One factory, both protocol eras, both transports |
## Feature stories
| Story | What it teaches | Transports | Era |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------ | -------------- |
| [`mrtr/`](./mrtr/README.md) | Multi-round-trip write-once tool, secure `requestState` | stdio + http | modern |
| [`subscriptions/`](./subscriptions/README.md) | `subscriptions/listen`: `client.listen()` + auto-open, `handler.notify` / `ServerEventBus` | stdio + http | modern |
| [`streaming/`](./streaming/README.md) | In-flight progress, logging, cancellation | stdio + http | dual |
| [`elicitation/`](./elicitation/README.md) | Elicitation (form + URL mode), both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`sampling/`](./sampling/README.md) | Tool that requests LLM sampling from the client, both eras: push-style on 2025, `inputRequired` on 2026 | stdio + http | dual |
| [`stickynotes/`](./stickynotes/README.md) | "Real app" capstone: tools mutate state, a resource per note, listChanged, elicitation-confirmed clear | stdio + http | dual |
| [`caching/`](./caching/README.md) | `cacheHints` stamping on cacheable results (2026-07-28) | stdio + http | modern |
| [`gateway/`](./gateway/README.md) | `connect({ prior })` — probe once, zero-round-trip connect for every worker (gateway pattern) | http | modern |
| [`custom-methods/`](./custom-methods/README.md) | Vendor-prefixed methods + custom notifications | stdio + http | dual |
| [`schema-validators/`](./schema-validators/README.md) | ArkType, Valibot, Zod, and `outputSchema` | stdio + http | dual |
| [`custom-version/`](./custom-version/README.md) | `supportedProtocolVersions` / version negotiation | stdio + http | legacy |
| [`parallel-calls/`](./parallel-calls/README.md) | Multiple clients / parallel tool calls, per-client notifications | stdio + http | dual |
| [`legacy-routing/`](./legacy-routing/README.md) | `isLegacyRequest` in front of an existing sessionful 1.x deployment + a strict modern entry on one port | http | dual (in-body) |
| [`bearer-auth/`](./bearer-auth/README.md) | Resource server with bearer token; `401` + `WWW-Authenticate` | http | dual |
| [`oauth/`](./oauth/README.md) | OAuth `authorization_code`: in-repo AS (auto-consent) + headless redirect-following client | http | dual |
| [`oauth-client-credentials/`](./oauth-client-credentials/README.md) | OAuth `client_credentials` (machine-to-machine): in-repo AS + `ClientCredentialsProvider` | http | dual |
| [`scoped-tools/`](./scoped-tools/README.md) | Per-tool scope on `createMcpHandler` — bearer-verify gate + handler-level `ctx.http?.authInfo` checks | http | modern |
## HTTP hosting variants
| Story | What it teaches | Transports | Era |
| --------------------------------------------------- | ------------------------------------------------------------- | ---------- | -------------- |
| [`stateless-legacy/`](./stateless-legacy/README.md) | `createMcpHandler` default posture (the minimal deployment) | http | dual (in-body) |
| [`json-response/`](./json-response/README.md) | `createMcpHandler({ responseMode: 'json' })` | http | modern |
| [`hono/`](./hono/README.md) | `createMcpHandler(...).fetch` on Hono / web-standard runtimes | http | dual |
| [`sse-polling/`](./sse-polling/README.md) | SEP-1699 SSE polling/resumption (sessionful 2025) | http | legacy |
| [`standalone-get/`](./standalone-get/README.md) | Standalone GET stream + `listChanged` push (sessionful 2025) | http | legacy |
`dual (in-body)` = the client connects to both eras inside one runner invocation; the story demonstrates one server serving both side by side.
## Excluded
| Directory | What it is | Why not in CI |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [`repl/`](./repl/README.md) | Fully-featured HTTP playground server + readline client | Interactive — `client.ts` reads from stdin. Run manually in two terminals. |
| [`guides/`](./guides/README.md) | Snippet collections synced into `docs/server.md` and `docs/client.md` | Typecheck-only; not a runnable pair. |
| `server-quickstart/`, `client-quickstart/` | Website-tutorial sources | External network / API key; typecheck-only. |
| `shared/` | Argv/assert scaffold (`parseExampleArgs`/`check`/`siblingPath`); demo OAuth provider + `InMemoryEventStore` at the `./auth` subpath | Not a story — imported by every story as scaffolding. |
## Multi-node deployment patterns
When deploying MCP servers in a horizontally scaled environment (multiple server instances), there are a few different options that can be useful for different use cases:
- **Stateless mode** - no need to maintain state between calls.
- **Persistent storage mode** - state stored in a database; any node can handle a session.
- **Local state with message routing** - stateful nodes + pub/sub routing for a session.
### Stateless mode
To enable stateless mode, configure the `NodeStreamableHTTPServerTransport` with:
```typescript
sessionIdGenerator: undefined;
```
```
┌─────────────────────────────────────────────┐
│ Client │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ MCP Server #1 │ │ MCP Server #2 │
│ (Node.js) │ │ (Node.js) │
└─────────────────┘ └─────────────────────┘
```
### Persistent storage mode
Configure the transport with session management, but use an external event store:
```typescript
sessionIdGenerator: () => randomUUID(),
eventStore: databaseEventStore
```
```
┌─────────────────────────────────────────────┐
│ Client │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ MCP Server #1 │ │ MCP Server #2 │
│ (Node.js) │ │ (Node.js) │
└─────────────────┘ └─────────────────────┘
│ │
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ Database (PostgreSQL) │
│ │
│ • Session state │
│ • Event storage for resumability │
└─────────────────────────────────────────────┘
```
### Streamable HTTP with distributed message routing
For scenarios where local in-memory state must be maintained on specific nodes, combine Streamable HTTP with pub/sub routing so one node can terminate the client connection while another node owns the session state.
```
┌─────────────────────────────────────────────┐
│ Client │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ MCP Server #1 │◄───►│ MCP Server #2 │
│ (Has Session A) │ │ (Has Session B) │
└─────────────────┘ └─────────────────────┘
▲│ ▲│
│▼ │▼
┌─────────────────────────────────────────────┐
│ Message Queue / Pub-Sub │
│ │
│ • Session ownership registry │
│ • Bidirectional message routing │
│ • Request/response forwarding │
└─────────────────────────────────────────────┘
```
## Backwards compatibility (Streamable HTTP ↔ legacy SSE)
A client that needs to fall back from Streamable HTTP to the legacy HTTP+SSE transport (for servers that only implement the older transport) follows the [`connect_sseFallback`](../docs/client.md#sse-fallback-for-legacy-servers) recipe in the client guide — try
`StreamableHTTPClientTransport` first, fall back to `SSEClientTransport` on a 4xx. There is no runnable pair for this in `examples/` (the legacy SSE server transport is deprecated); the snippet in `guides/clientGuide.examples.ts` is the complete pattern.

View file

@ -0,0 +1,6 @@
# bearer-auth
Resource-server-only auth: `requireBearerAuth` from `@modelcontextprotocol/express` in front of `createMcpHandler`. The client asserts `401` + `WWW-Authenticate` without a token, and that the verified `authInfo` reaches the factory (`ctx.authInfo`) with
one.
**HTTP-only** by definition. The full interactive OAuth set lives under `../oauth/` (run headlessly in CI via the demo AS's auto-consent mode).

View file

@ -0,0 +1,31 @@
/**
* Asserts a bare request is `401` with a `WWW-Authenticate` header, and that
* a request with `Authorization: Bearer demo-token` reaches the `whoami` tool
* with the verified `authInfo`.
*/
import { check, parseExampleArgs } from '@mcp-examples/shared';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const { url, era } = parseExampleArgs();
// Unauthenticated → 401 + WWW-Authenticate.
const unauth = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' })
});
check.equal(unauth.status, 401);
check.match(unauth.headers.get('www-authenticate') ?? '', /Bearer/);
// Authenticated → 200 and the tool sees the authInfo. Bearer auth is
// HTTP-layer and era-agnostic; the client honours `--legacy` via `era`.
const client = new Client(
{ name: 'bearer-auth-example-client', version: '1.0.0' },
{ versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' } }
);
await client.connect(new StreamableHTTPClientTransport(new URL(url), { authProvider: { token: async () => 'demo-token' } }));
const result = await client.callTool({ name: 'whoami', arguments: {} });
check.equal(result.content?.[0]?.type === 'text' ? result.content[0].text : '', 'client=demo-client');
await client.close();

View file

@ -0,0 +1,28 @@
{
"name": "@mcp-examples/bearer-auth",
"private": true,
"type": "module",
"scripts": {
"server": "tsx server.ts",
"client": "tsx client.ts"
},
"dependencies": {
"@mcp-examples/shared": "workspace:*",
"@modelcontextprotocol/client": "workspace:*",
"@modelcontextprotocol/express": "workspace:*",
"@modelcontextprotocol/node": "workspace:*",
"@modelcontextprotocol/server": "workspace:*",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
"tsx": "catalog:devTools"
},
"example": {
"transports": [
"http"
],
"era": "dual",
"path": "/mcp",
"//": "Bearer auth + 401/WWW-Authenticate is HTTP-layer and era-agnostic; the client honours --legacy via the inline versionNegotiation branch."
}
}

View file

@ -0,0 +1,51 @@
/**
* Minimal Resource-Server-only auth: `requireBearerAuth` + `OAuthTokenVerifier`
* in front of `createMcpHandler`. The verifier accepts a single static
* `demo-token`; the verified `authInfo` reaches the factory as `ctx.authInfo`.
*
* No Authorization Server here, and no metadata endpoints — see `examples/oauth/`
* for the full RS + AS discovery flow. HTTP-only by definition.
*/
import { parseExampleArgs } from '@mcp-examples/shared';
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import { createMcpExpressApp, requireBearerAuth } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import type { AuthInfo, McpServerFactory } from '@modelcontextprotocol/server';
import { createMcpHandler, McpServer, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const buildServer: McpServerFactory = ctx => {
const server = new McpServer({ name: 'bearer-auth-example', version: '1.0.0' });
server.registerTool('whoami', { description: 'Returns the authenticated subject.', inputSchema: z.object({}) }, async () => ({
content: [{ type: 'text', text: `client=${ctx.authInfo?.clientId ?? 'anon'}` }]
}));
return server;
};
const { port } = parseExampleArgs();
// Replace with JWT verification, RFC 7662 introspection, etc.
const staticTokenVerifier: OAuthTokenVerifier = {
async verifyAccessToken(token): Promise<AuthInfo> {
if (token !== 'demo-token') {
throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token');
}
return { token, clientId: 'demo-client', scopes: ['mcp'], expiresAt: Math.floor(Date.now() / 1000) + 3600 };
}
};
// Bearer auth is HTTP-layer (no stdio arm). The MCP handler is the canonical
// `createMcpHandler(buildServer)`; the Express auth middleware in front of it
// is the point of this story.
const handler = createMcpHandler(buildServer);
const app = createMcpExpressApp();
const auth = requireBearerAuth({ verifier: staticTokenVerifier, requiredScopes: ['mcp'] });
// `requireBearerAuth` sets `req.auth`; `toNodeHandler` reads it and passes it
// to the factory as `ctx.authInfo`.
const node = toNodeHandler(handler);
app.all('/mcp', auth, (req, res) => void node(req, res, req.body));
app.listen(port, () => {
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
});

View file

@ -0,0 +1,52 @@
# caching
`CacheableResult` freshness hints (protocol revision 2026-07-28). The server declares hints at two layers — a per-registration `cacheHint` on the resource and server-level `ServerOptions.cacheHints` — and the SDK resolves most-specific-author-first (handler-return fields would
take precedence over both) and stamps `ttlMs`/`cacheScope` on the wire toward modern clients only. The client honours the stamped values: a still-fresh held entry is served without a round trip.
```bash
pnpm tsx examples/caching/client.ts
```
The client calls `listTools()` and `readResource()` twice each; the second of each pair is served from the response cache. The server exposes a `request-count` tool (how many `tools/list` requests reached it) and a `read-count` tool (how many times the resource handler ran), so the example asserts each counter is unchanged after the cache-served call and increments after `cacheMode: 'refresh'`.
## `cacheMode`
Per-call control on the cacheable verbs (`listTools()` / `listPrompts()` / `listResources()` / `listResourceTemplates()` / `readResource()`):
```ts
await client.readResource({ uri: 'config://app' }); // 'use' (default): serve from cache if fresh
await client.readResource({ uri: 'config://app' }, { cacheMode: 'refresh' }); // always fetch, then re-store
await client.readResource({ uri: 'config://app' }, { cacheMode: 'bypass' }); // fetch; do not read or write the cache
```
A `list_changed` notification still evicts immediately regardless of TTL.
## Custom store
The default per-client `InMemoryResponseCacheStore` (bounded at 512 entries by default) is enough for most hosts. To back the cache with something persistent (Redis, KV, IndexedDB), implement the five-method `ResponseCacheStore` interface — the store is a dumb keyed-value carrier; freshness and partitioning are the client's job:
```ts
import type { CacheEntry, CacheKey, CacheScope, ResponseCacheStore } from '@modelcontextprotocol/client';
class MyStore implements ResponseCacheStore {
async get(key: CacheKey): Promise<CacheEntry | undefined> {
/* read {value, stamp, expiresAt, scope} from your backend */
}
async set(key: CacheKey, entry: { value: unknown; expiresAt?: number; scope?: CacheScope }): Promise<number> {
/* write entry under key; return a monotonically-increasing stamp */
}
async delete(key: CacheKey): Promise<void> {
/* drop the single entry under key (no-op if absent) */
}
async evict(method: string): Promise<void> {
/* drop every entry whose key.method === method (across every partition) */
}
async clear(): Promise<void> {
/* drop everything */
}
}
const client = new Client({ name: 'host', version: '1.0.0' }, { responseCacheStore: new MyStore(), cachePartition: principalId });
```
The SDK scopes every entry by the connected server's identity automatically — you do not encode server identity into `cachePartition` or the store key yourself. When one store backs several principals against the same server, set `ClientOptions.cachePartition` to a stable identity of the authorization context (e.g. the auth subject) so `'private'`-scoped entries are isolated per principal; `'public'`-scoped entries are shared within the connected server's namespace automatically. Note `serverInfo` is self-reported, so a server that deliberately impersonates another's `name`/`version` shares its `'public'` slot; the per-principal isolation holds regardless.

View file

@ -0,0 +1,78 @@
/**
* Reads the cache hints emitted on cacheable results (2026-07-28 connections
* only) and asserts the client honours them: a still-fresh cached entry is
* served without a round trip.
*/
import { check, parseExampleArgs, siblingPath } from '@mcp-examples/shared';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
interface Cacheable {
ttlMs?: number;
cacheScope?: 'public' | 'private';
}
async function callCount(client: Client, name: 'read-count' | 'request-count'): Promise<number> {
const r = await client.callTool({ name });
return Number((r.content[0] as { text: string }).text);
}
const { transport, url, era } = parseExampleArgs();
const client = new Client(
{ name: 'caching-example-client', version: '1.0.0' },
{ versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' } }
);
await (transport === 'stdio'
? client.connect(new StdioClientTransport({ command: 'npx', args: ['-y', 'tsx', siblingPath(import.meta.url, 'server.ts')] }))
: client.connect(new StreamableHTTPClientTransport(new URL(url))));
check.equal(client.getNegotiatedProtocolVersion(), '2026-07-28');
// The server stamps `tools/list` with `ttlMs: 30_000, cacheScope: 'public'`.
const tools = (await client.listTools()) as Cacheable & Awaited<ReturnType<typeof client.listTools>>;
check.equal(tools.ttlMs, 30_000);
check.equal(tools.cacheScope, 'public');
// `request-count` proves the wire was reached exactly once.
check.equal(await callCount(client, 'request-count'), 1);
// The second call is served from the response cache: the server-side
// `tools/list` counter is unchanged, and the result is a fresh copy of the
// held entry (so mutating it cannot reach the cache).
const toolsAgain = await client.listTools();
check.deepEqual(
toolsAgain.tools.map(t => t.name),
tools.tools.map(t => t.name)
);
check.equal(await callCount(client, 'request-count'), 1);
// `cacheMode: 'refresh'` always fetches and re-stores: the counter moves.
await client.listTools(undefined, { cacheMode: 'refresh' });
check.equal(await callCount(client, 'request-count'), 2);
const resources = (await client.listResources()) as Cacheable & Awaited<ReturnType<typeof client.listResources>>;
check.equal(resources.ttlMs, 5000);
check.equal(resources.cacheScope, 'public');
// `readResource`: the resource handler counts how many times it ran, and
// the `read-count` tool exposes that counter.
const read = (await client.readResource({ uri: 'config://app' })) as Cacheable & Awaited<ReturnType<typeof client.readResource>>;
check.equal(read.ttlMs, 60_000);
check.equal(read.cacheScope, 'private');
check.equal(await callCount(client, 'read-count'), 1);
// Within TTL, default `cacheMode: 'use'` → served from cache; the server
// handler does not run.
await client.readResource({ uri: 'config://app' });
check.equal(await callCount(client, 'read-count'), 1);
// `cacheMode: 'refresh'` always fetches and re-stores.
await client.readResource({ uri: 'config://app' }, { cacheMode: 'refresh' });
check.equal(await callCount(client, 'read-count'), 2);
// After the refresh the entry is fresh again — back to cache-served.
await client.readResource({ uri: 'config://app' });
check.equal(await callCount(client, 'read-count'), 2);
await client.close();

View file

@ -0,0 +1,22 @@
{
"name": "@mcp-examples/caching",
"private": true,
"type": "module",
"scripts": {
"server": "tsx server.ts",
"client": "tsx client.ts"
},
"dependencies": {
"@mcp-examples/shared": "workspace:*",
"@modelcontextprotocol/client": "workspace:*",
"@modelcontextprotocol/node": "workspace:*",
"@modelcontextprotocol/server": "workspace:*"
},
"devDependencies": {
"tsx": "catalog:devTools"
},
"example": {
"era": "modern",
"//": "cacheHints (ttlMs / cacheScope on cacheable results) are emitted only toward 2026-era clients."
}
}

102
examples/caching/server.ts Normal file
View file

@ -0,0 +1,102 @@
/**
* Cache hints (`CacheableResult`, protocol revision 2026-07-28).
*
* The 2026-07-28 revision requires `ttlMs`/`cacheScope` on the cacheable
* result types (the list operations and `resources/read`). The values are
* resolved most-specific-author-first:
*
* 1. fields the handler returns on the result itself,
* 2. a per-registration `cacheHint` (here: the resource's read result),
* 3. the server-level per-operation `ServerOptions.cacheHints`,
* 4. the conservative defaults (`ttlMs: 0`, `cacheScope: 'private'`).
*
* The fields are emitted ONLY toward 2026-era clients — a 2025-era response
* is byte-for-byte unchanged. One binary, either transport.
*/
import { createServer } from 'node:http';
import { parseExampleArgs } from '@mcp-examples/shared';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
// Module-level (process-wide) counters so the values survive the stateless
// HTTP leg (fresh `buildServer()` per request) as well as stdio's single
// per-connection instance. The client asserts against these to prove a
// cache-served call never reached the server.
let readCount = 0;
let listCount = 0;
function buildServer(): McpServer {
const server = new McpServer(
{ name: 'caching-example', version: '1.0.0' },
{
// Server-level per-operation hints: any list/read result that does not
// override a field gets these.
cacheHints: {
'resources/list': { ttlMs: 5000, cacheScope: 'public' },
'tools/list': { ttlMs: 30_000, cacheScope: 'public' }
}
}
);
// A direct resource carrying a per-registration hint that wins for its
// own resources/read result.
server.registerResource(
'app-config',
'config://app',
{
mimeType: 'application/json',
description: 'Static application config (rarely changes)',
cacheHint: { ttlMs: 60_000, cacheScope: 'private' }
},
async uri => {
readCount++;
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: '{"feature":true}' }] };
}
);
// A tool, so tools/list has something to cache.
server.registerTool('noop', { description: 'no-op' }, async () => ({ content: [{ type: 'text', text: 'ok' }] }));
// Exposes the server-side `resources/read` invocation count so the client
// can assert that a cache-served call did not reach the wire.
server.registerTool('read-count', { description: 'Number of resources/read calls that reached this server' }, async () => ({
content: [{ type: 'text', text: String(readCount) }]
}));
// Exposes the server-side `tools/list` invocation count.
server.registerTool('request-count', { description: 'Number of tools/list requests that reached this server' }, async () => ({
content: [{ type: 'text', text: String(listCount) }]
}));
// Wrap the auto-generated `tools/list` handler so the example can prove a
// cache-served `listTools()` never reached the wire. `McpServer` registers
// the handler lazily on the first `registerTool()`; we re-seat it here so
// every dispatch increments `listCount` before delegating to the original.
// (Reaches the underlying request-handler map directly — there is no public
// wrapper hook; acceptable for an instrumentation example.)
const handlers = (server.server as unknown as { _requestHandlers: Map<string, (...a: unknown[]) => Promise<unknown>> })
._requestHandlers;
const original = handlers.get('tools/list');
if (original) {
handlers.set('tools/list', (...a) => {
listCount++;
return original(...a);
});
}
return server;
}
const { transport, port } = parseExampleArgs();
if (transport === 'stdio') {
void serveStdio(buildServer);
console.error('[server] serving over stdio');
} else {
const handler = createMcpHandler(buildServer);
createServer(toNodeHandler(handler)).listen(port, () => {
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
});
}

View file

@ -1,51 +0,0 @@
# MCP TypeScript SDK Examples (Client)
This directory contains runnable MCP **client** examples built with `@modelcontextprotocol/client`.
For server examples, see [`../server/README.md`](../server/README.md). For guided docs, see [`../../docs/client.md`](../../docs/client.md).
## Running examples
From the repo root:
```bash
pnpm install
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleStreamableHttp.ts
```
Or, from within this package:
```bash
cd examples/client
pnpm tsx src/simpleStreamableHttp.ts
```
Most clients expect a server to be running. Start one from [`../server/README.md`](../server/README.md) (for example `src/simpleStreamableHttp.ts` in `examples/server`).
## Example index
| Scenario | Description | File |
| --------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Interactive Streamable HTTP client | CLI client that exercises tools/resources/prompts, notifications, and elicitation. | [`src/simpleStreamableHttp.ts`](src/simpleStreamableHttp.ts) |
| Backwards-compatible client (Streamable HTTP → SSE) | Tries Streamable HTTP first, falls back to legacy SSE on 4xx responses. | [`src/streamableHttpWithSseFallbackClient.ts`](src/streamableHttpWithSseFallbackClient.ts) |
| SSE polling client (legacy) | Polls a legacy HTTP+SSE server and demonstrates notification handling. | [`src/ssePollingClient.ts`](src/ssePollingClient.ts) |
| Parallel tool calls | Runs multiple tool calls in parallel. | [`src/parallelToolCallsClient.ts`](src/parallelToolCallsClient.ts) |
| Multiple clients in parallel | Connects multiple clients concurrently to the same server. | [`src/multipleClientsParallel.ts`](src/multipleClientsParallel.ts) |
| OAuth client (interactive) | OAuth-enabled client (dynamic registration, auth flow). | [`src/simpleOAuthClient.ts`](src/simpleOAuthClient.ts) |
| OAuth provider helper | Demonstrates reusable OAuth providers. | [`src/simpleOAuthClientProvider.ts`](src/simpleOAuthClientProvider.ts) |
| Client credentials (M2M) | Machine-to-machine OAuth client credentials example. | [`src/simpleClientCredentials.ts`](src/simpleClientCredentials.ts) |
| URL elicitation client | Drives URL-mode elicitation flows (sensitive input in a browser). | [`src/elicitationUrlExample.ts`](src/elicitationUrlExample.ts) |
## URL elicitation example (server + client)
Run the server first:
```bash
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/elicitationUrlExample.ts
```
Then run the client:
```bash
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/elicitationUrlExample.ts
```

View file

@ -1,14 +0,0 @@
// @ts-check
import baseConfig from '@modelcontextprotocol/eslint-config';
export default [
...baseConfig,
{
files: ['src/**/*.{ts,tsx,js,jsx,mts,cts}'],
rules: {
// Allow console statements in examples only
'no-console': 'off'
}
}
];

View file

@ -1,44 +0,0 @@
{
"name": "@modelcontextprotocol/examples-client",
"private": true,
"version": "2.0.0-alpha.0",
"description": "Model Context Protocol implementation for TypeScript",
"license": "MIT",
"author": "Anthropic, PBC (https://anthropic.com)",
"homepage": "https://modelcontextprotocol.io",
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
},
"engines": {
"node": ">=20"
},
"keywords": [
"modelcontextprotocol",
"mcp"
],
"scripts": {
"typecheck": "tsgo -p tsconfig.json --noEmit",
"build": "tsdown",
"build:watch": "tsdown --watch",
"prepack": "pnpm run build:esm && pnpm run build:cjs",
"lint": "eslint src/ && prettier --ignore-path ../../.prettierignore --check .",
"lint:fix": "eslint src/ --fix && prettier --ignore-path ../../.prettierignore --write .",
"check": "pnpm run typecheck && pnpm run lint"
},
"dependencies": {
"@modelcontextprotocol/client": "workspace:^",
"ajv": "catalog:runtimeShared",
"open": "^11.0.0",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
"@modelcontextprotocol/eslint-config": "workspace:^",
"@modelcontextprotocol/examples-shared": "workspace:^",
"@modelcontextprotocol/tsconfig": "workspace:^",
"@modelcontextprotocol/vitest-config": "workspace:^",
"tsdown": "catalog:devTools"
}
}

View file

@ -1,25 +0,0 @@
/**
* Custom (non-spec) method example: a client that sends `acme/search` and
* listens for `acme/searchProgress` notifications.
*
* Build `examples/server` first; this client spawns the server via stdio.
*/
import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import { z } from 'zod/v4';
const SearchResult = z.object({ items: z.array(z.string()) });
const SearchProgressParams = z.object({ stage: z.string(), pct: z.number() });
const client = new Client({ name: 'acme-search-client', version: '0.0.0' });
client.setNotificationHandler('acme/searchProgress', { params: SearchProgressParams }, params => {
console.log(`[progress] ${params.stage} ${Math.round(params.pct * 100)}%`);
});
await client.connect(new StdioClientTransport({ command: 'node', args: ['../server/dist/customMethodExample.js'] }));
const result = await client.request({ method: 'acme/search', params: { query: 'mcp', limit: 3 } }, SearchResult);
console.log('items:', result.items);
await client.close();

View file

@ -1,824 +0,0 @@
// Run with: pnpm tsx src/elicitationUrlExample.ts
//
// This example demonstrates how to use URL elicitation to securely
// collect user input in a remote (HTTP) server.
// URL elicitation allows servers to prompt the end-user to open a URL in their browser
// to collect sensitive information.
import { createServer } from 'node:http';
import { createInterface } from 'node:readline';
import type {
ElicitRequest,
ElicitRequestURLParams,
ElicitResult,
ListToolsRequest,
OAuthClientMetadata,
ResourceLink
} from '@modelcontextprotocol/client';
import {
Client,
getDisplayName,
ProtocolError,
ProtocolErrorCode,
StreamableHTTPClientTransport,
UnauthorizedError,
UrlElicitationRequiredError
} from '@modelcontextprotocol/client';
import open from 'open';
import { InMemoryOAuthClientProvider } from './simpleOAuthClientProvider';
// Set up OAuth (required for this example)
const OAUTH_CALLBACK_PORT = 8090; // Use different port than auth server (3001)
const OAUTH_CALLBACK_URL = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
console.log('Getting OAuth token...');
const clientMetadata: OAuthClientMetadata = {
client_name: 'Elicitation MCP Client',
redirect_uris: [OAUTH_CALLBACK_URL],
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code'],
token_endpoint_auth_method: 'client_secret_post',
scope: 'mcp:tools'
};
const oauthProvider = new InMemoryOAuthClientProvider(OAUTH_CALLBACK_URL, clientMetadata, (redirectUrl: URL) => {
console.log(`🌐 Opening browser for OAuth redirect: ${redirectUrl.toString()}`);
openBrowser(redirectUrl.toString());
});
// Create readline interface for user input
const readline = createInterface({
input: process.stdin,
output: process.stdout
});
let abortCommand = new AbortController();
// Global client and transport for interactive commands
let client: Client | null = null;
let transport: StreamableHTTPClientTransport | null = null;
let serverUrl = 'http://localhost:3000/mcp';
let sessionId: string | undefined;
// Elicitation queue management
interface QueuedElicitation {
request: ElicitRequest;
resolve: (result: ElicitResult) => void;
reject: (error: Error) => void;
}
let isProcessingCommand = false;
let isProcessingElicitations = false;
const elicitationQueue: QueuedElicitation[] = [];
let elicitationQueueSignal: (() => void) | null = null;
let elicitationsCompleteSignal: (() => void) | null = null;
// Map to track pending URL elicitations waiting for completion notifications
const pendingURLElicitations = new Map<
string,
{
resolve: () => void;
reject: (error: Error) => void;
timeout: NodeJS.Timeout;
}
>();
async function main(): Promise<void> {
console.log('MCP Interactive Client');
console.log('=====================');
// Connect to server immediately with default settings
await connect();
// Start the elicitation loop in the background
elicitationLoop().catch(error => {
console.error('Unexpected error in elicitation loop:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
});
// Short delay allowing the server to send any SSE elicitations on connection
await new Promise(resolve => setTimeout(resolve, 200));
// Wait until we are done processing any initial elicitations
await waitForElicitationsToComplete();
// Print help and start the command loop
printHelp();
await commandLoop();
}
async function waitForElicitationsToComplete(): Promise<void> {
// Wait until the queue is empty and nothing is being processed
while (elicitationQueue.length > 0 || isProcessingElicitations) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
function printHelp(): void {
console.log('\nAvailable commands:');
console.log(' connect [url] - Connect to MCP server (default: http://localhost:3000/mcp)');
console.log(' disconnect - Disconnect from server');
console.log(' terminate-session - Terminate the current session');
console.log(' reconnect - Reconnect to the server');
console.log(' list-tools - List available tools');
console.log(' call-tool <name> [args] - Call a tool with optional JSON arguments');
console.log(' payment-confirm - Test URL elicitation via error response with payment-confirm tool');
console.log(' third-party-auth - Test tool that requires third-party OAuth credentials');
console.log(' help - Show this help');
console.log(' quit - Exit the program');
}
async function commandLoop(): Promise<void> {
await new Promise<void>(resolve => {
if (isProcessingElicitations) {
elicitationsCompleteSignal = resolve;
} else {
resolve();
}
});
readline.question('\n> ', { signal: abortCommand.signal }, async input => {
isProcessingCommand = true;
const args = input.trim().split(/\s+/);
const command = args[0]?.toLowerCase();
try {
switch (command) {
case 'connect': {
await connect(args[1]);
break;
}
case 'disconnect': {
await disconnect();
break;
}
case 'terminate-session': {
await terminateSession();
break;
}
case 'reconnect': {
await reconnect();
break;
}
case 'list-tools': {
await listTools();
break;
}
case 'call-tool': {
if (args.length < 2) {
console.log('Usage: call-tool <name> [args]');
} else {
const toolName = args[1]!;
let toolArgs = {};
if (args.length > 2) {
try {
toolArgs = JSON.parse(args.slice(2).join(' '));
} catch {
console.log('Invalid JSON arguments. Using empty args.');
}
}
await callTool(toolName, toolArgs);
}
break;
}
case 'payment-confirm': {
await callPaymentConfirmTool();
break;
}
case 'third-party-auth': {
await callThirdPartyAuthTool();
break;
}
case 'help': {
printHelp();
break;
}
case 'quit':
case 'exit': {
await cleanup();
return;
}
default: {
if (command) {
console.log(`Unknown command: ${command}`);
}
break;
}
}
} catch (error) {
console.error(`Error executing command: ${error}`);
} finally {
isProcessingCommand = false;
}
// Process another command after we've processed the this one
await commandLoop();
});
}
async function elicitationLoop(): Promise<void> {
while (true) {
// Wait until we have elicitations to process
await new Promise<void>(resolve => {
if (elicitationQueue.length > 0) {
resolve();
} else {
elicitationQueueSignal = resolve;
}
});
isProcessingElicitations = true;
abortCommand.abort(); // Abort the command loop if it's running
// Process all queued elicitations
while (elicitationQueue.length > 0) {
const queued = elicitationQueue.shift()!;
console.log(`📤 Processing queued elicitation (${elicitationQueue.length} remaining)`);
try {
const result = await handleElicitationRequest(queued.request);
queued.resolve(result);
} catch (error) {
queued.reject(error instanceof Error ? error : new Error(String(error)));
}
}
console.log('✅ All queued elicitations processed. Resuming command loop...\n');
isProcessingElicitations = false;
// Reset the abort controller for the next command loop
abortCommand = new AbortController();
// Resume the command loop
if (elicitationsCompleteSignal) {
elicitationsCompleteSignal();
elicitationsCompleteSignal = null;
}
}
}
const ALLOWED_SCHEMES = new Set(['http:', 'https:']);
async function openBrowser(url: string): Promise<void> {
try {
const parsed = new URL(url);
if (!ALLOWED_SCHEMES.has(parsed.protocol)) {
console.error(`Refusing to open URL with unsupported scheme '${parsed.protocol}': ${url}`);
return;
}
} catch {
console.error(`Invalid URL: ${url}`);
return;
}
try {
await open(url);
} catch {
console.log(`Please manually open: ${url}`);
}
}
/**
* Enqueues an elicitation request and returns the result.
*
* This function is used so that our CLI (which can only handle one input request at a time)
* can handle elicitation requests and the command loop.
*
* @param request - The elicitation request to be handled
* @returns The elicitation result
*/
async function elicitationRequestHandler(request: ElicitRequest): Promise<ElicitResult> {
// If we are processing a command, handle this elicitation immediately
if (isProcessingCommand) {
console.log('📋 Processing elicitation immediately (during command execution)');
return await handleElicitationRequest(request);
}
// Otherwise, queue the request to be handled by the elicitation loop
console.log(`📥 Queueing elicitation request (queue size will be: ${elicitationQueue.length + 1})`);
return new Promise<ElicitResult>((resolve, reject) => {
elicitationQueue.push({
request,
resolve,
reject
});
// Signal the elicitation loop that there's work to do
if (elicitationQueueSignal) {
elicitationQueueSignal();
elicitationQueueSignal = null;
}
});
}
/**
* Handles an elicitation request.
*
* This function is used to handle the elicitation request and return the result.
*
* @param request - The elicitation request to be handled
* @returns The elicitation result
*/
async function handleElicitationRequest(request: ElicitRequest): Promise<ElicitResult> {
const mode = request.params.mode;
console.log('\n🔔 Elicitation Request Received:');
console.log(`Mode: ${mode}`);
if (mode === 'url') {
return {
action: await handleURLElicitation(request.params as ElicitRequestURLParams)
};
} else {
// Should not happen because the client declares its capabilities to the server,
// but being defensive is a good practice:
throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unsupported elicitation mode: ${mode}`);
}
}
/**
* Handles a URL elicitation by opening the URL in the browser.
*
* Note: This is a shared code for both request handlers and error handlers.
* As a result of sharing schema, there is no big forking of logic for the client.
*
* @param params - The URL elicitation request parameters
* @returns The action to take (accept, cancel, or decline)
*/
async function handleURLElicitation(params: ElicitRequestURLParams): Promise<ElicitResult['action']> {
const url = params.url;
const elicitationId = params.elicitationId;
const message = params.message;
console.log(`🆔 Elicitation ID: ${elicitationId}`); // Print for illustration
// Parse URL to show domain for security
let domain = 'unknown domain';
try {
const parsedUrl = new URL(url);
domain = parsedUrl.hostname;
} catch {
console.error('Invalid URL provided by server');
return 'decline';
}
// Example security warning to help prevent phishing attacks
console.log('\n⚠️ \u001B[33mSECURITY WARNING\u001B[0m ⚠️');
console.log('\u001B[33mThe server is requesting you to open an external URL.\u001B[0m');
console.log('\u001B[33mOnly proceed if you trust this server and understand why it needs this.\u001B[0m\n');
console.log(`🌐 Target domain: \u001B[36m${domain}\u001B[0m`);
console.log(`🔗 Full URL: \u001B[36m${url}\u001B[0m`);
console.log(`\nℹ️ Server's reason:\n\n\u001B[36m${message}\u001B[0m\n`);
// 1. Ask for user consent to open the URL
const consent = await new Promise<string>(resolve => {
readline.question('\nDo you want to open this URL in your browser? (y/n): ', input => {
resolve(input.trim().toLowerCase());
});
});
// 2. If user did not consent, return appropriate result
if (consent === 'no' || consent === 'n') {
console.log('❌ URL navigation declined.');
return 'decline';
} else if (consent !== 'yes' && consent !== 'y') {
console.log('🚫 Invalid response. Cancelling elicitation.');
return 'cancel';
}
// 3. Wait for completion notification in the background
const completionPromise = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => {
pendingURLElicitations.delete(elicitationId);
console.log(`\u001B[31m❌ Elicitation ${elicitationId} timed out waiting for completion.\u001B[0m`);
reject(new Error('Elicitation completion timeout'));
},
5 * 60 * 1000
); // 5 minute timeout
pendingURLElicitations.set(elicitationId, {
resolve: () => {
clearTimeout(timeout);
resolve();
},
reject,
timeout
});
});
completionPromise.catch(error => {
console.error('Background completion wait failed:', error);
});
// 4. Open the URL in the browser
console.log(`\n🚀 Opening browser to: ${url}`);
await openBrowser(url);
console.log('\n⏳ Waiting for you to complete the interaction in your browser...');
console.log(' The server will send a notification once you complete the action.');
// 5. Acknowledge the user accepted the elicitation
return 'accept';
}
/**
* Example OAuth callback handler - in production, use a more robust approach
* for handling callbacks and storing tokens
*/
/**
* Starts a temporary HTTP server to receive the OAuth callback
*/
async function waitForOAuthCallback(): Promise<string> {
return new Promise<string>((resolve, reject) => {
const server = createServer((req, res) => {
// Ignore favicon requests
if (req.url === '/favicon.ico') {
res.writeHead(404);
res.end();
return;
}
console.log(`📥 Received callback: ${req.url}`);
const parsedUrl = new URL(req.url || '', 'http://localhost');
const code = parsedUrl.searchParams.get('code');
const error = parsedUrl.searchParams.get('error');
if (code) {
console.log(`✅ Authorization code received: ${code?.slice(0, 10)}...`);
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization Successful!</h1>
<p>This simulates successful authorization of the MCP client, which now has an access token for the MCP server.</p>
<p>This window will close automatically in 10 seconds.</p>
<script>setTimeout(() => window.close(), 10000);</script>
</body>
</html>
`);
resolve(code);
setTimeout(() => server.close(), 15_000);
} else if (error) {
console.log(`❌ Authorization error: ${error}`);
res.writeHead(400, { 'Content-Type': 'text/html' });
res.end(`
<html>
<body>
<h1>Authorization Failed</h1>
<p>Error: ${error}</p>
</body>
</html>
`);
reject(new Error(`OAuth authorization failed: ${error}`));
} else {
console.log(`❌ No authorization code or error in callback`);
res.writeHead(400);
res.end('Bad request');
reject(new Error('No authorization code provided'));
}
});
server.listen(OAUTH_CALLBACK_PORT, () => {
console.log(`OAuth callback server started on http://localhost:${OAUTH_CALLBACK_PORT}`);
});
});
}
/**
* Attempts to connect to the MCP server with OAuth authentication.
* Handles OAuth flow recursively if authorization is required.
*/
async function attemptConnection(oauthProvider: InMemoryOAuthClientProvider): Promise<void> {
console.log('🚢 Creating transport with OAuth provider...');
const baseUrl = new URL(serverUrl);
transport = new StreamableHTTPClientTransport(baseUrl, {
sessionId: sessionId,
authProvider: oauthProvider
});
console.log('🚢 Transport created');
try {
console.log('🔌 Attempting connection (this will trigger OAuth redirect if needed)...');
await client!.connect(transport);
sessionId = transport.sessionId;
console.log('Transport created with session ID:', sessionId);
console.log('✅ Connected successfully');
} catch (error) {
if (error instanceof UnauthorizedError) {
console.log('🔐 OAuth required - waiting for authorization...');
const callbackPromise = waitForOAuthCallback();
const authCode = await callbackPromise;
await transport.finishAuth(authCode);
console.log('🔐 Authorization code received:', authCode);
console.log('🔌 Reconnecting with authenticated transport...');
// Recursively retry connection after OAuth completion
await attemptConnection(oauthProvider);
} else {
console.error('❌ Connection failed with non-auth error:', error);
throw error;
}
}
}
async function connect(url?: string): Promise<void> {
if (client) {
console.log('Already connected. Disconnect first.');
return;
}
if (url) {
serverUrl = url;
}
console.log(`🔗 Attempting to connect to ${serverUrl}...`);
// Create a new client with elicitation capability
console.log('👤 Creating MCP client...');
client = new Client(
{
name: 'example-client',
version: '1.0.0'
},
{
capabilities: {
elicitation: {
// Only URL elicitation is supported in this demo
// (see server/elicitationExample.ts for a demo of form mode elicitation)
url: {}
}
}
}
);
console.log('👤 Client created');
// Set up elicitation request handler with proper validation
client.setRequestHandler('elicitation/create', elicitationRequestHandler);
// Set up notification handler for elicitation completion
client.setNotificationHandler('notifications/elicitation/complete', notification => {
const { elicitationId } = notification.params;
const pending = pendingURLElicitations.get(elicitationId);
if (pending) {
clearTimeout(pending.timeout);
pendingURLElicitations.delete(elicitationId);
console.log(`\u001B[32m✅ Elicitation ${elicitationId} completed!\u001B[0m`);
pending.resolve();
} else {
// Shouldn't happen - discard it!
console.warn(`Received completion notification for unknown elicitation: ${elicitationId}`);
}
});
try {
console.log('🔐 Starting OAuth flow...');
await attemptConnection(oauthProvider!);
console.log('Connected to MCP server');
// Set up error handler after connection is established so we don't double log errors
client.onerror = error => {
console.error('\u001B[31mClient error:', error, '\u001B[0m');
};
} catch (error) {
console.error('Failed to connect:', error);
client = null;
transport = null;
return;
}
}
async function disconnect(): Promise<void> {
if (!client || !transport) {
console.log('Not connected.');
return;
}
try {
await transport.close();
console.log('Disconnected from MCP server');
client = null;
transport = null;
} catch (error) {
console.error('Error disconnecting:', error);
}
}
async function terminateSession(): Promise<void> {
if (!client || !transport) {
console.log('Not connected.');
return;
}
try {
console.log('Terminating session with ID:', transport.sessionId);
await transport.terminateSession();
console.log('Session terminated successfully');
// Check if sessionId was cleared after termination
if (transport.sessionId) {
console.log('Server responded with 405 Method Not Allowed (session termination not supported)');
console.log('Session ID is still active:', transport.sessionId);
} else {
console.log('Session ID has been cleared');
sessionId = undefined;
// Also close the transport and clear client objects
await transport.close();
console.log('Transport closed after session termination');
client = null;
transport = null;
}
} catch (error) {
console.error('Error terminating session:', error);
}
}
async function reconnect(): Promise<void> {
if (client) {
await disconnect();
}
await connect();
}
async function listTools(): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
console.log(' No tools available');
} else {
for (const tool of toolsResult.tools) {
console.log(` - id: ${tool.name}, name: ${getDisplayName(tool)}, description: ${tool.description}`);
}
}
} catch (error) {
console.log(`Tools not supported by this server (${error})`);
}
}
async function callTool(name: string, args: Record<string, unknown>): Promise<void> {
if (!client) {
console.log('Not connected to server.');
return;
}
try {
console.log(`Calling tool '${name}' with args:`, args);
const result = await client.callTool({ name, arguments: args });
console.log('Tool result:');
const resourceLinks: ResourceLink[] = [];
for (const item of result.content) {
switch (item.type) {
case 'text': {
console.log(` ${item.text}`);
break;
}
case 'resource_link': {
const resourceLink = item as ResourceLink;
resourceLinks.push(resourceLink);
console.log(` 📁 Resource Link: ${resourceLink.name}`);
console.log(` URI: ${resourceLink.uri}`);
if (resourceLink.mimeType) {
console.log(` Type: ${resourceLink.mimeType}`);
}
if (resourceLink.description) {
console.log(` Description: ${resourceLink.description}`);
}
break;
}
case 'resource': {
console.log(` [Embedded Resource: ${item.resource.uri}]`);
break;
}
case 'image': {
console.log(` [Image: ${item.mimeType}]`);
break;
}
case 'audio': {
console.log(` [Audio: ${item.mimeType}]`);
break;
}
default: {
console.log(` [Unknown content type]:`, item);
}
}
}
// Offer to read resource links
if (resourceLinks.length > 0) {
console.log(`\nFound ${resourceLinks.length} resource link(s). Use 'read-resource <uri>' to read their content.`);
}
} catch (error) {
if (error instanceof UrlElicitationRequiredError) {
console.log('\n🔔 Elicitation Required Error Received:');
console.log(`Message: ${error.message}`);
for (const e of error.elicitations) {
await handleURLElicitation(e); // For the error handler, we discard the action result because we don't respond to an error response
}
return;
}
console.log(`Error calling tool ${name}: ${error}`);
}
}
async function cleanup(): Promise<void> {
if (client && transport) {
try {
// First try to terminate the session gracefully
if (transport.sessionId) {
try {
console.log('Terminating session before exit...');
await transport.terminateSession();
console.log('Session terminated successfully');
} catch (error) {
console.error('Error terminating session:', error);
}
}
// Then close the transport
await transport.close();
} catch (error) {
console.error('Error closing transport:', error);
}
}
process.stdin.setRawMode(false);
readline.close();
console.log('\nGoodbye!');
// eslint-disable-next-line unicorn/no-process-exit
process.exit(0);
}
async function callPaymentConfirmTool(): Promise<void> {
console.log('Calling payment-confirm tool...');
await callTool('payment-confirm', { cartId: 'cart_123' });
}
async function callThirdPartyAuthTool(): Promise<void> {
console.log('Calling third-party-auth tool...');
await callTool('third-party-auth', { param1: 'test' });
}
// Set up raw mode for keyboard input to capture Escape key
process.stdin.setRawMode(true);
process.stdin.on('data', async data => {
// Check for Escape key (27)
if (data.length === 1 && data[0] === 27) {
console.log('\nESC key pressed. Disconnecting from server...');
// Abort current operation and disconnect from server
if (client && transport) {
await disconnect();
console.log('Disconnected. Press Enter to continue.');
} else {
console.log('Not connected to server.');
}
// Re-display the prompt
process.stdout.write('> ');
}
});
// Handle Ctrl+C
process.on('SIGINT', async () => {
console.log('\nReceived SIGINT. Cleaning up...');
await cleanup();
});
// Start the interactive client
try {
await main();
} catch (error) {
console.error('Error running MCP client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -1,152 +0,0 @@
import type { CallToolResult } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Multiple Clients MCP Example
*
* This client demonstrates how to:
* 1. Create multiple MCP clients in parallel
* 2. Each client calls a single tool
* 3. Track notifications from each client independently
*/
// Command line args processing
const args = process.argv.slice(2);
const serverUrl = args[0] || 'http://localhost:3000/mcp';
interface ClientConfig {
id: string;
name: string;
toolName: string;
toolArguments: Record<string, string | number | boolean>;
}
async function createAndRunClient(config: ClientConfig): Promise<{ id: string; result: CallToolResult }> {
console.log(`[${config.id}] Creating client: ${config.name}`);
const client = new Client({
name: config.name,
version: '1.0.0'
});
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
// Set up client-specific error handler
client.onerror = error => {
console.error(`[${config.id}] Client error:`, error);
};
// Set up client-specific notification handler
client.setNotificationHandler('notifications/message', notification => {
console.log(`[${config.id}] Notification: ${notification.params.data}`);
});
try {
// Connect to the server
await client.connect(transport);
console.log(`[${config.id}] Connected to MCP server`);
// Call the specified tool
console.log(`[${config.id}] Calling tool: ${config.toolName}`);
const result = await client.callTool({
name: config.toolName,
arguments: {
...config.toolArguments,
// Add client ID to arguments for identification in notifications
caller: config.id
}
});
console.log(`[${config.id}] Tool call completed`);
// Keep the connection open for a bit to receive notifications
await new Promise(resolve => setTimeout(resolve, 5000));
// Disconnect
await transport.close();
console.log(`[${config.id}] Disconnected from MCP server`);
return { id: config.id, result };
} catch (error) {
console.error(`[${config.id}] Error:`, error);
throw error;
}
}
async function main(): Promise<void> {
console.log('MCP Multiple Clients Example');
console.log('============================');
console.log(`Server URL: ${serverUrl}`);
console.log('');
try {
// Define client configurations
const clientConfigs: ClientConfig[] = [
{
id: 'client1',
name: 'basic-client-1',
toolName: 'start-notification-stream',
toolArguments: {
interval: 3, // 1 second between notifications
count: 5 // Send 5 notifications
}
},
{
id: 'client2',
name: 'basic-client-2',
toolName: 'start-notification-stream',
toolArguments: {
interval: 2, // 2 seconds between notifications
count: 3 // Send 3 notifications
}
},
{
id: 'client3',
name: 'basic-client-3',
toolName: 'start-notification-stream',
toolArguments: {
interval: 1, // 0.5 second between notifications
count: 8 // Send 8 notifications
}
}
];
// Start all clients in parallel
console.log(`Starting ${clientConfigs.length} clients in parallel...`);
console.log('');
const clientPromises = clientConfigs.map(config => createAndRunClient(config));
const results = await Promise.all(clientPromises);
// Display results from all clients
console.log('\n=== Final Results ===');
for (const { id, result } of results) {
console.log(`\n[${id}] Tool result:`);
if (Array.isArray(result.content)) {
for (const item of result.content) {
if (item.type === 'text' && item.text) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
}
} else {
console.log(` Unexpected result format:`, result);
}
}
console.log('\n=== All clients completed successfully ===');
} catch (error) {
console.error('Error running multiple clients:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
// Start the example
try {
await main();
} catch (error) {
console.error('Error running multiple clients:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -1,175 +0,0 @@
import type { CallToolResult, ListToolsRequest } from '@modelcontextprotocol/client';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Parallel Tool Calls MCP Client
*
* This client demonstrates how to:
* 1. Start multiple tool calls in parallel
* 2. Track notifications from each tool call using a caller parameter
*/
// Command line args processing
const args = process.argv.slice(2);
const serverUrl = args[0] || 'http://localhost:3000/mcp';
async function main(): Promise<void> {
console.log('MCP Parallel Tool Calls Client');
console.log('==============================');
console.log(`Connecting to server at: ${serverUrl}`);
let client: Client;
let transport: StreamableHTTPClientTransport;
try {
// Create client with streamable HTTP transport
client = new Client({
name: 'parallel-tool-calls-client',
version: '1.0.0'
});
client.onerror = error => {
console.error('Client error:', error);
};
// Connect to the server
transport = new StreamableHTTPClientTransport(new URL(serverUrl));
await client.connect(transport);
console.log('Successfully connected to MCP server');
// Set up notification handler with caller identification
client.setNotificationHandler('notifications/message', notification => {
console.log(`Notification: ${notification.params.data}`);
});
console.log('List tools');
const toolsRequest = await listTools(client);
console.log('Tools:', toolsRequest);
// 2. Start multiple notification tools in parallel
console.log('\n=== Starting Multiple Notification Streams in Parallel ===');
const toolResults = await startParallelNotificationTools(client);
// Log the results from each tool call
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
for (const item of result.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
}
}
// 3. Wait for all notifications (10 seconds)
console.log('\n=== Waiting for all notifications ===');
await new Promise(resolve => setTimeout(resolve, 10_000));
// 4. Disconnect
console.log('\n=== Disconnecting ===');
await transport.close();
console.log('Disconnected from MCP server');
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
/**
* List available tools on the server
*/
async function listTools(client: Client): Promise<void> {
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
console.log(' No tools available');
} else {
for (const tool of toolsResult.tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
}
} catch (error) {
console.log(`Tools not supported by this server: ${error}`);
}
}
/**
* Start multiple notification tools in parallel with different configurations
* Each tool call includes a caller parameter to identify its notifications
*/
async function startParallelNotificationTools(client: Client): Promise<Record<string, CallToolResult>> {
try {
// Define multiple tool calls with different configurations
const toolCalls = [
{
caller: 'fast-notifier',
args: {
interval: 2, // 0.5 second between notifications
count: 10, // Send 10 notifications
caller: 'fast-notifier' // Identify this tool call
}
},
{
caller: 'slow-notifier',
args: {
interval: 5, // 2 seconds between notifications
count: 5, // Send 5 notifications
caller: 'slow-notifier' // Identify this tool call
}
},
{
caller: 'burst-notifier',
args: {
interval: 1, // 0.1 second between notifications
count: 3, // Send just 3 notifications
caller: 'burst-notifier' // Identify this tool call
}
}
];
console.log(`Starting ${toolCalls.length} notification tools in parallel...`);
// Start all tool calls in parallel
const toolPromises = toolCalls.map(({ caller, args }) => {
console.log(`Starting tool call for ${caller}...`);
return client
.callTool({ name: 'start-notification-stream', arguments: args })
.then(result => ({ caller, result }))
.catch(error => {
console.error(`Error in tool call for ${caller}:`, error);
throw error;
});
});
// Wait for all tool calls to complete
const results = await Promise.all(toolPromises);
// Organize results by caller
const resultsByTool: Record<string, CallToolResult> = {};
for (const { caller, result } of results) {
resultsByTool[caller] = result;
}
return resultsByTool;
} catch (error) {
console.error(`Error starting parallel notification tools:`, error);
throw error;
}
}
try {
// Run the client
await main();
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -1,83 +0,0 @@
#!/usr/bin/env node
/**
* Example demonstrating client_credentials grant for machine-to-machine authentication.
*
* Supports two authentication methods based on environment variables:
*
* 1. client_secret_basic (default):
* MCP_CLIENT_ID - OAuth client ID (required)
* MCP_CLIENT_SECRET - OAuth client secret (required)
*
* 2. private_key_jwt (when MCP_CLIENT_PRIVATE_KEY_PEM is set):
* MCP_CLIENT_ID - OAuth client ID (required)
* MCP_CLIENT_PRIVATE_KEY_PEM - PEM-encoded private key for JWT signing (required)
* MCP_CLIENT_ALGORITHM - Signing algorithm (default: RS256)
*
* Common:
* MCP_SERVER_URL - Server URL (default: http://localhost:3000/mcp)
*/
import type { OAuthClientProvider } from '@modelcontextprotocol/client';
import { Client, ClientCredentialsProvider, PrivateKeyJwtProvider, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const DEFAULT_SERVER_URL = process.env.MCP_SERVER_URL || 'http://localhost:3000/mcp';
function createProvider(): OAuthClientProvider {
const clientId = process.env.MCP_CLIENT_ID;
if (!clientId) {
console.error('MCP_CLIENT_ID environment variable is required');
process.exit(1);
}
// If private key is provided, use private_key_jwt authentication
const privateKeyPem = process.env.MCP_CLIENT_PRIVATE_KEY_PEM;
if (privateKeyPem) {
const algorithm = process.env.MCP_CLIENT_ALGORITHM || 'RS256';
console.log('Using private_key_jwt authentication');
return new PrivateKeyJwtProvider({
clientId,
privateKey: privateKeyPem,
algorithm
});
}
// Otherwise, use client_secret_basic authentication
const clientSecret = process.env.MCP_CLIENT_SECRET;
if (!clientSecret) {
console.error('MCP_CLIENT_SECRET or MCP_CLIENT_PRIVATE_KEY_PEM environment variable is required');
process.exit(1);
}
console.log('Using client_secret_basic authentication');
return new ClientCredentialsProvider({
clientId,
clientSecret
});
}
async function main() {
const provider = createProvider();
const client = new Client({ name: 'client-credentials-example', version: '1.0.0' }, { capabilities: {} });
const transport = new StreamableHTTPClientTransport(new URL(DEFAULT_SERVER_URL), {
authProvider: provider
});
await client.connect(transport);
console.log('Connected successfully.');
const tools = await client.listTools();
console.log('Available tools:', tools.tools.map(t => t.name).join(', ') || '(none)');
await transport.close();
}
try {
await main();
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -1,109 +0,0 @@
/**
* SSE Polling Example Client (SEP-1699)
*
* This example demonstrates client-side behavior during server-initiated
* SSE stream disconnection and automatic reconnection.
*
* Key features demonstrated:
* - Automatic reconnection when server closes SSE stream
* - Event replay via Last-Event-ID header
* - Resumption token tracking via onresumptiontoken callback
*
* Run with: pnpm tsx src/ssePollingClient.ts
* Requires: ssePollingExample.ts server running on port 3001
*/
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const SERVER_URL = 'http://localhost:3001/mcp';
async function main(): Promise<void> {
console.log('SSE Polling Example Client');
console.log('==========================');
console.log(`Connecting to ${SERVER_URL}...`);
console.log('');
// Create transport with reconnection options
const transport = new StreamableHTTPClientTransport(new URL(SERVER_URL), {
// Use default reconnection options - SDK handles automatic reconnection
});
// Track the last event ID for debugging
let lastEventId: string | undefined;
// Set up transport error handler to observe disconnections
// Filter out expected errors from SSE reconnection
transport.onerror = error => {
// Skip abort errors during intentional close
if (error.message.includes('AbortError')) return;
// Show SSE disconnect (expected when server closes stream)
if (error.message.includes('Unexpected end of JSON')) {
console.log('[Transport] SSE stream disconnected - client will auto-reconnect');
return;
}
console.log(`[Transport] Error: ${error.message}`);
};
// Set up transport close handler
transport.onclose = () => {
console.log('[Transport] Connection closed');
};
// Create and connect client
const client = new Client({
name: 'sse-polling-client',
version: '1.0.0'
});
// Set up notification handler to receive progress updates
client.setNotificationHandler('notifications/message', notification => {
const data = notification.params.data;
console.log(`[Notification] ${data}`);
});
try {
await client.connect(transport);
console.log('[Client] Connected successfully');
console.log('');
// Call the long-operation tool
console.log('[Client] Calling long-operation tool...');
console.log('[Client] Server will disconnect mid-operation to demonstrate polling');
console.log('');
const result = await client.request(
{
method: 'tools/call',
params: {
name: 'long-operation',
arguments: {}
}
},
{
// Track resumption tokens for debugging
onresumptiontoken: token => {
lastEventId = token;
console.log(`[Event ID] ${token}`);
}
}
);
console.log('');
console.log('[Client] Tool completed!');
console.log(`[Result] ${JSON.stringify(result.content, null, 2)}`);
console.log('');
console.log(`[Debug] Final event ID: ${lastEventId}`);
} catch (error) {
console.error('[Error]', error);
} finally {
await transport.close();
console.log('[Client] Disconnected');
}
}
try {
await main();
} catch (error) {
console.error('Error running MCP client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

View file

@ -1,181 +0,0 @@
import type { ListToolsRequest } from '@modelcontextprotocol/client';
import { Client, SSEClientTransport, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
/**
* Simplified Backwards Compatible MCP Client
*
* This client demonstrates backward compatibility with both:
* 1. Modern servers using Streamable HTTP transport (protocol version 2025-03-26)
* 2. Older servers using HTTP+SSE transport (protocol version 2024-11-05)
*
* Following the MCP specification for backwards compatibility:
* - Attempts to POST an initialize request to the server URL first (modern transport)
* - If that fails with 4xx status, falls back to GET request for SSE stream (older transport)
*/
// Command line args processing
const args = process.argv.slice(2);
const serverUrl = args[0] || 'http://localhost:3000/mcp';
async function main(): Promise<void> {
console.log('MCP Backwards Compatible Client');
console.log('===============================');
console.log(`Connecting to server at: ${serverUrl}`);
let client: Client;
let transport: StreamableHTTPClientTransport | SSEClientTransport;
try {
// Try connecting with automatic transport detection
const connection = await connectWithBackwardsCompatibility(serverUrl);
client = connection.client;
transport = connection.transport;
// Set up notification handler
client.setNotificationHandler('notifications/message', notification => {
console.log(`Notification: ${notification.params.level} - ${notification.params.data}`);
});
// DEMO WORKFLOW:
// 1. List available tools
console.log('\n=== Listing Available Tools ===');
await listTools(client);
// 2. Call the notification tool
console.log('\n=== Starting Notification Stream ===');
await startNotificationTool(client);
// 3. Wait for all notifications (5 seconds)
console.log('\n=== Waiting for all notifications ===');
await new Promise(resolve => setTimeout(resolve, 5000));
// 4. Disconnect
console.log('\n=== Disconnecting ===');
await transport.close();
console.log('Disconnected from MCP server');
} catch (error) {
console.error('Error running client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}
}
/**
* Connect to an MCP server with backwards compatibility
* Following the spec for client backward compatibility
*/
async function connectWithBackwardsCompatibility(url: string): Promise<{
client: Client;
transport: StreamableHTTPClientTransport | SSEClientTransport;
transportType: 'streamable-http' | 'sse';
}> {
console.log('1. Trying Streamable HTTP transport first...');
// Step 1: Try Streamable HTTP transport first
const client = new Client({
name: 'backwards-compatible-client',
version: '1.0.0'
});
client.onerror = error => {
console.error('Client error:', error);
};
const baseUrl = new URL(url);
try {
// Create modern transport
const streamableTransport = new StreamableHTTPClientTransport(baseUrl);
await client.connect(streamableTransport);
console.log('Successfully connected using modern Streamable HTTP transport.');
return {
client,
transport: streamableTransport,
transportType: 'streamable-http'
};
} catch (error) {
// Step 2: If transport fails, try the older SSE transport
console.log(`StreamableHttp transport connection failed: ${error}`);
console.log('2. Falling back to deprecated HTTP+SSE transport...');
try {
// Create SSE transport pointing to /sse endpoint
const sseTransport = new SSEClientTransport(baseUrl);
const sseClient = new Client({
name: 'backwards-compatible-client',
version: '1.0.0'
});
await sseClient.connect(sseTransport);
console.log('Successfully connected using deprecated HTTP+SSE transport.');
return {
client: sseClient,
transport: sseTransport,
transportType: 'sse'
};
} catch (sseError) {
console.error(`Failed to connect with either transport method:\n1. Streamable HTTP error: ${error}\n2. SSE error: ${sseError}`);
throw new Error('Could not connect to server with any available transport');
}
}
}
/**
* List available tools on the server
*/
async function listTools(client: Client): Promise<void> {
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
console.log(' No tools available');
} else {
for (const tool of toolsResult.tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
}
} catch (error) {
console.log(`Tools not supported by this server: ${error}`);
}
}
/**
* Start a notification stream by calling the notification tool
*/
async function startNotificationTool(client: Client): Promise<void> {
try {
console.log('Calling notification tool...');
const result = await client.callTool({
name: 'start-notification-stream',
arguments: {
interval: 1000, // 1 second between notifications
count: 5 // Send 5 notifications
}
});
console.log('Tool result:');
for (const item of result.content) {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
}
} catch (error) {
console.log(`Error calling notification tool: ${error}`);
}
}
// Start the client
try {
await main();
} catch (error) {
console.error('Error running MCP client:', error);
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1);
}

Some files were not shown because too many files have changed in this diff Show more