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:
@@ -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"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user