feat(skill-mcp-manager): improve connection handling and client implementations

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-10 15:53:02 +09:00
parent 046ab1060d
commit e9b3ef0328
8 changed files with 259 additions and 94 deletions
@@ -1,6 +1,10 @@
import { afterAll, afterEach, beforeEach, describe, expect, it, mock, test } from "bun:test"
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"
import type { StdioServerParameters } from "@modelcontextprotocol/sdk/client/stdio.js"
import type { ClaudeCodeMcpServer } from "../claude-code-mcp-loader/types"
import type { SkillMcpClientInfo, SkillMcpManagerState } from "./types"
import { setHttpClientDependenciesForTesting } from "./http-client"
import { setStdioClientDependenciesForTesting } from "./stdio-client"
const trackedStates: SkillMcpManagerState[] = []
const createdStdioTransports: MockStdioClientTransport[] = []
@@ -8,33 +12,62 @@ const createdHttpTransports: MockStreamableHTTPClientTransport[] = []
class MockClient {
readonly close = mock(async () => {})
readonly listTools = mock(async () => ({ tools: [] }))
readonly listResources = mock(async () => ({ resources: [] }))
readonly listPrompts = mock(async () => ({ prompts: [] }))
readonly callTool = mock(async () => ({ content: [] }))
readonly readResource = mock(async () => ({ contents: [] }))
readonly getPrompt = mock(async () => ({ messages: [] }))
constructor(
_clientInfo: { name: string; version: string },
_options: { capabilities: Record<string, never> }
) {}
async connect(_transport: unknown): Promise<void> {
async connect(_transport: Transport): Promise<void> {
// Successful connect, env-related assertions happen on transport constructor args
}
}
class MockStdioClientTransport {
readonly close = mock(async () => {})
readonly options: { command: string; args?: string[]; env?: Record<string, string>; stderr?: string }
readonly start = mock(async () => {})
readonly send = mock(async () => {})
readonly options: StdioServerParameters
constructor(options: { command: string; args?: string[]; env?: Record<string, string>; stderr?: string }) {
constructor(options: StdioServerParameters) {
this.options = options
createdStdioTransports.push(this)
}
}
interface MockHttpTransportOptions {
requestInit?: { headers?: Record<string, string> }
requestInit?: RequestInit
}
function getHeaderValue(
headers: HeadersInit | undefined,
name: string,
): string | undefined {
if (!headers) {
return undefined
}
if (headers instanceof Headers) {
return headers.get(name) ?? undefined
}
if (Array.isArray(headers)) {
const entry = headers.find(([headerName]) => headerName.toLowerCase() === name.toLowerCase())
return entry?.[1]
}
return headers[name]
}
class MockStreamableHTTPClientTransport {
readonly close = mock(async () => {})
readonly send = mock(async () => {})
readonly url: URL
readonly options?: MockHttpTransportOptions
@@ -47,18 +80,6 @@ class MockStreamableHTTPClientTransport {
async start() {}
}
mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({
Client: MockClient,
}))
mock.module("@modelcontextprotocol/sdk/client/stdio.js", () => ({
StdioClientTransport: MockStdioClientTransport,
}))
mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({
StreamableHTTPClientTransport: MockStreamableHTTPClientTransport,
}))
afterAll(() => {
mock.restore()
})
@@ -110,6 +131,14 @@ const ORIGINAL_ENV = { ...process.env }
beforeEach(() => {
createdStdioTransports.length = 0
createdHttpTransports.length = 0
setStdioClientDependenciesForTesting({
createClient: (clientInfo, options) => new MockClient(clientInfo, options),
createTransport: (options) => new MockStdioClientTransport(options),
})
setHttpClientDependenciesForTesting({
createClient: (clientInfo, options) => new MockClient(clientInfo, options),
createTransport: (url, options) => new MockStreamableHTTPClientTransport(url, options),
})
})
afterEach(async () => {
@@ -126,6 +155,9 @@ afterEach(async () => {
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
process.env[key] = value
}
setStdioClientDependenciesForTesting()
setHttpClientDependenciesForTesting()
})
describe("getOrCreateClient env var expansion", () => {
@@ -265,11 +297,11 @@ describe("getOrCreateClient env var expansion", () => {
// when
await getOrCreateClient({ state, clientKey, info, config })
// then
expect(createdHttpTransports).toHaveLength(1)
expect(createdHttpTransports[0]?.options?.requestInit?.headers?.Authorization).toBe(
"Bearer xoxp-http-secret"
)
})
// then
expect(createdHttpTransports).toHaveLength(1)
expect(getHeaderValue(createdHttpTransports[0]?.options?.requestInit?.headers, "Authorization")).toBe(
"Bearer xoxp-http-secret"
)
})
})
})