mcp-instructions.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import { describe, expect } from "bun:test"
  2. import { Effect, Layer } from "effect"
  3. import { AgentV2 } from "@opencode-ai/core/agent"
  4. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  5. import { MCP } from "@opencode-ai/core/mcp/index"
  6. import { McpInstructions } from "@opencode-ai/core/mcp/instructions"
  7. import { PermissionV2 } from "@opencode-ai/core/permission"
  8. import { McpTool } from "@opencode-ai/core/tool/mcp"
  9. import { it } from "./lib/effect"
  10. import { readInitial, readUpdate } from "./lib/instructions"
  11. const build = AgentV2.ID.make("build")
  12. const selection = (permissions: PermissionV2.Ruleset = []) => {
  13. const info = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions })
  14. return { id: info.id, info }
  15. }
  16. const instructions = (server: string, text: string) =>
  17. new MCP.ServerInstructions({ server: MCP.ServerName.make(server), instructions: text })
  18. const tool = (server: string, name = "search") => new MCP.Tool({ server: MCP.ServerName.make(server), name })
  19. const layer = (catalog: () => MCP.ServerInstructions[], tools: () => MCP.Tool[]) =>
  20. AppNodeBuilder.build(McpInstructions.node, [
  21. [
  22. MCP.node,
  23. Layer.mock(MCP.Service, {
  24. instructions: () => Effect.succeed(catalog()),
  25. tools: () => Effect.succeed(tools()),
  26. }),
  27. ],
  28. ])
  29. describe("McpInstructions", () => {
  30. it.effect("renders instructions for servers with at least one permitted tool", () =>
  31. Effect.gen(function* () {
  32. const service = yield* McpInstructions.Service
  33. const generation = yield* service
  34. .load(
  35. selection([
  36. { action: McpTool.name("alpha", "restricted"), resource: "*", effect: "deny" },
  37. { action: McpTool.name("hidden", "search"), resource: "*", effect: "deny" },
  38. ]),
  39. )
  40. .pipe(Effect.flatMap(readInitial))
  41. expect(generation.text).toBe(
  42. [
  43. "<mcp_instructions>",
  44. ' <server name="alpha">',
  45. ' Use tools from this server through `execute` under `tools["alpha"]`.',
  46. " Alpha line one",
  47. " Alpha line two",
  48. " </server>",
  49. ' <server name="beta">',
  50. ' Use tools from this server through `execute` under `tools["beta"]`.',
  51. " Beta instructions",
  52. " </server>",
  53. "</mcp_instructions>",
  54. ].join("\n"),
  55. )
  56. }).pipe(
  57. Effect.provide(
  58. layer(
  59. () => [
  60. instructions("beta", "Beta instructions"),
  61. instructions("unused", "No tools"),
  62. instructions("hidden", "Denied tool"),
  63. instructions("alpha", "Alpha line one\nAlpha line two"),
  64. ],
  65. () => [tool("alpha"), tool("alpha", "restricted"), tool("beta"), tool("hidden")],
  66. ),
  67. ),
  68. ),
  69. )
  70. it.effect("omits instructions when the agent cannot use execute", () =>
  71. Effect.gen(function* () {
  72. const service = yield* McpInstructions.Service
  73. const generation = yield* service
  74. .load(selection([{ action: "execute", resource: "*", effect: "deny" }]))
  75. .pipe(Effect.flatMap(readInitial))
  76. expect(generation.text).toBe("")
  77. }).pipe(
  78. Effect.provide(
  79. layer(
  80. () => [instructions("alpha", "Alpha instructions")],
  81. () => [tool("alpha")],
  82. ),
  83. ),
  84. ),
  85. )
  86. it.effect("renders additions, changes, and removal", () => {
  87. let catalog = [instructions("alpha", "Alpha instructions")]
  88. const tools = [tool("alpha"), tool("beta")]
  89. return Effect.gen(function* () {
  90. const service = yield* McpInstructions.Service
  91. const initialized = yield* service.load(selection()).pipe(Effect.flatMap(readInitial))
  92. catalog = [instructions("alpha", "Alpha instructions"), instructions("beta", "Beta instructions")]
  93. const added = yield* readUpdate(yield* service.load(selection()), initialized)
  94. expect(added.text).toBe(
  95. [
  96. "New MCP server instructions are available in addition to those previously listed:",
  97. ' <server name="beta">',
  98. ' Use tools from this server through `execute` under `tools["beta"]`.',
  99. " Beta instructions",
  100. " </server>",
  101. ].join("\n"),
  102. )
  103. catalog = [instructions("alpha", "Updated alpha"), instructions("beta", "Beta instructions")]
  104. const changed = yield* readUpdate(yield* service.load(selection()), added)
  105. expect(changed.text).toBe(
  106. [
  107. "The available MCP server instructions have changed. This list supersedes the previous one.",
  108. "<mcp_instructions>",
  109. ' <server name="alpha">',
  110. ' Use tools from this server through `execute` under `tools["alpha"]`.',
  111. " Updated alpha",
  112. " </server>",
  113. ' <server name="beta">',
  114. ' Use tools from this server through `execute` under `tools["beta"]`.',
  115. " Beta instructions",
  116. " </server>",
  117. "</mcp_instructions>",
  118. ].join("\n"),
  119. )
  120. catalog = [instructions("beta", "Beta instructions")]
  121. const removed = yield* readUpdate(yield* service.load(selection()), changed)
  122. expect(removed.text).toBe("Instructions for the following MCP servers are no longer available: alpha.")
  123. catalog = []
  124. expect((yield* readUpdate(yield* service.load(selection()), removed)).text).toBe(
  125. "MCP server instructions are no longer available.",
  126. )
  127. }).pipe(
  128. Effect.provide(
  129. layer(
  130. () => catalog,
  131. () => tools,
  132. ),
  133. ),
  134. )
  135. })
  136. })