From 9e47b4dd1c06ec5c708afac768d5c2d9f7363497 Mon Sep 17 00:00:00 2001 From: Dmytro Maretskyi <35851437+dmaretskyi@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:19:29 +0200 Subject: [PATCH 1/3] Update OpenAiClient.ts --- packages/ai/openai/src/OpenAiClient.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ai/openai/src/OpenAiClient.ts b/packages/ai/openai/src/OpenAiClient.ts index 30617d223e1..d36275a3b03 100644 --- a/packages/ai/openai/src/OpenAiClient.ts +++ b/packages/ai/openai/src/OpenAiClient.ts @@ -165,18 +165,18 @@ export const make = (options: { inputTokens: chunk.usage.prompt_tokens, outputTokens: chunk.usage.completion_tokens, totalTokens: chunk.usage.prompt_tokens + chunk.usage.completion_tokens, - reasoningTokens: chunk.usage.completion_tokens_details.reasoning_tokens, - cacheReadInputTokens: chunk.usage.prompt_tokens_details.cached_tokens, + reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0, + cacheReadInputTokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0, cacheWriteInputTokens: usage.cacheWriteInputTokens } metadata = { ...metadata, serviceTier: chunk.service_tier, systemFingerprint: chunk.system_fingerprint, - acceptedPredictionTokens: chunk.usage.completion_tokens_details.accepted_prediction_tokens, - rejectedPredictionTokens: chunk.usage.completion_tokens_details.rejected_prediction_tokens, - inputAudioTokens: chunk.usage.prompt_tokens_details.audio_tokens, - outputAudioTokens: chunk.usage.completion_tokens_details.audio_tokens + acceptedPredictionTokens: chunk.usage.completion_tokens_details?.accepted_prediction_tokens ?? 0, + rejectedPredictionTokens: chunk.usage.completion_tokens_details?.rejected_prediction_tokens ?? 0, + inputAudioTokens: chunk.usage.prompt_tokens_details?.audio_tokens ?? 0, + outputAudioTokens: chunk.usage.completion_tokens_details?.audio_tokens ?? 0 } } @@ -300,7 +300,7 @@ interface RawUsage { readonly prompt_tokens: number readonly completion_tokens: number readonly total_tokens: number - readonly completion_tokens_details: { + readonly completion_tokens_details?: { readonly accepted_prediction_tokens: number readonly audio_tokens: number readonly reasoning_tokens: number From 5a5ec8ee6c27d2d3615ee8b7f9864533320bb6e7 Mon Sep 17 00:00:00 2001 From: Rich Burdon Date: Fri, 8 May 2026 23:53:26 +0100 Subject: [PATCH 2/3] feat(ai): support InitializeResult.instructions on McpServer Add an optional `instructions` field to the `McpServer` layer options (`layer`, `layerStdio`, `layerHttp`, `layerHttpRouter`). When provided, the string is returned in the `InitializeResult.instructions` field per the MCP spec, giving servers a built-in way to inject system-prompt-level guidance into clients at session start. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/mcp-server-instructions.md | 22 ++++++++ packages/ai/ai/src/McpServer.ts | 35 ++++++++++-- packages/ai/ai/test/McpServer.test.ts | 76 +++++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 .changeset/mcp-server-instructions.md create mode 100644 packages/ai/ai/test/McpServer.test.ts diff --git a/.changeset/mcp-server-instructions.md b/.changeset/mcp-server-instructions.md new file mode 100644 index 00000000000..d661a9e3c7e --- /dev/null +++ b/.changeset/mcp-server-instructions.md @@ -0,0 +1,22 @@ +--- +"@effect/ai": minor +--- + +Add optional `instructions` to `McpServer` layer options. + +When provided, the string is returned in the `InitializeResult.instructions` +field per the [MCP specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#initialization). +Clients can use it to improve the LLM's understanding of the server (for +example, by injecting it into the system prompt). + +```ts +McpServer.layerHttp({ + name: "Demo Server", + version: "1.0.0", + path: "/mcp", + instructions: "Always greet the user with a friendly hello." +}) +``` + +The option is supported on `layer`, `layerStdio`, `layerHttp`, and +`layerHttpRouter`. It is omitted from the response when not set. diff --git a/packages/ai/ai/src/McpServer.ts b/packages/ai/ai/src/McpServer.ts index e29b2f6bb64..9a921817796 100644 --- a/packages/ai/ai/src/McpServer.ts +++ b/packages/ai/ai/src/McpServer.ts @@ -295,7 +295,7 @@ const SUPPORTED_PROTOCOL_VERSIONS = [ * @category Constructors */ export const run: ( - options: { readonly name: string; readonly version: string } + options: { readonly name: string; readonly version: string; readonly instructions?: string | undefined } ) => Effect.Effect< never, never, @@ -303,6 +303,7 @@ export const run: ( > = Effect.fnUntraced(function*(options: { readonly name: string readonly version: string + readonly instructions?: string | undefined }) { const protocol = yield* RpcServer.Protocol const handlers = yield* Layer.build(layerHandlers(options)) @@ -437,6 +438,7 @@ export const run: ( export const layer = (options: { readonly name: string readonly version: string + readonly instructions?: string | undefined }): Layer.Layer => Layer.scopedDiscard(Effect.forkScoped(run(options))).pipe( Layer.provideMerge(McpServer.layer) @@ -504,6 +506,14 @@ export const layerStdio = (options: { readonly version: string readonly stdin: Stream readonly stdout: Sink + /** + * Optional instructions describing how to use the server and its features. + * + * When set, this string is returned in the `InitializeResult.instructions` + * field per the MCP specification. Clients can use it to improve the LLM's + * understanding of the server (e.g. by injecting it into the system prompt). + */ + readonly instructions?: string | undefined }): Layer.Layer => layer(options).pipe( Layer.provide(RpcServer.layerProtocolStdio({ @@ -579,6 +589,14 @@ export const layerHttp = (options: { readonly version: string readonly path: HttpRouter.PathInput readonly routerTag?: HttpRouter.HttpRouter.TagClass + /** + * Optional instructions describing how to use the server and its features. + * + * When set, this string is returned in the `InitializeResult.instructions` + * field per the MCP specification. Clients can use it to improve the LLM's + * understanding of the server (e.g. by injecting it into the system prompt). + */ + readonly instructions?: string | undefined }): Layer.Layer => layer(options).pipe( Layer.provide(RpcServer.layerProtocolHttp(options)), @@ -597,6 +615,14 @@ export const layerHttpRouter = (options: { readonly name: string readonly version: string readonly path: HttpRouter.PathInput + /** + * Optional instructions describing how to use the server and its features. + * + * When set, this string is returned in the `InitializeResult.instructions` + * field per the MCP specification. Clients can use it to improve the LLM's + * understanding of the server (e.g. by injecting it into the system prompt). + */ + readonly instructions?: string | undefined }): Layer.Layer => layer(options).pipe( Layer.provide(RpcServer.layerProtocolHttpRouter(options)), @@ -1132,13 +1158,15 @@ const compileUriTemplate = (segments: TemplateStringsArray, ...schemas: Readonly } as const } -const layerHandlers = (serverInfo: { +const layerHandlers = (options: { readonly name: string readonly version: string + readonly instructions?: string | undefined }) => ClientRpcs.toLayer( Effect.gen(function*() { const server = yield* McpServer + const serverInfo = { name: options.name, version: options.version } return { // Requests @@ -1166,7 +1194,8 @@ const layerHandlers = (serverInfo: { serverInfo, protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(requestedVersion) ? requestedVersion - : LATEST_PROTOCOL_VERSION + : LATEST_PROTOCOL_VERSION, + ...(options.instructions !== undefined ? { instructions: options.instructions } : {}) }) }, "completion/complete": server.completion, diff --git a/packages/ai/ai/test/McpServer.test.ts b/packages/ai/ai/test/McpServer.test.ts new file mode 100644 index 00000000000..e614522e627 --- /dev/null +++ b/packages/ai/ai/test/McpServer.test.ts @@ -0,0 +1,76 @@ +import { assert, describe, it } from "@effect/vitest" +import { Effect, Layer, Mailbox, Sink } from "effect" +import * as McpServer from "../src/McpServer.js" + +const encoder = new TextEncoder() +const decoder = new TextDecoder() + +const initializeRequest = { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { + protocolVersion: "2025-06-18", + capabilities: {}, + clientInfo: { name: "test-client", version: "0.0.0" } + } +} + +const initializeResultFor = ( + options: { readonly instructions?: string | undefined } +): Effect.Effect => + Effect.gen(function*() { + const stdinMailbox = yield* Mailbox.make() + const stdoutMailbox = yield* Mailbox.make() + + const stdin = Mailbox.toStream(stdinMailbox) + const stdout = Sink.forEach((chunk: Uint8Array | string) => stdoutMailbox.offer(chunk)) + + const ServerLayer = McpServer.layerStdio({ + name: "test-server", + version: "1.0.0", + stdin, + stdout, + ...(options.instructions !== undefined ? { instructions: options.instructions } : {}) + }) + + yield* Effect.forkScoped(Layer.launch(ServerLayer)) + + yield* stdinMailbox.offer(encoder.encode(JSON.stringify(initializeRequest) + "\n")) + + let response: any + while (response === undefined) { + const chunk = yield* stdoutMailbox.take + const text = typeof chunk === "string" ? chunk : decoder.decode(chunk) + for (const line of text.split("\n")) { + if (line.trim() === "") continue + const parsed = JSON.parse(line) + if (parsed.id === 1) { + response = parsed + break + } + } + } + + return response + }).pipe(Effect.scoped) as Effect.Effect + +describe("McpServer", () => { + describe("instructions", () => { + it.effect("returns instructions in InitializeResult when set", () => + Effect.gen(function*() { + const response = yield* initializeResultFor({ instructions: "be helpful" }) + assert.strictEqual(response.result.instructions, "be helpful") + assert.deepStrictEqual(response.result.serverInfo, { + name: "test-server", + version: "1.0.0" + }) + })) + + it.effect("omits instructions in InitializeResult when not set", () => + Effect.gen(function*() { + const response = yield* initializeResultFor({}) + assert.isFalse("instructions" in response.result) + })) + }) +}) From fe13c48b773ace9f9d2ac6f3bae28745dce67214 Mon Sep 17 00:00:00 2001 From: Rich Burdon Date: Wed, 13 May 2026 00:03:50 +0100 Subject: [PATCH 3/3] chore: downgrade changeset to patch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the repo's @effect/ai release cadence — recent additive changes (0.33.x, 0.34.0, 0.35.0) have all shipped as patches, and a minor here cascades into major bumps for all five @effect/ai-* provider packages because they peer-depend on @effect/ai. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/mcp-server-instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mcp-server-instructions.md b/.changeset/mcp-server-instructions.md index d661a9e3c7e..0115766b945 100644 --- a/.changeset/mcp-server-instructions.md +++ b/.changeset/mcp-server-instructions.md @@ -1,5 +1,5 @@ --- -"@effect/ai": minor +"@effect/ai": patch --- Add optional `instructions` to `McpServer` layer options.