Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mcp-v2026-plain-descriptors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Fix 2026-07-28 MCP listings failing for resources, resource templates and prompts registered as plain objects.
9 changes: 4 additions & 5 deletions packages/effect/src/ai/internal/mcpProtocol/v2026_07_28.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,12 @@ const projectContent = Match.type<PublicMcpSchema.ContentBlock>().pipe(
Match.exhaustive
)

const projectResource = (resource: PublicMcpSchema.Resource) =>
McpProtocol.transcode(PublicMcpSchema.Resource, McpSchema.Resource, resource)
// Registrations may be plain objects, so encode with the wire struct.
const projectResource = (resource: PublicMcpSchema.Resource) => Schema.encodeEffect(McpSchema.Resource)(resource)
const projectResourceTemplate = (resourceTemplate: PublicMcpSchema.ResourceTemplate) =>
McpProtocol.transcode(PublicMcpSchema.ResourceTemplate, McpSchema.ResourceTemplate, resourceTemplate)
Schema.encodeEffect(McpSchema.ResourceTemplate)(resourceTemplate)

const projectPrompt = (prompt: PublicMcpSchema.Prompt) =>
McpProtocol.transcode(PublicMcpSchema.Prompt, McpSchema.Prompt, prompt)
const projectPrompt = (prompt: PublicMcpSchema.Prompt) => Schema.encodeEffect(McpSchema.Prompt)(prompt)

const projectTool = (tool: PublicMcpSchema.Tool) => McpProtocol.transcode(PublicMcpSchema.Tool, McpSchema.Tool, tool)

Expand Down
48 changes: 48 additions & 0 deletions packages/effect/test/ai/McpServer/v2026_07_28.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assert, describe, it } from "@effect/vitest"
import * as McpProtocol from "effect/ai/McpProtocol"
import * as McpSchema from "effect/ai/McpSchema"
import * as McpServer from "effect/ai/McpServer"
import * as Context from "effect/Context"
import * as Duration from "effect/Duration"
import * as Effect from "effect/Effect"
import * as Base64 from "effect/encoding/Base64"
Expand Down Expand Up @@ -544,6 +545,53 @@ it.layer(testLayer)(`Mcp Conformance (${protocol.protocolVersion})`, (it) => {
])
}))

it.effect("should list resources, templates and prompts registered as plain objects", () =>
Effect.gen(function*() {
const icon = { src: "https://example.com/icon.png" }
const registrations = Layer.effectDiscard(Effect.gen(function*() {
const server = yield* McpServer.McpServer
yield* server.addResource({
resource: { uri: "file:///plain", name: "Plain", icons: [icon] },
annotations: Context.empty(),
handle: Effect.succeed({ contents: [] })
})
yield* server.addResourceTemplate({
template: { uriTemplate: "file:///plain/{id}", name: "PlainTemplate", icons: [icon] },
annotations: Context.empty(),
routerPath: "file:///plain/:id",
completions: {},
handle: () => Effect.succeed({ contents: [] })
})
yield* server.addPrompt({
prompt: { name: "PlainPrompt", icons: [icon] },
annotations: Context.empty(),
completions: {},
handle: () => Effect.succeed({ messages: [] })
})
}))
const harness = yield* makeHttpHarness(registrations.pipe(Layer.provideMerge(makeServerLayer({
name: "PlainDescriptorsServer",
protocols: [protocol]
}))))
const list = Effect.fnUntraced(function*(method: string) {
const response = yield* harness.post(request(method, method), headers(method))
return Schema.decodeUnknownSync(Schema.Struct({ result: Schema.JsonObject }))(
yield* readMcpHttpResponse(response)
).result
})

const resources = yield* list("resources/list")
assert.deepStrictEqual(resources.resources, [{ uri: "file:///plain", name: "Plain", icons: [icon] }])
const templates = yield* list("resources/templates/list")
assert.deepStrictEqual(templates.resourceTemplates, [{
uriTemplate: "file:///plain/{id}",
name: "PlainTemplate",
icons: [icon]
}])
const prompts = yield* list("prompts/list")
assert.deepStrictEqual(prompts.prompts, [{ name: "PlainPrompt", icons: [icon] }])
}))

// SEP-2549 cache hints are server-advertised freshness; omitted families and resource reads keep the stale default.
it.effect("should advertise configured cache hints and keep defaults for other results", () =>
Effect.gen(function*() {
Expand Down
Loading