From 498b7cc708f7d86700aac0da6b821ff77bef7a38 Mon Sep 17 00:00:00 2001 From: Matt Carey Date: Mon, 27 Jul 2026 09:25:02 +0100 Subject: [PATCH] feat: add MCP server instructions --- src/constants.ts | 10 ++++++++++ src/server.ts | 10 ++++++++-- tests/helpers/mcp.ts | 25 +++++++++++++++++++++++++ tests/mcp-modern.test.ts | 28 +++++++++++++++++++++++++++- tests/non-codemode.test.ts | 5 ++++- 5 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2b1e57d..36c6be5 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,6 +12,16 @@ export type ServerInfo = { name: string; version: string } */ export const SERVER_INFO: ServerInfo = { name: 'cloudflare-api', version: '0.1.0' } +/** + * Concise, cross-product guidance advertised during MCP discovery and + * initialization. Keep each variant aligned with the tools exposed by its mode. + */ +export const CODEMODE_SERVER_INSTRUCTIONS = + 'Use `docs` for product questions and `search` for exact API contracts before `execute`. Before changes, verify target IDs and current state; ask if the account, zone, or resource is ambiguous. Filter and paginate results.' + +export const NON_CODEMODE_SERVER_INSTRUCTIONS = + 'Use `docs` for product questions and endpoint schemas as exact API contracts. Before changes, verify target IDs and current state; ask if the account, zone, or resource is ambiguous. Filter and paginate large results.' + /** * TypeScript declarations describing the `cloudflare` helper and `accountId` * binding available to the `execute` tool's sandboxed code. Inlined into the diff --git a/src/server.ts b/src/server.ts index a986b46..ae1d949 100644 --- a/src/server.ts +++ b/src/server.ts @@ -4,11 +4,17 @@ import { registerNonCodemodeTools } from './tools/non-codemode' import { registerSearchTool } from './tools/search' import { registerExecuteTool } from './tools/execute' import { attachMetrics } from './metrics' -import { SERVER_INFO } from './constants' +import { + CODEMODE_SERVER_INSTRUCTIONS, + NON_CODEMODE_SERVER_INSTRUCTIONS, + SERVER_INFO +} from './constants' import type { AuthProps } from './auth/types' export async function createServer(props: AuthProps, codemode = true): Promise { - const server = new McpServer(SERVER_INFO) + const server = new McpServer(SERVER_INFO, { + instructions: codemode ? CODEMODE_SERVER_INSTRUCTIONS : NON_CODEMODE_SERVER_INSTRUCTIONS + }) if (!codemode) { await registerNonCodemodeTools(server, props) diff --git a/tests/helpers/mcp.ts b/tests/helpers/mcp.ts index 40f70df..409935c 100644 --- a/tests/helpers/mcp.ts +++ b/tests/helpers/mcp.ts @@ -2,6 +2,7 @@ import { exports } from 'cloudflare:workers' export const MCP_URL = 'https://mcp.cloudflare.com/mcp' export const MCP_HOST = 'mcp.cloudflare.com' +export const LEGACY_MCP_VERSION = '2025-11-25' export const MODERN_MCP_VERSION = '2026-07-28' /** Result envelope of an MCP request over Streamable HTTP. */ @@ -10,6 +11,7 @@ export interface McpToolResult { resultType?: string supportedVersions?: string[] serverInfo?: { name: string; version: string } + instructions?: string content?: Array<{ type: string; text: string }> isError?: boolean tools?: Array<{ @@ -21,6 +23,29 @@ export interface McpToolResult { error?: { code: number; message: string } } +/** Build an MCP 2025 initialization request to the worker's `/mcp` endpoint. */ +export function mcpInitializeRequest(token: string, id = 1): Request { + return new Request(MCP_URL, { + method: 'POST', + headers: { + Host: MCP_HOST, + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + id, + method: 'initialize', + params: { + protocolVersion: LEGACY_MCP_VERSION, + capabilities: {}, + clientInfo: { name: 'cloudflare-mcp-tests', version: '1.0.0' } + } + }) + }) +} + /** Build a legacy JSON-RPC `tools/list` request to the worker's `/mcp` endpoint. */ export function mcpToolListRequest(token: string, id = 1): Request { return new Request(MCP_URL, { diff --git a/tests/mcp-modern.test.ts b/tests/mcp-modern.test.ts index 468fe8b..225b68a 100644 --- a/tests/mcp-modern.test.ts +++ b/tests/mcp-modern.test.ts @@ -9,9 +9,11 @@ import { mockIdentityProbe } from './helpers/cloudflare-api' import { clearKv } from './helpers/kv' +import { CODEMODE_SERVER_INSTRUCTIONS, NON_CODEMODE_SERVER_INSTRUCTIONS } from '../src/constants' import { MCP_HOST, MCP_URL, + mcpInitializeRequest, mcpToolListRequest, modernMcpRequest, parseMcpResult @@ -55,11 +57,24 @@ describe('MCP 2026-07-28 stateless handler', () => { result: { resultType: 'complete', supportedVersions: ['2026-07-28'], - serverInfo: { name: 'cloudflare-api', version: '0.1.0' } + serverInfo: { name: 'cloudflare-api', version: '0.1.0' }, + instructions: CODEMODE_SERVER_INSTRUCTIONS } }) }) + it('advertises mode-specific instructions for endpoint tools', async () => { + const response = await exports.default.fetch( + modernMcpRequest(API_TOKEN, 'server/discover', {}, { url: `${MCP_URL}?codemode=false` }) + ) + const body = await parseMcpResult(response) + + expect(response.status).toBe(200) + expect(body.result?.instructions).toBe(NON_CODEMODE_SERVER_INSTRUCTIONS) + expect(body.result?.instructions).not.toContain('`search`') + expect(body.result?.instructions).not.toContain('`execute`') + }) + it('serves modern tools/list with a complete result', async () => { const response = await exports.default.fetch(modernMcpRequest(API_TOKEN, 'tools/list')) const body = await parseMcpResult(response) @@ -210,6 +225,17 @@ describe('MCP 2026-07-28 stateless handler', () => { }) }) + it('advertises the same instructions during MCP 2025 initialization', async () => { + const response = await exports.default.fetch(mcpInitializeRequest(API_TOKEN)) + const body = await parseMcpResult(response) + + expect(response.status).toBe(200) + expect(body.result).toMatchObject({ + serverInfo: { name: 'cloudflare-api', version: '0.1.0' }, + instructions: CODEMODE_SERVER_INSTRUCTIONS + }) + }) + it('retains stateless 2025 compatibility by default', async () => { const response = await exports.default.fetch(mcpToolListRequest(API_TOKEN)) const body = await parseMcpResult(response) diff --git a/tests/non-codemode.test.ts b/tests/non-codemode.test.ts index bd91499..10f206c 100644 --- a/tests/non-codemode.test.ts +++ b/tests/non-codemode.test.ts @@ -2,6 +2,7 @@ import { afterEach, describe, it, expect, vi } from 'vitest' import { Client } from '@modelcontextprotocol/client' import { InMemoryTransport, McpServer } from '@modelcontextprotocol/server' import { createServer } from '../src/server' +import { CODEMODE_SERVER_INSTRUCTIONS } from '../src/constants' import { buildInputSchema, buildNonCodemodeTools, @@ -739,7 +740,9 @@ describe('createServer with codemode=false', () => { const execute = (server as any)._registeredTools['execute'] const accountIdDescription = execute.inputSchema.shape.account_id.description - expect((server as any).server._instructions).toBeUndefined() + expect((server as any).server._instructions).toBe(CODEMODE_SERVER_INSTRUCTIONS) + expect((server as any).server._instructions).not.toContain('acct-1') + expect((server as any).server._instructions).not.toContain('Account 1') expect(execute.description).toContain('Available accounts') expect(execute.description).toContain('acct-1 (Account 1)') expect(execute.description).toContain('acct-30 (Account 30)')