lifecycle.test.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. import { test, expect, mock, beforeEach } from "bun:test"
  2. // --- Mock infrastructure ---
  3. // Per-client state for controlling mock behavior
  4. interface MockClientState {
  5. tools: Array<{ name: string; description?: string; inputSchema: object }>
  6. listToolsCalls: number
  7. listToolsShouldFail: boolean
  8. listToolsError: string
  9. listPromptsShouldFail: boolean
  10. listResourcesShouldFail: boolean
  11. prompts: Array<{ name: string; description?: string }>
  12. resources: Array<{ name: string; uri: string; description?: string }>
  13. closed: boolean
  14. notificationHandlers: Map<unknown, (...args: any[]) => any>
  15. }
  16. const clientStates = new Map<string, MockClientState>()
  17. let lastCreatedClientName: string | undefined
  18. let connectShouldFail = false
  19. let connectShouldHang = false
  20. let connectError = "Mock transport cannot connect"
  21. // Tracks how many Client instances were created (detects leaks)
  22. let clientCreateCount = 0
  23. // Tracks how many times transport.close() is called across all mock transports
  24. let transportCloseCount = 0
  25. function getOrCreateClientState(name?: string): MockClientState {
  26. const key = name ?? "default"
  27. let state = clientStates.get(key)
  28. if (!state) {
  29. state = {
  30. tools: [{ name: "test_tool", description: "A test tool", inputSchema: { type: "object", properties: {} } }],
  31. listToolsCalls: 0,
  32. listToolsShouldFail: false,
  33. listToolsError: "listTools failed",
  34. listPromptsShouldFail: false,
  35. listResourcesShouldFail: false,
  36. prompts: [],
  37. resources: [],
  38. closed: false,
  39. notificationHandlers: new Map(),
  40. }
  41. clientStates.set(key, state)
  42. }
  43. return state
  44. }
  45. // Mock transport that succeeds or fails based on connectShouldFail / connectShouldHang
  46. class MockStdioTransport {
  47. stderr: null = null
  48. pid = 12345
  49. constructor(_opts: any) {}
  50. async start() {
  51. if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
  52. if (connectShouldFail) throw new Error(connectError)
  53. }
  54. async close() {
  55. transportCloseCount++
  56. }
  57. }
  58. class MockStreamableHTTP {
  59. constructor(_url: URL, _opts?: any) {}
  60. async start() {
  61. if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
  62. if (connectShouldFail) throw new Error(connectError)
  63. }
  64. async close() {
  65. transportCloseCount++
  66. }
  67. async finishAuth() {}
  68. }
  69. class MockSSE {
  70. constructor(_url: URL, _opts?: any) {}
  71. async start() {
  72. if (connectShouldHang) return new Promise<void>(() => {}) // never resolves
  73. if (connectShouldFail) throw new Error(connectError)
  74. }
  75. async close() {
  76. transportCloseCount++
  77. }
  78. }
  79. mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
  80. StdioClientTransport: MockStdioTransport,
  81. }))
  82. mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
  83. StreamableHTTPClientTransport: MockStreamableHTTP,
  84. }))
  85. mock.module("@modelcontextprotocol/sdk/client/sse.js", () => ({
  86. SSEClientTransport: MockSSE,
  87. }))
  88. mock.module("@modelcontextprotocol/sdk/client/auth.js", () => ({
  89. UnauthorizedError: class extends Error {
  90. constructor() {
  91. super("Unauthorized")
  92. }
  93. },
  94. }))
  95. // Mock Client that delegates to per-name MockClientState
  96. mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
  97. Client: class MockClient {
  98. _state!: MockClientState
  99. transport: any
  100. constructor(_opts: any) {
  101. clientCreateCount++
  102. }
  103. async connect(transport: { start: () => Promise<void> }) {
  104. this.transport = transport
  105. await transport.start()
  106. // After successful connect, bind to the last-created client name
  107. this._state = getOrCreateClientState(lastCreatedClientName)
  108. }
  109. setNotificationHandler(schema: unknown, handler: (...args: any[]) => any) {
  110. this._state?.notificationHandlers.set(schema, handler)
  111. }
  112. async listTools() {
  113. if (this._state) this._state.listToolsCalls++
  114. if (this._state?.listToolsShouldFail) {
  115. throw new Error(this._state.listToolsError)
  116. }
  117. return { tools: this._state?.tools ?? [] }
  118. }
  119. async listPrompts() {
  120. if (this._state?.listPromptsShouldFail) {
  121. throw new Error("listPrompts failed")
  122. }
  123. return { prompts: this._state?.prompts ?? [] }
  124. }
  125. async listResources() {
  126. if (this._state?.listResourcesShouldFail) {
  127. throw new Error("listResources failed")
  128. }
  129. return { resources: this._state?.resources ?? [] }
  130. }
  131. async close() {
  132. if (this._state) this._state.closed = true
  133. }
  134. },
  135. }))
  136. beforeEach(() => {
  137. clientStates.clear()
  138. lastCreatedClientName = undefined
  139. connectShouldFail = false
  140. connectShouldHang = false
  141. connectError = "Mock transport cannot connect"
  142. clientCreateCount = 0
  143. transportCloseCount = 0
  144. })
  145. // Import after mocks
  146. const { MCP } = await import("../../src/mcp/index")
  147. const { Instance } = await import("../../src/project/instance")
  148. const { tmpdir } = await import("../fixture/fixture")
  149. // --- Helper ---
  150. function withInstance(config: Record<string, any>, fn: () => Promise<void>) {
  151. return async () => {
  152. await using tmp = await tmpdir({
  153. init: async (dir) => {
  154. await Bun.write(
  155. `${dir}/opencode.json`,
  156. JSON.stringify({
  157. $schema: "https://opencode.ai/config.json",
  158. mcp: config,
  159. }),
  160. )
  161. },
  162. })
  163. await Instance.provide({
  164. directory: tmp.path,
  165. fn: async () => {
  166. await fn()
  167. // dispose instance to clean up state between tests
  168. await Instance.dispose()
  169. },
  170. })
  171. }
  172. }
  173. // ========================================================================
  174. // Test: tools() are cached after connect
  175. // ========================================================================
  176. test(
  177. "tools() reuses cached tool definitions after connect",
  178. withInstance({}, async () => {
  179. lastCreatedClientName = "my-server"
  180. const serverState = getOrCreateClientState("my-server")
  181. serverState.tools = [
  182. { name: "do_thing", description: "does a thing", inputSchema: { type: "object", properties: {} } },
  183. ]
  184. // First: add the server successfully
  185. const addResult = await MCP.add("my-server", {
  186. type: "local",
  187. command: ["echo", "test"],
  188. })
  189. expect((addResult.status as any)["my-server"]?.status ?? (addResult.status as any).status).toBe("connected")
  190. expect(serverState.listToolsCalls).toBe(1)
  191. const toolsA = await MCP.tools()
  192. const toolsB = await MCP.tools()
  193. expect(Object.keys(toolsA).length).toBeGreaterThan(0)
  194. expect(Object.keys(toolsB).length).toBeGreaterThan(0)
  195. expect(serverState.listToolsCalls).toBe(1)
  196. }),
  197. )
  198. // ========================================================================
  199. // Test: tool change notifications refresh the cache
  200. // ========================================================================
  201. test(
  202. "tool change notifications refresh cached tool definitions",
  203. withInstance({}, async () => {
  204. lastCreatedClientName = "status-server"
  205. const serverState = getOrCreateClientState("status-server")
  206. await MCP.add("status-server", {
  207. type: "local",
  208. command: ["echo", "test"],
  209. })
  210. const before = await MCP.tools()
  211. expect(Object.keys(before).some((key) => key.includes("test_tool"))).toBe(true)
  212. expect(serverState.listToolsCalls).toBe(1)
  213. serverState.tools = [{ name: "next_tool", description: "next", inputSchema: { type: "object", properties: {} } }]
  214. const handler = Array.from(serverState.notificationHandlers.values())[0]
  215. expect(handler).toBeDefined()
  216. await handler?.()
  217. const after = await MCP.tools()
  218. expect(Object.keys(after).some((key) => key.includes("next_tool"))).toBe(true)
  219. expect(Object.keys(after).some((key) => key.includes("test_tool"))).toBe(false)
  220. expect(serverState.listToolsCalls).toBe(2)
  221. }),
  222. )
  223. // ========================================================================
  224. // Test: connect() / disconnect() lifecycle
  225. // ========================================================================
  226. test(
  227. "disconnect sets status to disabled and removes client",
  228. withInstance(
  229. {
  230. "disc-server": {
  231. type: "local",
  232. command: ["echo", "test"],
  233. },
  234. },
  235. async () => {
  236. lastCreatedClientName = "disc-server"
  237. getOrCreateClientState("disc-server")
  238. await MCP.add("disc-server", {
  239. type: "local",
  240. command: ["echo", "test"],
  241. })
  242. const statusBefore = await MCP.status()
  243. expect(statusBefore["disc-server"]?.status).toBe("connected")
  244. await MCP.disconnect("disc-server")
  245. const statusAfter = await MCP.status()
  246. expect(statusAfter["disc-server"]?.status).toBe("disabled")
  247. // Tools should be empty after disconnect
  248. const tools = await MCP.tools()
  249. const serverTools = Object.keys(tools).filter((k) => k.startsWith("disc-server"))
  250. expect(serverTools.length).toBe(0)
  251. },
  252. ),
  253. )
  254. test(
  255. "connect() after disconnect() re-establishes the server",
  256. withInstance(
  257. {
  258. "reconn-server": {
  259. type: "local",
  260. command: ["echo", "test"],
  261. },
  262. },
  263. async () => {
  264. lastCreatedClientName = "reconn-server"
  265. const serverState = getOrCreateClientState("reconn-server")
  266. serverState.tools = [{ name: "my_tool", description: "a tool", inputSchema: { type: "object", properties: {} } }]
  267. await MCP.add("reconn-server", {
  268. type: "local",
  269. command: ["echo", "test"],
  270. })
  271. await MCP.disconnect("reconn-server")
  272. expect((await MCP.status())["reconn-server"]?.status).toBe("disabled")
  273. // Reconnect
  274. await MCP.connect("reconn-server")
  275. expect((await MCP.status())["reconn-server"]?.status).toBe("connected")
  276. const tools = await MCP.tools()
  277. expect(Object.keys(tools).some((k) => k.includes("my_tool"))).toBe(true)
  278. },
  279. ),
  280. )
  281. // ========================================================================
  282. // Test: add() closes existing client before replacing
  283. // ========================================================================
  284. test(
  285. "add() closes the old client when replacing a server",
  286. // Don't put the server in config — add it dynamically so we control
  287. // exactly which client instance is "first" vs "second".
  288. withInstance({}, async () => {
  289. lastCreatedClientName = "replace-server"
  290. const firstState = getOrCreateClientState("replace-server")
  291. await MCP.add("replace-server", {
  292. type: "local",
  293. command: ["echo", "test"],
  294. })
  295. expect(firstState.closed).toBe(false)
  296. // Create new state for second client
  297. clientStates.delete("replace-server")
  298. const secondState = getOrCreateClientState("replace-server")
  299. // Re-add should close the first client
  300. await MCP.add("replace-server", {
  301. type: "local",
  302. command: ["echo", "test"],
  303. })
  304. expect(firstState.closed).toBe(true)
  305. expect(secondState.closed).toBe(false)
  306. }),
  307. )
  308. // ========================================================================
  309. // Test: state init with mixed success/failure
  310. // ========================================================================
  311. test(
  312. "init connects available servers even when one fails",
  313. withInstance(
  314. {
  315. "good-server": {
  316. type: "local",
  317. command: ["echo", "good"],
  318. },
  319. "bad-server": {
  320. type: "local",
  321. command: ["echo", "bad"],
  322. },
  323. },
  324. async () => {
  325. // Set up good server
  326. const goodState = getOrCreateClientState("good-server")
  327. goodState.tools = [{ name: "good_tool", description: "works", inputSchema: { type: "object", properties: {} } }]
  328. // Set up bad server - will fail on listTools during create()
  329. const badState = getOrCreateClientState("bad-server")
  330. badState.listToolsShouldFail = true
  331. // Add good server first
  332. lastCreatedClientName = "good-server"
  333. await MCP.add("good-server", {
  334. type: "local",
  335. command: ["echo", "good"],
  336. })
  337. // Add bad server - should fail but not affect good server
  338. lastCreatedClientName = "bad-server"
  339. await MCP.add("bad-server", {
  340. type: "local",
  341. command: ["echo", "bad"],
  342. })
  343. const status = await MCP.status()
  344. expect(status["good-server"]?.status).toBe("connected")
  345. expect(status["bad-server"]?.status).toBe("failed")
  346. // Good server's tools should still be available
  347. const tools = await MCP.tools()
  348. expect(Object.keys(tools).some((k) => k.includes("good_tool"))).toBe(true)
  349. },
  350. ),
  351. )
  352. // ========================================================================
  353. // Test: disabled server via config
  354. // ========================================================================
  355. test(
  356. "disabled server is marked as disabled without attempting connection",
  357. withInstance(
  358. {
  359. "disabled-server": {
  360. type: "local",
  361. command: ["echo", "test"],
  362. enabled: false,
  363. },
  364. },
  365. async () => {
  366. const countBefore = clientCreateCount
  367. await MCP.add("disabled-server", {
  368. type: "local",
  369. command: ["echo", "test"],
  370. enabled: false,
  371. } as any)
  372. // No client should have been created
  373. expect(clientCreateCount).toBe(countBefore)
  374. const status = await MCP.status()
  375. expect(status["disabled-server"]?.status).toBe("disabled")
  376. },
  377. ),
  378. )
  379. // ========================================================================
  380. // Test: prompts() and resources()
  381. // ========================================================================
  382. test(
  383. "prompts() returns prompts from connected servers",
  384. withInstance(
  385. {
  386. "prompt-server": {
  387. type: "local",
  388. command: ["echo", "test"],
  389. },
  390. },
  391. async () => {
  392. lastCreatedClientName = "prompt-server"
  393. const serverState = getOrCreateClientState("prompt-server")
  394. serverState.prompts = [{ name: "my-prompt", description: "A test prompt" }]
  395. await MCP.add("prompt-server", {
  396. type: "local",
  397. command: ["echo", "test"],
  398. })
  399. const prompts = await MCP.prompts()
  400. expect(Object.keys(prompts).length).toBe(1)
  401. const key = Object.keys(prompts)[0]
  402. expect(key).toContain("prompt-server")
  403. expect(key).toContain("my-prompt")
  404. },
  405. ),
  406. )
  407. test(
  408. "resources() returns resources from connected servers",
  409. withInstance(
  410. {
  411. "resource-server": {
  412. type: "local",
  413. command: ["echo", "test"],
  414. },
  415. },
  416. async () => {
  417. lastCreatedClientName = "resource-server"
  418. const serverState = getOrCreateClientState("resource-server")
  419. serverState.resources = [{ name: "my-resource", uri: "file:///test.txt", description: "A test resource" }]
  420. await MCP.add("resource-server", {
  421. type: "local",
  422. command: ["echo", "test"],
  423. })
  424. const resources = await MCP.resources()
  425. expect(Object.keys(resources).length).toBe(1)
  426. const key = Object.keys(resources)[0]
  427. expect(key).toContain("resource-server")
  428. expect(key).toContain("my-resource")
  429. },
  430. ),
  431. )
  432. test(
  433. "prompts() skips disconnected servers",
  434. withInstance(
  435. {
  436. "prompt-disc-server": {
  437. type: "local",
  438. command: ["echo", "test"],
  439. },
  440. },
  441. async () => {
  442. lastCreatedClientName = "prompt-disc-server"
  443. const serverState = getOrCreateClientState("prompt-disc-server")
  444. serverState.prompts = [{ name: "hidden-prompt", description: "Should not appear" }]
  445. await MCP.add("prompt-disc-server", {
  446. type: "local",
  447. command: ["echo", "test"],
  448. })
  449. await MCP.disconnect("prompt-disc-server")
  450. const prompts = await MCP.prompts()
  451. expect(Object.keys(prompts).length).toBe(0)
  452. },
  453. ),
  454. )
  455. // ========================================================================
  456. // Test: connect() on nonexistent server
  457. // ========================================================================
  458. test(
  459. "connect() on nonexistent server does not throw",
  460. withInstance({}, async () => {
  461. // Should not throw
  462. await MCP.connect("nonexistent")
  463. const status = await MCP.status()
  464. expect(status["nonexistent"]).toBeUndefined()
  465. }),
  466. )
  467. // ========================================================================
  468. // Test: disconnect() on nonexistent server
  469. // ========================================================================
  470. test(
  471. "disconnect() on nonexistent server does not throw",
  472. withInstance({}, async () => {
  473. await MCP.disconnect("nonexistent")
  474. // Should complete without error
  475. }),
  476. )
  477. // ========================================================================
  478. // Test: tools() with no MCP servers configured
  479. // ========================================================================
  480. test(
  481. "tools() returns empty when no MCP servers are configured",
  482. withInstance({}, async () => {
  483. const tools = await MCP.tools()
  484. expect(Object.keys(tools).length).toBe(0)
  485. }),
  486. )
  487. // ========================================================================
  488. // Test: connect failure during create()
  489. // ========================================================================
  490. test(
  491. "server that fails to connect is marked as failed",
  492. withInstance(
  493. {
  494. "fail-connect": {
  495. type: "local",
  496. command: ["echo", "test"],
  497. },
  498. },
  499. async () => {
  500. lastCreatedClientName = "fail-connect"
  501. getOrCreateClientState("fail-connect")
  502. connectShouldFail = true
  503. connectError = "Connection refused"
  504. await MCP.add("fail-connect", {
  505. type: "local",
  506. command: ["echo", "test"],
  507. })
  508. const status = await MCP.status()
  509. expect(status["fail-connect"]?.status).toBe("failed")
  510. if (status["fail-connect"]?.status === "failed") {
  511. expect(status["fail-connect"].error).toContain("Connection refused")
  512. }
  513. // No tools should be available
  514. const tools = await MCP.tools()
  515. expect(Object.keys(tools).length).toBe(0)
  516. },
  517. ),
  518. )
  519. // ========================================================================
  520. // Bug #5: McpOAuthCallback.cancelPending uses wrong key
  521. // ========================================================================
  522. test("McpOAuthCallback.cancelPending is keyed by mcpName but pendingAuths uses oauthState", async () => {
  523. const { McpOAuthCallback } = await import("../../src/mcp/oauth-callback")
  524. // Register a pending auth with an oauthState key, associated to an mcpName
  525. const oauthState = "abc123hexstate"
  526. const callbackPromise = McpOAuthCallback.waitForCallback(oauthState, "my-mcp-server")
  527. // cancelPending is called with mcpName — should find the entry via reverse index
  528. McpOAuthCallback.cancelPending("my-mcp-server")
  529. // The callback should still be pending because cancelPending looked up
  530. // "my-mcp-server" in a map keyed by "abc123hexstate"
  531. let resolved = false
  532. let rejected = false
  533. callbackPromise.then(() => (resolved = true)).catch(() => (rejected = true))
  534. // Give it a tick
  535. await new Promise((r) => setTimeout(r, 50))
  536. // cancelPending("my-mcp-server") should have rejected the pending callback
  537. expect(rejected).toBe(true)
  538. await McpOAuthCallback.stop()
  539. })
  540. // ========================================================================
  541. // Test: multiple tools from same server get correct name prefixes
  542. // ========================================================================
  543. test(
  544. "tools() prefixes tool names with sanitized server name",
  545. withInstance(
  546. {
  547. "my.special-server": {
  548. type: "local",
  549. command: ["echo", "test"],
  550. },
  551. },
  552. async () => {
  553. lastCreatedClientName = "my.special-server"
  554. const serverState = getOrCreateClientState("my.special-server")
  555. serverState.tools = [
  556. { name: "tool-a", description: "Tool A", inputSchema: { type: "object", properties: {} } },
  557. { name: "tool.b", description: "Tool B", inputSchema: { type: "object", properties: {} } },
  558. ]
  559. await MCP.add("my.special-server", {
  560. type: "local",
  561. command: ["echo", "test"],
  562. })
  563. const tools = await MCP.tools()
  564. const keys = Object.keys(tools)
  565. // Server name dots should be replaced with underscores
  566. expect(keys.some((k) => k.startsWith("my_special-server_"))).toBe(true)
  567. // Tool name dots should be replaced with underscores
  568. expect(keys.some((k) => k.endsWith("tool_b"))).toBe(true)
  569. expect(keys.length).toBe(2)
  570. },
  571. ),
  572. )
  573. // ========================================================================
  574. // Test: transport leak — local stdio timeout (#19168)
  575. // ========================================================================
  576. test(
  577. "local stdio transport is closed when connect times out (no process leak)",
  578. withInstance({}, async () => {
  579. lastCreatedClientName = "hanging-server"
  580. getOrCreateClientState("hanging-server")
  581. connectShouldHang = true
  582. const addResult = await MCP.add("hanging-server", {
  583. type: "local",
  584. command: ["node", "fake.js"],
  585. timeout: 100,
  586. })
  587. const serverStatus = (addResult.status as any)["hanging-server"] ?? addResult.status
  588. expect(serverStatus.status).toBe("failed")
  589. expect(serverStatus.error).toContain("timed out")
  590. // Transport must be closed to avoid orphaned child process
  591. expect(transportCloseCount).toBeGreaterThanOrEqual(1)
  592. }),
  593. )
  594. // ========================================================================
  595. // Test: transport leak — remote timeout (#19168)
  596. // ========================================================================
  597. test(
  598. "remote transport is closed when connect times out",
  599. withInstance({}, async () => {
  600. lastCreatedClientName = "hanging-remote"
  601. getOrCreateClientState("hanging-remote")
  602. connectShouldHang = true
  603. const addResult = await MCP.add("hanging-remote", {
  604. type: "remote",
  605. url: "http://localhost:9999/mcp",
  606. timeout: 100,
  607. oauth: false,
  608. })
  609. const serverStatus = (addResult.status as any)["hanging-remote"] ?? addResult.status
  610. expect(serverStatus.status).toBe("failed")
  611. // Transport must be closed to avoid leaked HTTP connections
  612. expect(transportCloseCount).toBeGreaterThanOrEqual(1)
  613. }),
  614. )
  615. // ========================================================================
  616. // Test: transport leak — failed remote transports not closed (#19168)
  617. // ========================================================================
  618. test(
  619. "failed remote transport is closed before trying next transport",
  620. withInstance({}, async () => {
  621. lastCreatedClientName = "fail-remote"
  622. getOrCreateClientState("fail-remote")
  623. connectShouldFail = true
  624. connectError = "Connection refused"
  625. const addResult = await MCP.add("fail-remote", {
  626. type: "remote",
  627. url: "http://localhost:9999/mcp",
  628. timeout: 5000,
  629. oauth: false,
  630. })
  631. const serverStatus = (addResult.status as any)["fail-remote"] ?? addResult.status
  632. expect(serverStatus.status).toBe("failed")
  633. // Both StreamableHTTP and SSE transports should be closed
  634. expect(transportCloseCount).toBeGreaterThanOrEqual(2)
  635. }),
  636. )