P0.9.1 — ν MCP Bridge — Packet

Step 3 of the 5-step chain. Execution plan — exact file shapes, change diffs, and implementation order. This packet gates Step 4 (implementation).

Overview

Four files to create or modify:

  1. src/config.ts — add COLIBRI_MCP_TIMEOUT to Zod schema
  2. src/domains/integrations/mcp-bridge.ts — the bridge library
  3. src/domains/integrations/index.ts — add barrel export
  4. src/__tests__/domains/integrations/mcp-bridge.test.ts — tests

One doc file to create:

  1. docs/verification/p0-9-1-nu-mcp-bridge-verification.md — Step 5

1. src/config.ts — diff

Add one line to the Zod schema block (after the existing COLIBRI_STARTUP_TIMEOUT_MS entry):

+  /**
+   * Timeout for outbound MCP bridge connections and tool calls (P0.9.1).
+   *
+   * Applied to both the initial `connect()` handshake and to each
+   * `callTool()` attempt. Configures `AbortController` signal duration.
+   * Defaults to 30000 ms (30 s) per spec.
+   *
+   * Namespace: COLIBRI_* only. AMS_* is rejected by assertNoDonorNamespace.
+   */
+  COLIBRI_MCP_TIMEOUT: z.coerce
+    .number({
+      invalid_type_error: 'COLIBRI_MCP_TIMEOUT must be a positive integer',
+    })
+    .int('COLIBRI_MCP_TIMEOUT must be an integer')
+    .positive('COLIBRI_MCP_TIMEOUT must be > 0')
+    .default(30000),

2. src/domains/integrations/mcp-bridge.ts — full file plan

Module structure

// JSDoc file header (task + canonical refs)
// Imports: Client from SDK, StreamableHTTPClientTransport, ErrorCode, McpError, CallToolResultSchema
// Imports: z from 'zod'
// ─────────────────────────────────────────────
// Section: Exported types
//   McpBridge interface
//   TransportFactory type
//   ConnectOptions interface
//   CallToolOptions interface
// ─────────────────────────────────────────────
// Section: Error class
//   McpBridgeError extends Error
// ─────────────────────────────────────────────
// Section: Internal helpers
//   resolveTimeoutMs(env, override): number
//   isRetryable(err): boolean
//   sleep(ms): Promise<void>
//   callWithTimeout<T>(fn, timeoutMs): Promise<T>
// ─────────────────────────────────────────────
// Section: Exported constants
//   MCP_TIMEOUT_ENV_KEY = 'COLIBRI_MCP_TIMEOUT'
//   DEFAULT_TIMEOUT_MS = 30_000
//   DEFAULT_MAX_ATTEMPTS = 3
//   BACKOFF_BASE_MS = 1_000
// ─────────────────────────────────────────────
// Section: Public API
//   connectToServer(url, options?): Promise<McpBridge>
//   callTool(bridge, name, args, options?): Promise<CallToolResult>

Key implementation decisions

connectToServer — transport factory injection:

const factory = options?.transportFactory ?? defaultTransportFactory;
const transport = factory(url);
const client = new Client({ name: 'colibri-mcp-bridge', version: '0.0.1' });
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
  await client.connect(transport, { signal: controller.signal });
} finally {
  clearTimeout(timer);
}
return { client, url, close: () => client.close() };

defaultTransportFactory:

function defaultTransportFactory(url: string): Transport {
  return new StreamableHTTPClientTransport(new URL(url));
}

callTool — retry loop with backoff:

for (let attempt = 1; attempt <= maxAttempts; attempt++) {
  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), timeoutMs);
    try {
      const result = await bridge.client.callTool(
        { name, arguments: args },
        CallToolResultSchema,
        { signal: controller.signal },
      );
      return result;
    } finally {
      clearTimeout(timer);
    }
  } catch (err) {
    if (!isRetryable(err) || attempt === maxAttempts) {
      throw new McpBridgeError(
        `callTool "${name}" failed after ${attempt} attempt(s): ${errMsg(err)}`,
        { cause: err, attempts: attempt, retryable: isRetryable(err) },
      );
    }
    const delay = BACKOFF_BASE_MS * Math.pow(2, attempt - 1);
    logger(`[mcp-bridge] retrying in ${delay}ms (error: ${errMsg(err)})`);
    await sleep(delay);
  }
}

isRetryable:

function isRetryable(err: unknown): boolean {
  if (err instanceof McpError) {
    return (
      err.code === ErrorCode.InternalError ||
      err.code === ErrorCode.RequestTimeout ||
      err.code === ErrorCode.ConnectionClosed
    );
  }
  if (err instanceof DOMException && err.name === 'AbortError') return true;
  if (err instanceof TypeError) return true; // network failure
  return false;
}

3. src/domains/integrations/index.ts — diff

 export * from './notifications.js';
+export * from './mcp-bridge.js';

4. src/__tests__/domains/integrations/mcp-bridge.test.ts — test plan

Test transport helper

/**
 * Creates a linked Client+Server InMemoryTransport pair for unit testing.
 * The server has a single tool: `echo` that returns its args as content.
 */
async function createInMemoryBridge(): Promise<{
  bridge: McpBridge;
  serverTransport: InMemoryTransport;
}>

Test cases (8 groups)

  1. connectToServer returns a valid bridge
    • Assert bridge.url === url
    • Assert bridge.client instanceof Client
    • Assert bridge.close is a function
  2. callTool roundtrip via InMemory server
    • Mock server with echo tool (returns { text: JSON.stringify(args) })
    • Call callTool(bridge, 'echo', { hello: 'world' })
    • Assert result.content contains expected text
  3. Timeout via COLIBRI_MCP_TIMEOUT env
    • Pass env = { NODE_ENV: 'test', COLIBRI_MCP_TIMEOUT: '100' }
    • Mock transport that hangs (never resolves)
    • Assert McpBridgeError thrown with cause that is AbortError/timeout
  4. Retry on transient error — success on 3rd attempt
    • Mock transport that throws McpError(ErrorCode.InternalError) twice, succeeds on 3rd
    • Assert result is returned; assert attempts is 3 (via spy on logger)
    • Use fake sleep (override sleep via Jest fake timers or inject)
  5. Non-retryable error — no retry
    • Mock transport throws McpError(ErrorCode.MethodNotFound)
    • Assert McpBridgeError thrown after exactly 1 attempt
    • Assert error.retryable === false
    • Assert error.attempts === 1
  6. Max retries exceeded → McpBridgeError
    • Mock transport always throws McpError(ErrorCode.InternalError)
    • Assert McpBridgeError thrown; error.attempts === 3
  7. close() resolves cleanly
    • Call bridge.close()
    • Await resolves without error
  8. Default timeout is 30000 ms
    • Call resolveTimeoutMs(process.env, undefined) with no env override
    • Assert result === 30000

Test infrastructure

  • Use jest.useFakeTimers() to avoid real sleep delays in retry tests.
  • Override sleep via a module-level injectable or use jest.spyOn + jest.runAllTimersAsync().
  • The InMemory mock server uses Server from @modelcontextprotocol/sdk/server/index.js with setRequestHandler(CallToolRequestSchema, ...).

5. Verification doc structure (Step 5)

After tests pass, record:

  • npm run build output (zero errors)
  • npm test -- --testPathPattern mcp-bridge output (all tests pass)
  • Full suite pass count
  • Any pre-existing flakes noted

Migration decision

No migration. Bridge state is ephemeral. The contract §7 justification stands.


Implementation order

  1. Edit src/config.ts — add COLIBRI_MCP_TIMEOUT
  2. Create src/domains/integrations/mcp-bridge.ts
  3. Edit src/domains/integrations/index.ts — add barrel export
  4. Create src/__tests__/domains/integrations/mcp-bridge.test.ts
  5. Run npm run build && npm test
  6. Write docs/verification/p0-9-1-nu-mcp-bridge-verification.md

All changes land in a single feat(p0-9-1) commit (Step 4).


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.