Selaa lähdekoodia

fix(opencode): honor disabled deferred MCP tools

Aiden Cline 1 kuukausi sitten
vanhempi
sitoutus
ca359adafa

+ 14 - 2
packages/opencode/src/session/mcp-tools.ts

@@ -238,7 +238,10 @@ export const resolve = Effect.fn("SessionMcpTools.resolve")(function* (input: In
     Object.keys(mcpTools),
     Permission.merge(input.agent.permission, input.session.permission ?? []),
   )
-  const allowedMcpTools = Object.fromEntries(Object.entries(mcpTools).filter(([key]) => !mcpDisabled.has(key)))
+  const userTools = currentUserToolOverrides(input.messages)
+  const allowedMcpTools = Object.fromEntries(
+    Object.entries(mcpTools).filter(([key]) => userTools?.[key] !== false && !mcpDisabled.has(key)),
+  )
   const deferredDescriptors =
     flags.experimentalToolSearch && Object.keys(allowedMcpTools).length > 0
       ? yield* deferredToolDescriptors(allowedMcpTools)
@@ -277,6 +280,7 @@ export const resolve = Effect.fn("SessionMcpTools.resolve")(function* (input: In
 export const systemPrompt = Effect.fn("SessionMcpTools.systemPrompt")(function* (input: {
   agent: Agent.Info
   session: Session.Info
+  messages: SessionV1.WithParts[]
 }) {
   const mcp = yield* MCP.Service
   const flags = yield* RuntimeFlags.Service
@@ -287,7 +291,10 @@ export const systemPrompt = Effect.fn("SessionMcpTools.systemPrompt")(function*
     Object.keys(mcpTools),
     Permission.merge(input.agent.permission, input.session.permission ?? []),
   )
-  const allowedTools = Object.fromEntries(Object.entries(mcpTools).filter(([key]) => !mcpDisabled.has(key)))
+  const userTools = currentUserToolOverrides(input.messages)
+  const allowedTools = Object.fromEntries(
+    Object.entries(mcpTools).filter(([key]) => userTools?.[key] !== false && !mcpDisabled.has(key)),
+  )
   if (Object.keys(allowedTools).length === 0) return undefined
 
   const deferredDescriptors = yield* deferredToolDescriptors(allowedTools)
@@ -755,6 +762,11 @@ function searchDeferredTools(descriptors: DeferredToolDescriptor[], query: strin
     .map((item) => item.descriptor)
 }
 
+function currentUserToolOverrides(messages: SessionV1.WithParts[]) {
+  const user = messages.findLast((message) => message.info.role === "user")
+  return user?.info.role === "user" ? user.info.tools : undefined
+}
+
 function mcpToolContent(result: unknown): McpToolContent[] {
   if (!isRecord(result) || !Array.isArray(result.content)) return []
   return result.content.flatMap((item): McpToolContent[] => {

+ 1 - 1
packages/opencode/src/session/prompt.ts

@@ -1260,7 +1260,7 @@ export const layer = Layer.effect(
               instruction.system().pipe(Effect.orDie),
               sys.mcp(agent, session.permission),
               tools.search_deferred_tools && tools.call_deferred_tool
-                ? SessionTools.deferredSystemPrompt({ agent, session }).pipe(
+                ? SessionTools.deferredSystemPrompt({ agent, session, messages: msgs }).pipe(
                     Effect.provideService(MCP.Service, mcp),
                     Effect.provideService(RuntimeFlags.Service, flags),
                   )

+ 38 - 3
packages/opencode/test/session/tools.test.ts

@@ -54,6 +54,14 @@ const assistant = {
   time: { created: 0 },
   sessionID,
 } satisfies SessionV1.Assistant
+const user = {
+  id: MessageID.make("msg_user"),
+  sessionID,
+  role: "user",
+  time: { created: 0 },
+  agent: agent.name,
+  model: { providerID: ProviderV2.ID.make(model.providerID), modelID: ModelV2.ID.make(model.id) },
+} satisfies SessionV1.User
 const processor = {
   message: assistant,
   updateToolCall: () => Effect.succeed(undefined),
@@ -124,14 +132,14 @@ const belowThresholdIt = makeIt({
   queryDescription: "Natural language analytics query",
 })
 
-function resolveTools() {
+function resolveTools(messages: SessionV1.WithParts[] = []) {
   return SessionTools.resolve({
     agent,
     model,
     session,
     processor,
     bypassAgentCheck: false,
-    messages: [],
+    messages,
     promptOps,
   })
 }
@@ -172,13 +180,40 @@ describe("session.tools", () => {
 
   deferredIt.instance("lists deferred MCP servers in the system prompt", () =>
     Effect.gen(function* () {
-      const prompt = yield* SessionTools.deferredSystemPrompt({ agent, session })
+      const prompt = yield* SessionTools.deferredSystemPrompt({ agent, session, messages: [] })
 
       expect(prompt).toContain("Deferred MCP servers available through `search_deferred_tools`:")
       expect(prompt).toContain("- posthog: 2 tools")
     }),
   )
 
+  deferredIt.instance("does not expose per-message disabled MCP tools through deferred search", () =>
+    Effect.gen(function* () {
+      const tools = yield* resolveTools([
+        {
+          info: { ...user, tools: { posthog_feature_flags: false } },
+          parts: [],
+        },
+      ])
+
+      const search = tools.search_deferred_tools.execute
+      if (!search) throw new Error("missing search_deferred_tools executor")
+      const searchResult = yield* Effect.promise(() =>
+        Promise.resolve(search({ query: "feature flags" }, toolExecutionOptions)),
+      )
+      const parsed = JSON.parse(searchResult.output) as { tools: Array<{ tool_id: string }> }
+      expect(parsed.tools.map((item) => item.tool_id)).not.toContain("posthog_feature_flags")
+
+      const call = tools.call_deferred_tool.execute
+      if (!call) throw new Error("missing call_deferred_tool executor")
+      yield* Effect.promise(async () => {
+        await expect(
+          Promise.resolve(call({ tool_id: "posthog_feature_flags", arguments: {} }, toolExecutionOptions)),
+        ).rejects.toThrow('Deferred tool "posthog_feature_flags" is not available')
+      })
+    }),
+  )
+
   directIt.instance("keeps MCP tools direct when tool search is disabled", () =>
     Effect.gen(function* () {
       const tools = yield* resolveTools()