2
0

headers.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. import { test, expect, mock, beforeEach } from "bun:test"
  2. // Track what options were passed to each transport constructor
  3. const transportCalls: Array<{
  4. type: "streamable" | "sse"
  5. url: string
  6. options: { authProvider?: unknown; requestInit?: RequestInit }
  7. }> = []
  8. // Mock the transport constructors to capture their arguments
  9. mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
  10. StreamableHTTPClientTransport: class MockStreamableHTTP {
  11. constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
  12. transportCalls.push({
  13. type: "streamable",
  14. url: url.toString(),
  15. options: options ?? {},
  16. })
  17. }
  18. async start() {
  19. throw new Error("Mock transport cannot connect")
  20. }
  21. },
  22. }))
  23. mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
  24. SSEClientTransport: class MockSSE {
  25. constructor(url: URL, options?: { authProvider?: unknown; requestInit?: RequestInit }) {
  26. transportCalls.push({
  27. type: "sse",
  28. url: url.toString(),
  29. options: options ?? {},
  30. })
  31. }
  32. async start() {
  33. throw new Error("Mock transport cannot connect")
  34. }
  35. },
  36. }))
  37. beforeEach(() => {
  38. transportCalls.length = 0
  39. })
  40. // Import MCP after mocking
  41. const { MCP } = await import("../../src/mcp/index")
  42. const { Instance } = await import("../../src/project/instance")
  43. const { tmpdir } = await import("../fixture/fixture")
  44. test("headers are passed to transports when oauth is enabled (default)", async () => {
  45. await using tmp = await tmpdir({
  46. init: async (dir) => {
  47. await Bun.write(
  48. `${dir}/opencode.json`,
  49. JSON.stringify({
  50. $schema: "https://opencode.ai/config.json",
  51. mcp: {
  52. "test-server": {
  53. type: "remote",
  54. url: "https://example.com/mcp",
  55. headers: {
  56. Authorization: "Bearer test-token",
  57. "X-Custom-Header": "custom-value",
  58. },
  59. },
  60. },
  61. }),
  62. )
  63. },
  64. })
  65. await Instance.provide({
  66. directory: tmp.path,
  67. fn: async () => {
  68. // Trigger MCP initialization - it will fail to connect but we can check the transport options
  69. await MCP.add("test-server", {
  70. type: "remote",
  71. url: "https://example.com/mcp",
  72. headers: {
  73. Authorization: "Bearer test-token",
  74. "X-Custom-Header": "custom-value",
  75. },
  76. }).catch(() => {})
  77. // Both transports should have been created with headers
  78. expect(transportCalls.length).toBeGreaterThanOrEqual(1)
  79. for (const call of transportCalls) {
  80. expect(call.options.requestInit).toBeDefined()
  81. expect(call.options.requestInit?.headers).toEqual({
  82. Authorization: "Bearer test-token",
  83. "X-Custom-Header": "custom-value",
  84. })
  85. // OAuth should be enabled by default, so authProvider should exist
  86. expect(call.options.authProvider).toBeDefined()
  87. }
  88. },
  89. })
  90. })
  91. test("headers are passed to transports when oauth is explicitly disabled", async () => {
  92. await using tmp = await tmpdir()
  93. await Instance.provide({
  94. directory: tmp.path,
  95. fn: async () => {
  96. transportCalls.length = 0
  97. await MCP.add("test-server-no-oauth", {
  98. type: "remote",
  99. url: "https://example.com/mcp",
  100. oauth: false,
  101. headers: {
  102. Authorization: "Bearer test-token",
  103. },
  104. }).catch(() => {})
  105. expect(transportCalls.length).toBeGreaterThanOrEqual(1)
  106. for (const call of transportCalls) {
  107. expect(call.options.requestInit).toBeDefined()
  108. expect(call.options.requestInit?.headers).toEqual({
  109. Authorization: "Bearer test-token",
  110. })
  111. // OAuth is disabled, so no authProvider
  112. expect(call.options.authProvider).toBeUndefined()
  113. }
  114. },
  115. })
  116. })
  117. test("no requestInit when headers are not provided", async () => {
  118. await using tmp = await tmpdir()
  119. await Instance.provide({
  120. directory: tmp.path,
  121. fn: async () => {
  122. transportCalls.length = 0
  123. await MCP.add("test-server-no-headers", {
  124. type: "remote",
  125. url: "https://example.com/mcp",
  126. }).catch(() => {})
  127. expect(transportCalls.length).toBeGreaterThanOrEqual(1)
  128. for (const call of transportCalls) {
  129. // No headers means requestInit should be undefined
  130. expect(call.options.requestInit).toBeUndefined()
  131. }
  132. },
  133. })
  134. })