feat(core): surface mcp server instructions in the system prompt
Capture each server's initialize instructions on the Connection at connect
time and expose them via MCP.instructions(). Add McpGuidance, a system-context
source modeled on SkillGuidance, that renders the <mcp_instructions> block and
hides servers whose contributed tools are all denied for the agent.
instructions() returns just { server, instructions }; the guidance layer
fetches tools() separately and does the agent-aware permission correlation,
mirroring how SkillGuidance composes raw data with agent permissions.
This commit is contained in:
parent
baf3f5e5af
commit
d4e00c061d
6 changed files with 95 additions and 3 deletions
|
|
@ -24,6 +24,8 @@ export class ConnectError extends Schema.TaggedErrorClass<ConnectError>()("MCP.C
|
|||
|
||||
/** Handle over a connected MCP server that keeps the SDK `Client` out of the rest of core. */
|
||||
export interface Connection {
|
||||
/** Server-supplied usage instructions from the initialize result, if any. */
|
||||
readonly instructions: string | undefined
|
||||
readonly onClose: (callback: () => void) => void
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ export const connect = Effect.fnUntraced(function* (
|
|||
if (Exit.isSuccess(exit)) {
|
||||
yield* Effect.addFinalizer(() => Effect.promise(() => client.close()).pipe(Effect.ignore))
|
||||
return {
|
||||
instructions: client.getInstructions()?.trim() || undefined,
|
||||
onClose: (callback) => {
|
||||
client.onclose = callback
|
||||
},
|
||||
|
|
|
|||
75
packages/core/src/mcp/guidance.ts
Normal file
75
packages/core/src/mcp/guidance.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
export * as McpGuidance from "./guidance"
|
||||
|
||||
import { makeLocationNode } from "../effect/app-node"
|
||||
import { Context, Effect, Layer, Schema } from "effect"
|
||||
import { AgentV2 } from "../agent"
|
||||
import { PermissionV2 } from "../permission"
|
||||
import { MCP } from "./index"
|
||||
import { SystemContext } from "../system-context/index"
|
||||
|
||||
const Summary = Schema.Struct({
|
||||
server: Schema.String,
|
||||
instructions: Schema.String,
|
||||
})
|
||||
type Summary = typeof Summary.Type
|
||||
|
||||
const render = (servers: ReadonlyArray<Summary>) =>
|
||||
[
|
||||
"<mcp_instructions>",
|
||||
...servers.flatMap((server) => [
|
||||
` <server name="${server.server}">`,
|
||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
||||
" </server>",
|
||||
]),
|
||||
"</mcp_instructions>",
|
||||
].join("\n")
|
||||
|
||||
export interface Interface {
|
||||
readonly load: (agent: AgentV2.Selection) => Effect.Effect<SystemContext.SystemContext>
|
||||
}
|
||||
|
||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/McpGuidance") {}
|
||||
|
||||
export const layer = Layer.effect(
|
||||
Service,
|
||||
Effect.gen(function* () {
|
||||
const mcp = yield* MCP.Service
|
||||
|
||||
return Service.of({
|
||||
load: Effect.fn("McpGuidance.load")(function* (selection) {
|
||||
const agent = selection.info
|
||||
if (!agent) return SystemContext.empty
|
||||
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
|
||||
concurrency: "unbounded",
|
||||
})
|
||||
// Hide a server only when every tool it contributes is wholly denied for this agent.
|
||||
const visible = instructions
|
||||
.filter((item) => {
|
||||
const owned = tools.filter((tool) => tool.server === item.server)
|
||||
return (
|
||||
owned.length === 0 ||
|
||||
owned.some((tool) => PermissionV2.evaluate(tool.name, "*", agent.permissions).effect !== "deny")
|
||||
)
|
||||
})
|
||||
.map((item) => ({ server: item.server, instructions: item.instructions }))
|
||||
if (visible.length === 0) return SystemContext.empty
|
||||
return SystemContext.make({
|
||||
key: SystemContext.Key.make("core/mcp-guidance"),
|
||||
codec: Schema.toCodecJson(Schema.Array(Summary)),
|
||||
load: Effect.succeed(visible),
|
||||
baseline: render,
|
||||
update: (_previous, current) =>
|
||||
[
|
||||
"The available MCP server instructions have changed. This list supersedes the previous one.",
|
||||
render(current),
|
||||
].join("\n"),
|
||||
removed: () => "MCP server instructions are no longer available.",
|
||||
})
|
||||
}),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
export const locationLayer = layer
|
||||
|
||||
export const node = makeLocationNode({ service: Service, layer, deps: [MCP.node] })
|
||||
|
|
@ -55,7 +55,6 @@ export class ServerInfo extends Schema.Class<ServerInfo>("MCP.ServerInfo")({
|
|||
export class ServerInstructions extends Schema.Class<ServerInstructions>("MCP.ServerInstructions")({
|
||||
server: ServerName,
|
||||
instructions: Schema.String,
|
||||
tools: Schema.Array(Schema.String),
|
||||
}) {}
|
||||
|
||||
export class Tool extends Schema.Class<Tool>("MCP.Tool")({
|
||||
|
|
@ -271,7 +270,13 @@ export const layer = Layer.effect(
|
|||
}),
|
||||
instructions: Effect.fn("MCP.instructions")(function* () {
|
||||
yield* whenAllReady
|
||||
return []
|
||||
return Array.from(runtime)
|
||||
.flatMap(([server, entry]) => {
|
||||
const instructions = entry.client?.instructions
|
||||
if (!instructions) return []
|
||||
return [new ServerInstructions({ server, instructions })]
|
||||
})
|
||||
.toSorted((a, b) => a.server.localeCompare(b.server))
|
||||
}),
|
||||
prompts: Effect.fn("MCP.prompts")(function* () {
|
||||
yield* whenAllReady
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { SystemContext } from "../../system-context/index"
|
|||
import { SystemContextRegistry } from "../../system-context/registry"
|
||||
import { SkillGuidance } from "../../skill/guidance"
|
||||
import { ReferenceGuidance } from "../../reference/guidance"
|
||||
import { McpGuidance } from "../../mcp/guidance"
|
||||
import { ToolRegistry } from "../../tool/registry"
|
||||
import { ToolOutputStore } from "../../tool-output-store"
|
||||
import { SessionContextEpoch } from "../context-epoch"
|
||||
|
|
@ -102,6 +103,7 @@ export const layer = Layer.effect(
|
|||
const systemContext = yield* SystemContextRegistry.Service
|
||||
const skillGuidance = yield* SkillGuidance.Service
|
||||
const referenceGuidance = yield* ReferenceGuidance.Service
|
||||
const mcpGuidance = yield* McpGuidance.Service
|
||||
const snapshots = yield* Snapshot.Service
|
||||
const db = (yield* Database.Service).db
|
||||
const compaction = yield* SessionCompaction.Service
|
||||
|
|
@ -160,7 +162,7 @@ export const layer = Layer.effect(
|
|||
new TurnTransitionError({ _tag: "ContinueAfterOverflowCompaction", step })
|
||||
|
||||
const loadSystemContext = (agent: AgentV2.Selection) =>
|
||||
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load()], {
|
||||
Effect.all([systemContext.load(), skillGuidance.load(agent), referenceGuidance.load(), mcpGuidance.load(agent)], {
|
||||
concurrency: "unbounded",
|
||||
}).pipe(Effect.map(SystemContext.combine))
|
||||
|
||||
|
|
@ -422,6 +424,7 @@ export const node = makeLocationNode({
|
|||
SystemContextRegistry.node,
|
||||
SkillGuidance.node,
|
||||
ReferenceGuidance.node,
|
||||
McpGuidance.node,
|
||||
SessionCompaction.node,
|
||||
Snapshot.node,
|
||||
Database.node,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry
|
|||
import { SystemContext } from "@opencode-ai/core/system-context"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
import { describe, expect } from "bun:test"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { Effect, Layer } from "effect"
|
||||
|
|
@ -72,6 +73,7 @@ const systemContext = SystemContextRegistry.layer
|
|||
const location = Location.layer({ directory: AbsolutePath.make("/project") }).pipe(Layer.provide(Project.defaultLayer))
|
||||
const skillGuidance = Layer.mock(SkillGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed([]) }))
|
||||
const runner = SessionRunnerLLM.defaultLayer.pipe(
|
||||
Layer.provide(SessionCompaction.layer),
|
||||
|
|
@ -87,6 +89,7 @@ const runner = SessionRunnerLLM.defaultLayer.pipe(
|
|||
Layer.provide(agents),
|
||||
Layer.provide(skillGuidance),
|
||||
Layer.provide(referenceGuidance),
|
||||
Layer.provide(mcpGuidance),
|
||||
Layer.provide(config),
|
||||
)
|
||||
const execution = Layer.effect(
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import { SystemContext } from "@opencode-ai/core/system-context"
|
|||
import { SystemContextRegistry } from "@opencode-ai/core/system-context/registry"
|
||||
import { SkillGuidance } from "@opencode-ai/core/skill/guidance"
|
||||
import { ReferenceGuidance } from "@opencode-ai/core/reference/guidance"
|
||||
import { McpGuidance } from "@opencode-ai/core/mcp/guidance"
|
||||
import { ModelV2 } from "@opencode-ai/core/model"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||
|
|
@ -217,6 +218,7 @@ const skillGuidance = Layer.mock(SkillGuidance.Service, {
|
|||
),
|
||||
})
|
||||
const referenceGuidance = Layer.mock(ReferenceGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const mcpGuidance = Layer.mock(McpGuidance.Service, { load: () => Effect.succeed(SystemContext.empty) })
|
||||
const config = Layer.succeed(
|
||||
Config.Service,
|
||||
Config.Service.of({
|
||||
|
|
@ -248,6 +250,7 @@ const runner = SessionRunnerLLM.layer.pipe(
|
|||
Layer.provide(agents),
|
||||
Layer.provide(skillGuidance),
|
||||
Layer.provide(referenceGuidance),
|
||||
Layer.provide(mcpGuidance),
|
||||
Layer.provide(config),
|
||||
)
|
||||
const execution = Layer.effect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue