docs: correctness and coverage pass for the 2026-07-28 revision (#2389)

This commit is contained in:
Felix Weinberger 2026-06-29 18:44:25 +01:00 • committed by GitHub
commit 199a28e9e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1335 additions and 390 deletions

View file

@ -0,0 +1,5 @@
# client-quickstart
Source for the [Client Quickstart](../../docs/client-quickstart.md) tutorial: an LLM-powered chatbot that connects to an MCP server over stdio and calls its tools. The tutorial walks through `src/index.ts` end to end.
The `package.json` and `tsconfig.json` here are monorepo-internal (`workspace:`/`catalog:` protocols; typecheck-only in CI). To build the client yourself, use the standalone manifests from the tutorial.

View file

@ -1,11 +1,8 @@
{
"name": "@modelcontextprotocol/examples-client-quickstart",
"name": "@mcp-examples/client-quickstart",
"private": true,
"version": "2.0.0-alpha.0",
"type": "module",
"bin": {
"mcp-client-cli": "./build/index.js"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"

View file

@ -4,12 +4,11 @@ import { Client } from '@modelcontextprotocol/client';
import { StdioClientTransport } from '@modelcontextprotocol/client/stdio';
import readline from 'readline/promises';
const ANTHROPIC_MODEL = 'claude-sonnet-4-5';
const ANTHROPIC_MODEL = 'claude-sonnet-4-6';
class MCPClient {
private mcp: Client;
private _anthropic: Anthropic | null = null;
private transport: StdioClientTransport | null = null;
private tools: Anthropic.Tool[] = [];
constructor() {
@ -37,8 +36,8 @@ class MCPClient {
: process.execPath;
// Initialize transport and connect to server
this.transport = new StdioClientTransport({ command, args: [serverScriptPath] });
await this.mcp.connect(this.transport);
const transport = new StdioClientTransport({ command, args: [serverScriptPath] });
await this.mcp.connect(transport);
// List available tools
const toolsResult = await this.mcp.listTools();
@ -65,29 +64,40 @@ class MCPClient {
];
// Initial Claude API call
const response = await this.anthropic.messages.create({
let response = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
tools: this.tools,
});
// Process response and handle tool calls
// Process responses, executing tool calls until Claude stops requesting them
const finalText = [];
for (const content of response.content) {
if (content.type === 'text') {
finalText.push(content.text);
} else if (content.type === 'tool_use') {
// Execute tool call
const toolName = content.name;
const toolArgs = content.input as Record<string, unknown> | undefined;
while (true) {
const toolUses: Anthropic.ToolUseBlock[] = [];
for (const content of response.content) {
if (content.type === 'text') {
finalText.push(content.text);
} else if (content.type === 'tool_use') {
toolUses.push(content);
}
}
if (toolUses.length === 0) {
break;
}
// Execute every requested tool call and collect the results
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const toolUse of toolUses) {
const toolArgs = toolUse.input as Record<string, unknown>;
const result = await this.mcp.callTool({
name: toolName,
name: toolUse.name,
arguments: toolArgs,
});
finalText.push(`[Calling tool ${toolName} with args ${JSON.stringify(toolArgs)}]`);
finalText.push(`[Calling tool ${toolUse.name} with args ${JSON.stringify(toolArgs)}]`);
// Extract text from tool result content blocks
const toolResultText = result.content
@ -95,30 +105,33 @@ class MCPClient {
.map((block) => block.text)
.join('\n');
// Continue conversation with tool results
messages.push({
role: 'assistant',
content: response.content,
toolResults.push({
type: 'tool_result',
tool_use_id: toolUse.id,
content: toolResultText,
// Tell Claude when the tool call failed
...(result.isError ? { is_error: true } : {}),
});
messages.push({
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: content.id,
content: toolResultText,
}],
});
// Get next response from Claude
const followUp = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
});
const firstBlock = followUp.content[0];
finalText.push(firstBlock?.type === 'text' ? firstBlock.text : '');
}
// Continue the conversation: the assistant turn, then ALL tool
// results together in a single user turn
messages.push({
role: 'assistant',
content: response.content,
});
messages.push({
role: 'user',
content: toolResults,
});
// Get next response from Claude
response = await this.anthropic.messages.create({
model: ANTHROPIC_MODEL,
max_tokens: 1000,
messages,
tools: this.tools,
});
}
return finalText.join('\n');

View file

@ -10,6 +10,8 @@
//#region imports
import type {
AuthProvider,
CallToolResult,
InputRequiredResult,
OAuthClientInformationContext,
OAuthClientInformationMixed,
OAuthClientMetadata,
@ -19,16 +21,21 @@ import type {
} from '@modelcontextprotocol/client';
import {
applyMiddlewares,
checkResourceAllowed,
Client,
ClientCredentialsProvider,
createMiddleware,
CrossAppAccessProvider,
discoverAndRequestJwtAuthGrant,
isInputRequiredResult,
IssuerMismatchError,
LOG_LEVEL_META_KEY,
PrivateKeyJwtProvider,
ProtocolError,
resourceUrlFromServerUrl,
SdkError,
SdkErrorCode,
SdkHttpError,
SSEClientTransport,
StreamableHTTPClientTransport,
TRACEPARENT_META_KEY,
@ -296,6 +303,20 @@ function auth_oauthClientProvider(onRedirect: (url: URL) => void) {
authProvider: provider
});
//#endregion auth_oauthClientProvider
//#region auth_validateResourceURL
class PinnedResourceProvider extends MyOAuthProvider {
async validateResourceURL(serverUrl: string | URL, resource?: string): Promise<URL | undefined> {
const expected = resourceUrlFromServerUrl(serverUrl); // strips the fragment (RFC 8707 §2)
if (resource && !checkResourceAllowed({ requestedResource: expected, configuredResource: resource })) {
throw new Error(`Refusing resource ${resource} for server ${expected.href}`);
}
return expected;
}
}
//#endregion auth_validateResourceURL
void PinnedResourceProvider;
return { provider, transport };
}
@ -465,6 +486,23 @@ async function complete_basic(client: Client) {
//#endregion complete_basic
}
// ---------------------------------------------------------------------------
// Response caching
// ---------------------------------------------------------------------------
/** Example: Per-call cache disposition via cacheMode. */
async function responseCache_basic(client: Client) {
//#region responseCache_basic
const tools = await client.listTools(); // network, then cached for the server's ttlMs
const again = await client.listTools(); // served from cache while still fresh
await client.listTools(undefined, { cacheMode: 'refresh' }); // always refetch and re-store
await client.readResource({ uri: 'config://app' }, { cacheMode: 'bypass' }); // no cache read or write
//#endregion responseCache_basic
void tools;
void again;
}
// ---------------------------------------------------------------------------
// Notifications
// ---------------------------------------------------------------------------
@ -493,6 +531,18 @@ async function setLoggingLevel_basic(client: Client) {
//#endregion setLoggingLevel_basic
}
/** Example: Per-request log-level opt-in on a 2026-07-28 connection. */
async function logLevelMeta_modern(client: Client) {
//#region logLevelMeta_modern
const result = await client.callTool({
name: 'fetch-data',
arguments: { url: 'https://example.com' },
_meta: { [LOG_LEVEL_META_KEY]: 'debug' }
});
//#endregion logLevelMeta_modern
void result;
}
/** Example: Automatic list-change tracking via the listChanged option. */
async function listChanged_basic() {
//#region listChanged_basic
@ -519,11 +569,45 @@ async function listChanged_basic() {
return client;
}
/** Example: Open a subscriptions/listen stream explicitly (2026-07-28). */
async function listen_basic(client: Client) {
//#region listen_basic
client.setNotificationHandler('notifications/tools/list_changed', async () => {
const { tools } = await client.listTools();
console.log('Tools changed:', tools.length);
});
client.setNotificationHandler('notifications/resources/updated', async notification => {
console.log('Resource updated:', notification.params.uri);
});
const subscription = await client.listen({
toolsListChanged: true,
resourceSubscriptions: ['config://app']
});
console.log('Server honored:', subscription.honoredFilter);
// Later: tear the stream down
await subscription.close();
//#endregion listen_basic
}
/** Example: Watch loop that re-listens on unexpected closes. */
async function listen_watchLoop(client: Client, watching: boolean) {
//#region listen_watchLoop
while (watching) {
const sub = await client.listen({ resourceSubscriptions: ['config://app'] });
const reason = await sub.closed;
if (reason !== 'remote') break; // 'local' or 'graceful': done
await new Promise(resolve => setTimeout(resolve, 1000)); // back off, then re-listen
}
//#endregion listen_watchLoop
}
// ---------------------------------------------------------------------------
// Handling server-initiated requests
// ---------------------------------------------------------------------------
/** Example: Declare client capabilities for sampling and elicitation. */
/** Example: Declare client capabilities for sampling, elicitation, and roots. */
function capabilities_declaration() {
//#region capabilities_declaration
const client = new Client(
@ -531,7 +615,8 @@ function capabilities_declaration() {
{
capabilities: {
sampling: {},
elicitation: { form: {} }
elicitation: { form: {} },
roots: { listChanged: true }
}
}
);
@ -590,6 +675,42 @@ function roots_handler(client: Client) {
//#endregion roots_handler
}
/** Example: Manual multi-round-trip handling with autoFulfill disabled (2026-07-28). */
async function inputRequired_manual(transport: StreamableHTTPClientTransport) {
//#region inputRequired_manual
const client = new Client(
{ name: 'my-client', version: '1.0.0' },
{
capabilities: { elicitation: { form: {} } },
versionNegotiation: { mode: 'auto' },
inputRequired: { autoFulfill: false }
}
);
await client.connect(transport);
const value = (await client.request(
{ method: 'tools/call', params: { name: 'deploy', arguments: { env: 'prod' } } },
{ allowInputRequired: true }
)) as CallToolResult | InputRequiredResult;
if (isInputRequiredResult(value)) {
// Collect responses for value.inputRequests from your UI, then retry:
await client.request(
{
method: 'tools/call',
params: {
name: 'deploy',
arguments: { env: 'prod' },
inputResponses: { confirm: { action: 'accept', content: { confirm: true } } },
requestState: value.requestState // echo byte-exact
}
},
{ allowInputRequired: true }
);
}
//#endregion inputRequired_manual
}
// ---------------------------------------------------------------------------
// Error handling
// ---------------------------------------------------------------------------
@ -655,6 +776,21 @@ async function errorHandling_timeout(client: Client) {
//#endregion errorHandling_timeout
}
/** Example: Typed HTTP transport errors. */
async function errorHandling_http(client: Client, transport: StreamableHTTPClientTransport) {
//#region errorHandling_http
try {
await client.connect(transport);
} catch (error) {
if (error instanceof SdkHttpError) {
console.error(`HTTP ${error.status} (${error.statusText ?? ''}) [${error.code}]`);
} else {
throw error;
}
}
//#endregion errorHandling_http
}
// ---------------------------------------------------------------------------
// Advanced patterns
// ---------------------------------------------------------------------------
@ -765,16 +901,22 @@ void readResource_basic;
void subscribeResource_basic;
void getPrompt_basic;
void complete_basic;
void responseCache_basic;
void notificationHandler_basic;
void setLoggingLevel_basic;
void logLevelMeta_modern;
void listChanged_basic;
void listen_basic;
void listen_watchLoop;
void capabilities_declaration;
void sampling_handler;
void elicitation_handler;
void roots_handler;
void inputRequired_manual;
void errorHandling_toolErrors;
void errorHandling_lifecycle;
void errorHandling_timeout;
void errorHandling_http;
void middleware_basic;
void traceContext_perRequest;
void traceContext_middleware;

View file

@ -9,12 +9,28 @@
//#region imports
import { randomUUID } from 'node:crypto';
import { createServer } from 'node:http';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ResourceLink } from '@modelcontextprotocol/server';
import { completable, McpServer, ResourceTemplate, TRACEPARENT_META_KEY } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import {
createMcpExpressApp,
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
requireBearerAuth
} from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport, toNodeHandler } from '@modelcontextprotocol/node';
import type { CallToolResult, InputRequiredResult, OAuthMetadata, ResourceLink } from '@modelcontextprotocol/server';
import {
acceptedContent,
completable,
createMcpHandler,
createRequestStateCodec,
inputRequired,
McpServer,
ResourceTemplate,
TRACEPARENT_META_KEY
} from '@modelcontextprotocol/server';
import { serveStdio, StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
//#endregion imports
@ -289,6 +305,40 @@ function extensionCapabilities_register(server: McpServer) {
//#endregion extensionCapabilities_register
}
// ---------------------------------------------------------------------------
// Cache hints
// ---------------------------------------------------------------------------
/** Example: cache hints via ServerOptions.cacheHints and a per-resource cacheHint. */
function cacheHints_basic() {
//#region cacheHints_basic
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{
cacheHints: {
// The tool list is the same for every caller and rarely changes:
'tools/list': { ttlMs: 60_000, cacheScope: 'public' }
}
}
);
server.registerResource(
'config',
'config://app',
{
mimeType: 'text/plain',
// Wins field-by-field over a cacheHints['resources/read'] entry;
// cacheScope stays at the 'private' default here.
cacheHint: { ttlMs: 300_000 }
},
async uri => ({
contents: [{ uri: uri.href, text: 'App configuration here' }]
})
);
//#endregion cacheHints_basic
return server;
}
// ---------------------------------------------------------------------------
// Logging
// ---------------------------------------------------------------------------
@ -379,6 +429,50 @@ function registerTool_traceContext(server: McpServer) {
//#endregion registerTool_traceContext
}
// ---------------------------------------------------------------------------
// Change notifications
// ---------------------------------------------------------------------------
/** Example: hand-wired resources/subscribe handlers + sendResourceUpdated (2025-era). */
function subscriptions_legacy() {
//#region subscriptions_legacy
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{ capabilities: { resources: { subscribe: true, listChanged: true } } }
);
const subscriptions = new Set<string>();
server.server.setRequestHandler('resources/subscribe', async request => {
subscriptions.add(request.params.uri);
return {};
});
server.server.setRequestHandler('resources/unsubscribe', async request => {
subscriptions.delete(request.params.uri);
return {};
});
// When the underlying data changes:
async function onConfigChanged() {
if (subscriptions.has('config://app')) {
await server.server.sendResourceUpdated({ uri: 'config://app' });
}
}
//#endregion subscriptions_legacy
return onConfigChanged;
}
/** Example: publishing change events through the createMcpHandler notify facade (2026-07-28). */
function subscriptions_notify(buildServer: () => McpServer) {
//#region subscriptions_notify
const handler = createMcpHandler(() => buildServer());
// When the underlying data changes:
handler.notify.resourceUpdated('config://app');
handler.notify.toolsChanged();
//#endregion subscriptions_notify
return handler;
}
// ---------------------------------------------------------------------------
// Server-initiated requests
// ---------------------------------------------------------------------------
@ -479,6 +573,90 @@ function registerTool_roots(server: McpServer) {
//#endregion registerTool_roots
}
/** Example: write-once tool requesting input via an input_required return (2026-07-28). */
function registerTool_inputRequired(server: McpServer) {
//#region registerTool_inputRequired
server.registerTool(
'deploy',
{
description: 'Deploy after user confirmation',
inputSchema: z.object({ env: z.string() })
},
async ({ env }, ctx): Promise<CallToolResult | InputRequiredResult> => {
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: `Deploy to ${env}?`,
requestedSchema: {
type: 'object',
properties: { confirm: { type: 'boolean' } },
required: ['confirm']
}
})
}
});
}
return { content: [{ type: 'text', text: `Deployed to ${env}` }] };
}
);
//#endregion registerTool_inputRequired
}
/** Example: HMAC-protected requestState via createRequestStateCodec + the verify hook. */
function requestState_codec() {
//#region requestState_codec
const stateCodec = createRequestStateCodec<{ step: string }>({
key: crypto.getRandomValues(new Uint8Array(32)), // >= 32 bytes; share across instances in a fleet
ttlSeconds: 600
});
const server = new McpServer(
{ name: 'my-server', version: '1.0.0' },
{ capabilities: { tools: {} }, requestState: { verify: stateCodec.verify } }
);
//#endregion requestState_codec
//#region requestState_mintDecode
server.registerTool(
'wipe-cache',
{ description: 'Confirm, then pick a scope, then wipe', inputSchema: z.object({}) },
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
const state = ctx.mcpReq.requestState<{ step: string }>();
if (state?.step !== 'confirmed') {
const confirmed = acceptedContent<{ confirm: boolean }>(ctx.mcpReq.inputResponses, 'confirm');
if (confirmed?.confirm !== true) {
return inputRequired({
inputRequests: {
confirm: inputRequired.elicit({
message: 'Really wipe the cache?',
requestedSchema: { type: 'object', properties: { confirm: { type: 'boolean' } }, required: ['confirm'] }
})
}
});
}
// Mint only what the response above already proved: the user confirmed.
return inputRequired({
inputRequests: {
scope: inputRequired.elicit({
message: 'Which scope?',
requestedSchema: { type: 'object', properties: { scope: { type: 'string' } }, required: ['scope'] }
})
},
requestState: await stateCodec.mint({ step: 'confirmed' })
});
}
const scope = acceptedContent<{ scope: string }>(ctx.mcpReq.inputResponses, 'scope');
return { content: [{ type: 'text', text: `Wiped ${scope?.scope ?? 'all'}` }] };
}
);
//#endregion requestState_mintDecode
return server;
}
// ---------------------------------------------------------------------------
// Transports
// ---------------------------------------------------------------------------
@ -532,6 +710,38 @@ async function stdio_basic() {
//#endregion stdio_basic
}
/** Example: serveStdio serving both protocol eras on stdio from one factory. */
function serveStdio_basic() {
//#region serveStdio_basic
serveStdio(() => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// register tools/resources/prompts once; the same factory serves both eras
return server;
});
//#endregion serveStdio_basic
}
/** Example: createMcpHandler serving both protocol eras over HTTP from one factory. */
function createMcpHandler_basic() {
//#region createMcpHandler_basic
const handler = createMcpHandler(() => {
const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
// register tools/resources/prompts once; the same factory serves both eras
return server;
});
//#endregion createMcpHandler_basic
return handler;
}
/** Example: mounting an McpHttpHandler on node:http via toNodeHandler. */
function createMcpHandler_node(handler: ReturnType<typeof createMcpHandler>) {
//#region createMcpHandler_node
createServer(toNodeHandler(handler)).listen(3000);
// Express: app.all('/mcp', toNodeHandler(handler));
// behind express.json(): const node = toNodeHandler(handler); app.all('/mcp', (req, res) => void node(req, res, req.body));
//#endregion createMcpHandler_node
}
// ---------------------------------------------------------------------------
// Shutdown
// ---------------------------------------------------------------------------
@ -595,6 +805,50 @@ function dnsRebinding_allowedHosts() {
return app;
}
// ---------------------------------------------------------------------------
// Authorization (OAuth resource server)
// ---------------------------------------------------------------------------
/** Example: protecting an HTTP server as an OAuth resource server. */
function auth_resourceServer(
verifyJwt: (token: string) => Promise<{ sub: string; scopes: string[]; exp: number }>,
oauthMetadata: OAuthMetadata,
buildServer: () => McpServer
) {
//#region auth_resourceServer
const mcpServerUrl = new URL('https://api.example.com/mcp');
// Verify tokens however your deployment requires: JWT verification,
// RFC 7662 introspection, a call to your IdP.
const verifier: OAuthTokenVerifier = {
async verifyAccessToken(token) {
const payload = await verifyJwt(token);
return { token, clientId: payload.sub, scopes: payload.scopes, expiresAt: payload.exp };
}
};
// Public deployment: allow-list the public host (see DNS rebinding protection).
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['api.example.com'] });
// Serves /.well-known/oauth-protected-resource/mcp (RFC 9728) and mirrors the
// authorization server's metadata, so clients can discover your AS.
app.use(mcpAuthMetadataRouter({ oauthMetadata, resourceServerUrl: mcpServerUrl }));
// 401/403 responses carry `WWW-Authenticate: Bearer …` with `resource_metadata`
// pointing at the document above. That challenge is what starts the client
// SDK's OAuth flow.
const auth = requireBearerAuth({
verifier,
requiredScopes: ['mcp'],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
const node = toNodeHandler(createMcpHandler(buildServer));
app.all('/mcp', auth, (req, res) => void node(req, res, req.body));
//#endregion auth_resourceServer
return app;
}
// Suppress unused-function warnings (functions exist solely for type-checking)
void instructions_basic;
void registerTool_basic;
@ -608,16 +862,25 @@ void registerTool_traceContext;
void registerTool_sampling;
void registerTool_elicitation;
void registerTool_roots;
void registerTool_inputRequired;
void requestState_codec;
void registerResource_static;
void registerResource_template;
void registerPrompt_basic;
void registerPrompt_completion;
void extensionCapabilities_register;
void cacheHints_basic;
void subscriptions_legacy;
void subscriptions_notify;
void streamableHttp_stateful;
void streamableHttp_stateless;
void streamableHttp_jsonResponse;
void stdio_basic;
void serveStdio_basic;
void createMcpHandler_basic;
void createMcpHandler_node;
void shutdown_statefulHttp;
void shutdown_stdio;
void dnsRebinding_basic;
void dnsRebinding_allowedHosts;
void auth_resourceServer;

View file

@ -0,0 +1,5 @@
# server-quickstart
Source for the [Server Quickstart](../../docs/server-quickstart.md) tutorial: a stdio weather server exposing `get-alerts` and `get-forecast` tools. The tutorial walks through `src/index.ts` end to end.
The `package.json` and `tsconfig.json` here are monorepo-internal (`workspace:`/`catalog:` protocols; typecheck-only in CI). To build the server yourself, use the standalone manifests from the tutorial.

View file

@ -1,11 +1,8 @@
{
"name": "@modelcontextprotocol/examples-server-quickstart",
"name": "@mcp-examples/server-quickstart",
"private": true,
"version": "2.0.0-alpha.0",
"type": "module",
"bin": {
"weather": "./build/index.js"
},
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"

View file

@ -1,16 +1,10 @@
//#region prelude
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const NWS_API_BASE = 'https://api.weather.gov';
const USER_AGENT = 'weather-app/1.0';
// Create server instance
const server = new McpServer({
name: 'weather',
version: '1.0.0',
});
//#endregion prelude
//#region helpers
@ -83,140 +77,147 @@ interface ForecastResponse {
//#endregion helpers
//#region registerTools
// Register weather tools
server.registerTool(
'get-alerts',
{
title: 'Get Weather Alerts',
description: 'Get weather alerts for a state',
inputSchema: z.object({
state: z.string().length(2)
.describe('Two-letter state code (e.g. CA, NY)'),
}),
},
async ({ state }) => {
const stateCode = state.toUpperCase();
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
// Create a server with both weather tools registered. The serving entry calls
// this factory to build the instance it serves, so keep it cheap and
// side-effect-free.
function createServer(): McpServer {
const server = new McpServer({
name: 'weather',
version: '1.0.0',
});
server.registerTool(
'get-alerts',
{
title: 'Get Weather Alerts',
description: 'Get weather alerts for a state',
inputSchema: z.object({
state: z.string().length(2)
.describe('Two-letter state code (e.g. CA, NY)'),
}),
},
async ({ state }) => {
const stateCode = state.toUpperCase();
const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
if (!alertsData) {
return {
content: [{
type: 'text',
text: 'Failed to retrieve alerts data',
}],
isError: true,
};
}
const features = alertsData.features || [];
if (features.length === 0) {
return {
content: [{
type: 'text',
text: `No active alerts for ${stateCode}`,
}],
};
}
const formattedAlerts = features.map(formatAlert);
if (!alertsData) {
return {
content: [{
type: 'text' as const,
text: 'Failed to retrieve alerts data',
type: 'text',
text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`,
}],
};
}
},
);
const features = alertsData.features || [];
server.registerTool(
'get-forecast',
{
title: 'Get Weather Forecast',
description: 'Get weather forecast for a location',
inputSchema: z.object({
latitude: z.number().min(-90).max(90)
.describe('Latitude of the location'),
longitude: z.number().min(-180).max(180)
.describe('Longitude of the location'),
}),
},
async ({ latitude, longitude }) => {
// Get grid point data
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
if (!pointsData) {
return {
content: [{
type: 'text',
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
}],
isError: true,
};
}
const forecastUrl = pointsData.properties?.forecast;
if (!forecastUrl) {
return {
content: [{
type: 'text',
text: 'Failed to get forecast URL from grid point data',
}],
isError: true,
};
}
// Get forecast data
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
if (!forecastData) {
return {
content: [{
type: 'text',
text: 'Failed to retrieve forecast data',
}],
isError: true,
};
}
const periods = forecastData.properties?.periods || [];
if (periods.length === 0) {
return {
content: [{
type: 'text',
text: 'No forecast periods available',
}],
};
}
// Format forecast periods
const formattedForecast = periods.map((period: ForecastPeriod) =>
[
`${period.name || 'Unknown'}:`,
`Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`,
`Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`,
`${period.shortForecast || 'No forecast available'}`,
'---',
].join('\n'),
);
if (features.length === 0) {
return {
content: [{
type: 'text' as const,
text: `No active alerts for ${stateCode}`,
type: 'text',
text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`,
}],
};
}
},
);
const formattedAlerts = features.map(formatAlert);
return {
content: [{
type: 'text' as const,
text: `Active alerts for ${stateCode}:\n\n${formattedAlerts.join('\n')}`,
}],
};
},
);
server.registerTool(
'get-forecast',
{
title: 'Get Weather Forecast',
description: 'Get weather forecast for a location',
inputSchema: z.object({
latitude: z.number().min(-90).max(90)
.describe('Latitude of the location'),
longitude: z.number().min(-180).max(180)
.describe('Longitude of the location'),
}),
},
async ({ latitude, longitude }) => {
// Get grid point data
const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
if (!pointsData) {
return {
content: [{
type: 'text' as const,
text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
}],
};
}
const forecastUrl = pointsData.properties?.forecast;
if (!forecastUrl) {
return {
content: [{
type: 'text' as const,
text: 'Failed to get forecast URL from grid point data',
}],
};
}
// Get forecast data
const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
if (!forecastData) {
return {
content: [{
type: 'text' as const,
text: 'Failed to retrieve forecast data',
}],
};
}
const periods = forecastData.properties?.periods || [];
if (periods.length === 0) {
return {
content: [{
type: 'text' as const,
text: 'No forecast periods available',
}],
};
}
// Format forecast periods
const formattedForecast = periods.map((period: ForecastPeriod) =>
[
`${period.name || 'Unknown'}:`,
`Temperature: ${period.temperature || 'Unknown'}°${period.temperatureUnit || 'F'}`,
`Wind: ${period.windSpeed || 'Unknown'} ${period.windDirection || ''}`,
`${period.shortForecast || 'No forecast available'}`,
'---',
].join('\n'),
);
return {
content: [{
type: 'text' as const,
text: `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join('\n')}`,
}],
};
},
);
return server;
}
//#endregion registerTools
//#region main
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Weather MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});
void serveStdio(createServer);
console.error('Weather MCP Server running on stdio');
//#endregion main