From 851dffeeddd9667df2a389c5f4b34835ce62dd9c Mon Sep 17 00:00:00 2001 From: Abe Wheeler Date: Wed, 6 May 2026 16:30:35 -0500 Subject: [PATCH] feat: expose MCP server-wide instructions on ServerConfig Adds an `instructions?: string` field to `ServerConfig` that's passed through to the MCP SDK and surfaced in the `initialize` response. Hosts may inject it into the model's system prompt to teach cross-tool relationships and constraints that don't fit in per-tool descriptions. Wired up in both the dev server (`createAppServer`) and the production server (`createProductionMcpServer`). Tests verify the field shows up in the initialize result when set and is absent otherwise. Documented in the server-entry and production-server pages, the MCP overview, the template's server.ts, the create-sunpeak-app skill, and CLAUDE.md. --- CLAUDE.md | 2 +- .../app-framework/tools/production-server.mdx | 2 +- docs/app-framework/tools/server-entry.mdx | 28 +++++++++++++++++++ docs/mcp-apps/mcp/overview.mdx | 2 +- examples/albums-example/package.json | 2 +- examples/albums-example/src/server.ts | 5 ++++ examples/carousel-example/package.json | 2 +- examples/carousel-example/src/server.ts | 5 ++++ examples/map-example/package.json | 2 +- examples/map-example/src/server.ts | 5 ++++ examples/review-example/package.json | 2 +- examples/review-example/src/server.ts | 5 ++++ packages/sunpeak/src/mcp/production-server.ts | 5 +++- packages/sunpeak/src/mcp/server.ts | 5 +++- packages/sunpeak/src/mcp/stateless.test.ts | 27 ++++++++++++++++++ packages/sunpeak/src/mcp/types.ts | 22 ++++++++++++++- packages/sunpeak/template/src/server.ts | 5 ++++ skills/create-sunpeak-app/SKILL.md | 2 +- 18 files changed, 117 insertions(+), 11 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f45e18b9..8843f641 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -312,6 +312,6 @@ The SDK's main entry (`app.d.ts`) uses `export * from "./types"` to re-export al - Resources discovered from `src/resources/{name}/{name}.tsx` - Tools discovered from `src/tools/{name}.ts` (each exports `tool: AppToolConfig`, `schema`, optional `outputSchema`, `default` handler) - Simulations discovered from `tests/simulations/*.json` (flat directory, `"tool"` string field references tool filename) -- Optional server entry at `src/server.ts` (exports `server: ServerConfig` for identity/icons, `auth()` for request authentication) +- Optional server entry at `src/server.ts` (exports `server: ServerConfig` for identity/icons/instructions, `auth()` for request authentication). `instructions` is a server-wide string sent in the MCP `initialize` response that hosts may inject into the model's system prompt — for cross-tool workflows and constraints. - Hook file naming: `use-{kebab-name}.ts` → export `use{PascalName}` (e.g., `use-download-file.ts` → `useDownloadFile`) - SDK re-exports in `src/index.ts` are organized into five sections: core classes/functions, app options/tool registration/Standard Schema types, method constants, Zod schemas, protocol types diff --git a/docs/app-framework/tools/production-server.mdx b/docs/app-framework/tools/production-server.mdx index 6107aa77..fd47bd9f 100644 --- a/docs/app-framework/tools/production-server.mdx +++ b/docs/app-framework/tools/production-server.mdx @@ -67,7 +67,7 @@ app.listen(3000); - Full server identity. Overrides `name` and `version` when provided. Supports additional fields like `title`, `description`, `websiteUrl`, and `icons`. + Full server identity. Overrides `name` and `version` when provided. Supports additional fields like `title`, `description`, `websiteUrl`, `icons`, and `instructions` (sent in the MCP `initialize` response so hosts can inject it into the model's system prompt). See [Server Entry](/app-framework/tools/server-entry#server-object-optional) for the full field list. diff --git a/docs/app-framework/tools/server-entry.mdx b/docs/app-framework/tools/server-entry.mdx index 800a1e91..f8ea51f9 100644 --- a/docs/app-framework/tools/server-entry.mdx +++ b/docs/app-framework/tools/server-entry.mdx @@ -86,6 +86,34 @@ export const server = { name: 'My App', version: '1.0.0' }; Server version. + + Human-readable display name. Shown by hosts when more polish is needed than `name`. + + + + Short description of what the server does. + + + + URL of the server's homepage. + + + + 64x64 PNG icons (data URIs preferred). Light and dark variants supported via the `theme` field on each icon. + + + + Server-wide instructions sent in the MCP `initialize` response. Hosts (ChatGPT, Claude) may surface this to the model, typically by injecting it into the system prompt. Use it for guidance that spans multiple tools or describes the server as a whole — e.g., "Always call `get_user` before `update_user`" or "This server is read-only between 5pm and 9am UTC". Per-tool guidance still belongs in each tool's `description`. + + ```ts + export const server: ServerConfig = { + name: 'My App', + version: '1.0.0', + instructions: 'Call list_orders before fulfill_order. Refunds require a manager scope.', + }; + ``` + + ## Using Auth in Tool Handlers The `authInfo` from the server entry is available in every tool handler: diff --git a/docs/mcp-apps/mcp/overview.mdx b/docs/mcp-apps/mcp/overview.mdx index a892cb58..faac769e 100644 --- a/docs/mcp-apps/mcp/overview.mdx +++ b/docs/mcp-apps/mcp/overview.mdx @@ -54,7 +54,7 @@ sequenceDiagram ``` 1. The client sends `initialize` with its capabilities and preferred protocol version. -2. The server responds with its own capabilities and the negotiated protocol version. +2. The server responds with its own capabilities and the negotiated protocol version. The response can also include an optional `instructions` string the host may surface to the model (e.g., as part of the system prompt) to describe cross-tool relationships and constraints. 3. The client sends `notifications/initialized` to confirm the session is ready. 4. Requests flow in both directions for the duration of the session. diff --git a/examples/albums-example/package.json b/examples/albums-example/package.json index 3757fd8d..bc3c0b9c 100644 --- a/examples/albums-example/package.json +++ b/examples/albums-example/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", - "sunpeak": "^0.20.16", + "sunpeak": "^0.20.17", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, diff --git a/examples/albums-example/src/server.ts b/examples/albums-example/src/server.ts index 5ccbf9b8..5f4130c6 100644 --- a/examples/albums-example/src/server.ts +++ b/examples/albums-example/src/server.ts @@ -22,10 +22,15 @@ export async function auth(req: IncomingMessage): Promise { * to embed the icon inline (no external fetch required by the host): * * icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }] + * + * `instructions` is a server-wide hint hosts may inject into the model's + * system prompt — useful for cross-tool workflows or constraints that + * don't fit in any single tool's `description`. */ export const server: ServerConfig = { // name defaults to package.json "name" field when omitted version: '1.0.0', description: 'A sunpeak MCP app', + // instructions: 'Always call get_user before update_user.', // icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }], }; diff --git a/examples/carousel-example/package.json b/examples/carousel-example/package.json index a6801888..60798dd8 100644 --- a/examples/carousel-example/package.json +++ b/examples/carousel-example/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "embla-carousel-wheel-gestures": "^8.1.0", - "sunpeak": "^0.20.16", + "sunpeak": "^0.20.17", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, diff --git a/examples/carousel-example/src/server.ts b/examples/carousel-example/src/server.ts index 5ccbf9b8..5f4130c6 100644 --- a/examples/carousel-example/src/server.ts +++ b/examples/carousel-example/src/server.ts @@ -22,10 +22,15 @@ export async function auth(req: IncomingMessage): Promise { * to embed the icon inline (no external fetch required by the host): * * icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }] + * + * `instructions` is a server-wide hint hosts may inject into the model's + * system prompt — useful for cross-tool workflows or constraints that + * don't fit in any single tool's `description`. */ export const server: ServerConfig = { // name defaults to package.json "name" field when omitted version: '1.0.0', description: 'A sunpeak MCP app', + // instructions: 'Always call get_user before update_user.', // icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }], }; diff --git a/examples/map-example/package.json b/examples/map-example/package.json index d12b208c..bc3a8bf2 100644 --- a/examples/map-example/package.json +++ b/examples/map-example/package.json @@ -18,7 +18,7 @@ "clsx": "^2.1.1", "embla-carousel-react": "^8.6.0", "mapbox-gl": "^3.23.1", - "sunpeak": "^0.20.16", + "sunpeak": "^0.20.17", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, diff --git a/examples/map-example/src/server.ts b/examples/map-example/src/server.ts index 5ccbf9b8..5f4130c6 100644 --- a/examples/map-example/src/server.ts +++ b/examples/map-example/src/server.ts @@ -22,10 +22,15 @@ export async function auth(req: IncomingMessage): Promise { * to embed the icon inline (no external fetch required by the host): * * icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }] + * + * `instructions` is a server-wide hint hosts may inject into the model's + * system prompt — useful for cross-tool workflows or constraints that + * don't fit in any single tool's `description`. */ export const server: ServerConfig = { // name defaults to package.json "name" field when omitted version: '1.0.0', description: 'A sunpeak MCP app', + // instructions: 'Always call get_user before update_user.', // icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }], }; diff --git a/examples/review-example/package.json b/examples/review-example/package.json index 683281c7..dd1944fb 100644 --- a/examples/review-example/package.json +++ b/examples/review-example/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "clsx": "^2.1.1", - "sunpeak": "^0.20.16", + "sunpeak": "^0.20.17", "tailwind-merge": "^3.5.0", "zod": "^4.4.3" }, diff --git a/examples/review-example/src/server.ts b/examples/review-example/src/server.ts index 5ccbf9b8..5f4130c6 100644 --- a/examples/review-example/src/server.ts +++ b/examples/review-example/src/server.ts @@ -22,10 +22,15 @@ export async function auth(req: IncomingMessage): Promise { * to embed the icon inline (no external fetch required by the host): * * icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }] + * + * `instructions` is a server-wide hint hosts may inject into the model's + * system prompt — useful for cross-tool workflows or constraints that + * don't fit in any single tool's `description`. */ export const server: ServerConfig = { // name defaults to package.json "name" field when omitted version: '1.0.0', description: 'A sunpeak MCP app', + // instructions: 'Always call get_user before update_user.', // icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }], }; diff --git a/packages/sunpeak/src/mcp/production-server.ts b/packages/sunpeak/src/mcp/production-server.ts index 53fbfb0d..3e7bd224 100644 --- a/packages/sunpeak/src/mcp/production-server.ts +++ b/packages/sunpeak/src/mcp/production-server.ts @@ -261,7 +261,10 @@ export function createProductionMcpServer(config: ProductionServerConfig): McpSe }, ], }, - { capabilities: { resources: {}, tools: {} } } + { + capabilities: { resources: {}, tools: {} }, + ...(serverInfo?.instructions ? { instructions: serverInfo.instructions } : {}), + } ); // Build resource lookup: resource name → ProductionResource diff --git a/packages/sunpeak/src/mcp/server.ts b/packages/sunpeak/src/mcp/server.ts index bea1aece..964f923b 100644 --- a/packages/sunpeak/src/mcp/server.ts +++ b/packages/sunpeak/src/mcp/server.ts @@ -290,7 +290,10 @@ function createAppServer( }, ], }, - { capabilities: { resources: {}, tools: {} } } + { + capabilities: { resources: {}, tools: {} }, + ...(serverInfo?.instructions ? { instructions: serverInfo.instructions } : {}), + } ); // Capture the connecting host's clientInfo.name after MCP initialization. diff --git a/packages/sunpeak/src/mcp/stateless.test.ts b/packages/sunpeak/src/mcp/stateless.test.ts index 104b9b8b..82f5b882 100644 --- a/packages/sunpeak/src/mcp/stateless.test.ts +++ b/packages/sunpeak/src/mcp/stateless.test.ts @@ -159,6 +159,33 @@ describe('createHandler stateless mode', () => { }); }); +describe('serverInfo.instructions', () => { + it('includes instructions in initialize result when set', async () => { + const text = 'Always call get_user before update_user.'; + const handler = createHandler({ + ...baseWebConfig, + serverInfo: { name: 'test', version: '1.0.0', instructions: text }, + }); + const res = await handler(makePostRequest(initializeBody())); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.result?.instructions).toBe(text); + }); + + it('omits instructions from initialize result when unset', async () => { + const handler = createHandler({ + ...baseWebConfig, + serverInfo: { name: 'test', version: '1.0.0' }, + }); + const res = await handler(makePostRequest(initializeBody())); + expect(res.status).toBe(200); + + const body = await res.json(); + expect(body.result?.instructions).toBeUndefined(); + }); +}); + describe('createHandler stateful mode (default)', () => { it('returns 404 for unknown session ID', async () => { const handler = createHandler({ ...baseWebConfig, stateless: false }); diff --git a/packages/sunpeak/src/mcp/types.ts b/packages/sunpeak/src/mcp/types.ts index ee9d2541..3b4435a4 100644 --- a/packages/sunpeak/src/mcp/types.ts +++ b/packages/sunpeak/src/mcp/types.ts @@ -24,12 +24,17 @@ export type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; * * If omitted, a default sunpeak icon is used. * + * `instructions` is sent in the MCP initialize response. Hosts may inject it + * into the model's system prompt to teach cross-tool relationships, workflow + * patterns, and constraints that don't fit in per-tool descriptions. + * * @example * ```ts * export const server: ServerConfig = { * name: 'my-app', * version: '1.0.0', * description: 'My MCP app', + * instructions: 'Always call get_user before update_user. Read-only after 5pm UTC.', * icons: [ * { src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }, * { src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'], theme: 'dark' }, @@ -37,7 +42,22 @@ export type { AuthInfo } from '@modelcontextprotocol/sdk/server/auth/types.js'; * }; * ``` */ -export type ServerConfig = Partial; +export type ServerConfig = Partial & { + /** + * Server-wide instructions sent in the MCP initialize response. + * + * Hosts (ChatGPT, Claude) may surface this string to the model, typically + * by injecting it into the system prompt. Use it for guidance that spans + * multiple tools or describes the server as a whole, e.g.: + * + * - "Always call `get_user` before `update_user`" + * - "This server is read-only between 5pm and 9am UTC" + * - "Prefer `search_albums` over listing all albums for queries with a name" + * + * Per-tool guidance still belongs in each tool's `description`. + */ + instructions?: string; +}; /** * Extra context passed to tool handlers as the second argument. diff --git a/packages/sunpeak/template/src/server.ts b/packages/sunpeak/template/src/server.ts index 5ccbf9b8..5f4130c6 100644 --- a/packages/sunpeak/template/src/server.ts +++ b/packages/sunpeak/template/src/server.ts @@ -22,10 +22,15 @@ export async function auth(req: IncomingMessage): Promise { * to embed the icon inline (no external fetch required by the host): * * icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }] + * + * `instructions` is a server-wide hint hosts may inject into the model's + * system prompt — useful for cross-tool workflows or constraints that + * don't fit in any single tool's `description`. */ export const server: ServerConfig = { // name defaults to package.json "name" field when omitted version: '1.0.0', description: 'A sunpeak MCP app', + // instructions: 'Always call get_user before update_user.', // icons: [{ src: 'data:image/png;base64,...', mimeType: 'image/png', sizes: ['64x64'] }], }; diff --git a/skills/create-sunpeak-app/SKILL.md b/skills/create-sunpeak-app/SKILL.md index 2fa3a64d..a431e723 100644 --- a/skills/create-sunpeak-app/SKILL.md +++ b/skills/create-sunpeak-app/SKILL.md @@ -27,7 +27,7 @@ sunpeak-app/ │ │ └── {name}.tsx # Resource component + ResourceConfig export │ ├── tools/ │ │ └── {name}.ts # Tool metadata, Zod schema, handler -│ ├── server.ts # Optional server entry (auth, config, icons) +│ ├── server.ts # Optional server entry (auth, identity, icons, instructions) │ └── styles/ │ └── globals.css # Tailwind imports ├── tests/