feat(core): support MCP resources

This commit is contained in:
Aiden Cline 2026-07-06 23:34:15 -05:00
commit ab5bd520e2
11 changed files with 931 additions and 85 deletions

View file

@ -9,6 +9,7 @@ export { Form } from "./form.js"
export { Integration } from "./integration.js"
export { LLM } from "./llm.js"
export { Location } from "./location.js"
export { Mcp } from "./mcp.js"
export { Model } from "./model.js"
export { Permission } from "./permission.js"
export { PermissionSaved } from "./permission-saved.js"

View file

@ -10,6 +10,13 @@ export const ToolsChanged = Event.ephemeral({
},
})
export const ResourcesChanged = Event.ephemeral({
type: "mcp.resources.changed",
schema: {
server: Schema.String,
},
})
export const BrowserOpenFailed = Event.ephemeral({
type: "mcp.browser.open.failed",
schema: {
@ -27,4 +34,4 @@ export const StatusChanged = Event.ephemeral({
},
})
export const Definitions = Event.inventory(ToolsChanged, StatusChanged)
export const Definitions = Event.inventory(ToolsChanged, ResourcesChanged, StatusChanged)

View file

@ -25,14 +25,9 @@ const NeedsClientRegistration = Schema.Struct({
}).annotate({ identifier: "Mcp.Status.NeedsClientRegistration" })
export type Status = typeof Status.Type
export const Status = Schema.Union([
Connected,
Pending,
Disabled,
Failed,
NeedsAuth,
NeedsClientRegistration,
]).pipe(Schema.toTaggedUnion("status"))
export const Status = Schema.Union([Connected, Pending, Disabled, Failed, NeedsAuth, NeedsClientRegistration]).pipe(
Schema.toTaggedUnion("status"),
)
export interface Server extends Schema.Schema.Type<typeof Server> {}
export const Server = Schema.Struct({
@ -42,3 +37,50 @@ export const Server = Schema.Struct({
// without matching by name, which could collide with provider or plugin integrations.
integrationID: optional(IntegrationID),
}).annotate({ identifier: "Mcp.Server" })
export interface Resource extends Schema.Schema.Type<typeof Resource> {}
export const Resource = Schema.Struct({
server: Schema.String,
name: Schema.String,
uri: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.Resource" })
export interface ResourceTemplate extends Schema.Schema.Type<typeof ResourceTemplate> {}
export const ResourceTemplate = Schema.Struct({
server: Schema.String,
name: Schema.String,
uriTemplate: Schema.String,
description: optional(Schema.String),
mimeType: optional(Schema.String),
}).annotate({ identifier: "Mcp.ResourceTemplate" })
export interface ResourceCatalog extends Schema.Schema.Type<typeof ResourceCatalog> {}
export const ResourceCatalog = Schema.Struct({
resources: Schema.Array(Resource),
templates: Schema.Array(ResourceTemplate),
}).annotate({ identifier: "Mcp.ResourceCatalog" })
export const ResourceContentPart = Schema.Union([
Schema.Struct({
type: Schema.Literal("text"),
uri: Schema.String,
text: Schema.String,
mimeType: optional(Schema.String),
}),
Schema.Struct({
type: Schema.Literal("blob"),
uri: Schema.String,
blob: Schema.String,
mimeType: optional(Schema.String),
}),
]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Mcp.ResourceContentPart" }))
export type ResourceContentPart = typeof ResourceContentPart.Type
export interface ResourceContent extends Schema.Schema.Type<typeof ResourceContent> {}
export const ResourceContent = Schema.Struct({
server: Schema.String,
uri: Schema.String,
contents: Schema.Array(ResourceContentPart),
}).annotate({ identifier: "Mcp.ResourceContent" })

View file

@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
import { DateTime, Schema } from "effect"
import { Agent } from "../src/agent.js"
import { FileSystem } from "../src/filesystem.js"
import { Mcp } from "../src/mcp.js"
import { Model } from "../src/model.js"
import { Project } from "../src/project.js"
import { Provider } from "../src/provider.js"
@ -51,6 +52,11 @@ describe("contract hygiene", () => {
const identifiers = [
Agent.Color,
FileSystem.Submatch,
Mcp.Resource,
Mcp.ResourceTemplate,
Mcp.ResourceCatalog,
Mcp.ResourceContentPart,
Mcp.ResourceContent,
Model.Ref,
Model.Capabilities,
Model.Cost,

View file

@ -53,6 +53,7 @@ describe("public event manifest", () => {
expect(EventManifest.Server.get("mcp.status.changed")).toBe(McpEvent.StatusChanged)
expect(EventManifest.Server.get("session.deleted")).toBe(SessionEvent.Deleted)
expect(EventManifest.Server.has("mcp.tools.changed")).toBe(false)
expect(EventManifest.Server.has("mcp.resources.changed")).toBe(false)
expect(Agent.Event.Updated.durable).toBeUndefined()
expect(EventManifest.Durable.has("agent.updated")).toBe(false)
})
@ -76,7 +77,7 @@ describe("public event manifest", () => {
expect(Form.Event.Definitions).toEqual([Form.Event.Created, Form.Event.Replied, Form.Event.Cancelled])
expect(Reference.Event.Definitions).toEqual([Reference.Event.Updated])
expect(Plugin.Event.Definitions).toEqual([Plugin.Event.Added, Plugin.Event.Updated])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.StatusChanged])
expect(McpEvent.Definitions).toEqual([McpEvent.ToolsChanged, McpEvent.ResourcesChanged, McpEvent.StatusChanged])
expect(EventManifest.Latest.has("mcp.browser.open.failed")).toBe(false)
expect(EventManifest.Latest.has("ide.installed")).toBe(false)
expect(IdeEvent.Definitions).toEqual([IdeEvent.Installed])

View file

@ -0,0 +1,37 @@
import { describe, expect, test } from "bun:test"
import { Schema } from "effect"
import { Mcp } from "../src/mcp.js"
describe("Mcp resources", () => {
test("decodes resource catalogs and omits absent metadata", () => {
const value = Schema.decodeUnknownSync(Mcp.ResourceCatalog)({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
expect(Schema.encodeSync(Mcp.ResourceCatalog)(value)).toEqual({
resources: [{ server: "docs", name: "Readme", uri: "docs://readme" }],
templates: [{ server: "docs", name: "File", uriTemplate: "docs://{path}" }],
})
})
test("preserves text and base64 blob contents", () => {
expect(
Schema.decodeUnknownSync(Mcp.ResourceContent)({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
}),
).toEqual({
server: "docs",
uri: "docs://readme",
contents: [
{ type: "text", uri: "docs://readme", text: "hello", mimeType: "text/plain" },
{ type: "blob", uri: "docs://logo", blob: "aGVsbG8=", mimeType: "image/png" },
],
})
})
})