Skip to content

add support for streaming http mcp servers#26

Merged
j4ys0n merged 5 commits intomainfrom
streaming-mcp
Feb 12, 2026
Merged

add support for streaming http mcp servers#26
j4ys0n merged 5 commits intomainfrom
streaming-mcp

Conversation

@j4ys0n
Copy link
Contributor

@j4ys0n j4ys0n commented Feb 10, 2026

  • add ability to mcp servers with streaming http transport in addition to stdio transport.
  • full mcp specification docs provided in mcp-spec directory.

Copilot AI review requested due to automatic review settings February 10, 2026 21:49
@j4ys0n
Copy link
Contributor Author

j4ys0n commented Feb 10, 2026

Automated review 🤖

Summary of Changes
This PR adds support for Streamable HTTP transport to the MCP API service, enabling servers to be configured with either stdio (existing) or streamable_http transports. The implementation introduces a discriminated union for server configuration, updates the connection lifecycle to support both transports, and adds SSE fallback for legacy servers. Backward compatibility is preserved: existing stdio servers default to transportType: 'stdio', and new servers can be installed or configured with transportType: 'streamable_http'. The version bump to 1.9.0 reflects this new capability.

Architecture & Organization

  • Introduced MCPTransportType and discriminated union types (StdioServerConfig, StreamableHttpServerConfig) to enforce valid config combinations in src/services/mcp.ts.
  • Abstracted transport creation via createTransport() and createSseTransport() factories, decoupling connection logic from transport implementation.
  • Unified server key generation using buildServerKey() to ensure stable keys regardless of transport type, replacing previous key construction based on command.
  • Added MCPServerRecord for database persistence and normalizeServerRecord() to migrate legacy records with transportType and field normalization.
  • Centralized teardown logic in teardownServerConnection() to handle both stdio and HTTP transport cleanup, including optional terminateSession() for HTTP.

Key Changes & Positives

  • Transport abstraction via Transport interface from MCP SDK enables seamless switching between StdioClientTransport and StreamableHTTPClientTransport (🟢).
  • SSE fallback on HTTP 400/404/405 errors ensures backward compatibility with legacy servers (🟢).
  • Discriminated union and assertTransportConfigCompatible() prevent invalid config combinations at runtime (🟢).
  • buildServerKey() ensures stable server keys across transport changes, enabling enable/disable/update without reconnection issues (🟢).
  • OAuth callback integration path documented in .nexus/guides/streaming-transport-implementation.md supports future token refresh (🟢).

Potential Issues & Recommendations

  1. Issue / Risk: teardownServerConnection() calls terminateSession() only for StreamableHTTPClientTransport, but does not handle terminateSession() failure gracefully in all code paths (e.g., during disableServer).

    • Impact: Orphaned sessions on the server may accumulate over time, causing resource leaks.
    • Recommendation: Wrap terminateSession() in a try/catch and log warnings without blocking teardown; consider adding a session cleanup job.
    • Status: 🟡 Needs review
  2. Issue / Risk: SSE fallback logic in connectToServer() reuses the same serverKey, but does not persist the fallback decision or update the stored server config to reflect transportType: 'streamable_http' vs 'sse'.

    • Impact: Subsequent restarts may attempt Streamable HTTP again instead of using SSE, causing repeated fallbacks and delays.
    • Recommendation: After fallback, persist transportType: 'streamable_http' and sessionId (if available) to the database to avoid repeated fallback attempts.
    • Status: 🟡 Needs review
  3. Issue / Risk: PackageService.installPackage skips package upgrades for non-stdio servers, but the error message (Package upgrades are only supported for stdio servers.) may mislead users into thinking HTTP servers cannot be upgraded at all, when the limitation is due to stdio-specific command resolution.

    • Impact: Users may be unable to upgrade packages for HTTP servers without manual intervention.
    • Recommendation: Clarify error message to distinguish between "stdio-specific upgrade logic" and "HTTP server upgrade not supported", or implement HTTP-compatible upgrade path (e.g., via URL-based versioning).
    • Status: 🟡 Needs review

Language/Framework Checks

  • TypeScript types correctly use discriminated unions (MCPServer) and instanceof checks (StdioClientTransport) for safe narrowing in src/services/mcp.ts.
  • createTransport() and createSseTransport() correctly import and instantiate StreamableHTTPClientTransport and SSEClientTransport from @modelcontextprotocol/sdk/client/streamableHttp.js and sse.js.
  • Error extraction via extractHttpStatusFromError() handles both StreamableHTTPError.code and regex parsing of Error.message, covering SDK error formats.
  • buildServerKey() uses sanitizeString() consistently, matching existing patterns in src/utils/general.ts.

Security & Privacy

  • HTTP headers (including Authorization) are stored in server config and passed via requestInit to StreamableHTTPClientTransport, preserving token security without exposing secrets in logs.
  • OAuth callback flow stores tokens in MissionSquad’s Secrets service and merges them into server headers only after successful exchange, avoiding plaintext token handling in mcp-api.
  • No new secrets or credentials are introduced; existing Secrets service integration remains unchanged.

Build/CI & Ops

  • Version bump to 1.9.0 in package.json aligns with feature addition.
  • No new external dependencies added; uses existing MCP SDK modules (streamableHttp, sse).
  • Migration logic (migrateServerTransport) handles legacy records automatically, reducing ops overhead during deployment.

Tests

  • Unit tests in test/mcp-transport.spec.ts verify buildServerKey, createTransport, and config validation for both transports.
  • Integration test in test/mcp-streamable-http.integration.spec.ts validates full lifecycle: initialize, list tools, tool call with progress, SSE reconnect, and SSE resumability via eventStore.
  • Missing regression tests for stdio servers (existing behavior) — should add tests to ensure stdio servers continue to start, fetch tools, and call tools without regression.

Approval Recommendation
Approve with caveats

  • Address terminateSession() error handling in teardownServerConnection() to prevent orphaned sessions.
  • Persist fallback decision (transportType, sessionId) after SSE fallback to avoid repeated fallback attempts.
  • Clarify or extend package upgrade support for HTTP servers (e.g., update error message or implement URL-based upgrade).
  • Add regression tests for stdio servers to ensure no behavioral regressions.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds comprehensive support for streaming HTTP MCP servers alongside the existing stdio transport mechanism. The implementation introduces discriminated union types to ensure type-safe configuration, includes automatic migration logic for existing servers, and provides SSE fallback for legacy HTTP servers that don't support the streaming protocol.

Changes:

  • Introduces transport-agnostic server architecture using discriminated unions (transportType: 'stdio' | 'streamable_http')
  • Implements StreamableHTTPClientTransport with automatic fallback to SSEClientTransport for legacy servers returning 400/404/405 status codes
  • Adds comprehensive integration tests for HTTP transport including tool execution, progress notifications, and SSE reconnection

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/services/mcp.ts Core transport abstraction with factory functions, migration logic, SSE fallback, and unified connection lifecycle management
src/services/packages.ts Updates package installation to support transport type specification and validates HTTP-only installs
src/controllers/mcp.ts Updates API types to use new AddServerInput/UpdateServerInput and returns transport-specific fields in server listings
test/mcp-transport.spec.ts Unit tests for transport selection, server key stability, and config validation
test/mcp-streamable-http.integration.spec.ts End-to-end integration test covering initialization, tool calls, progress notifications, and SSE reconnection
package.json Version bump to 1.9.0 reflecting new feature
.nexus/guides/streaming-transport-implementation.md Comprehensive implementation guide documenting design decisions, SDK usage patterns, and OAuth integration strategy

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +55 to +62
test('streamable_http config rejects stdio-only fields', () => {
expect(() =>
assertTransportConfigCompatible({
transportType: 'streamable_http',
command: 'node'
})
).toThrow('Streamable HTTP servers cannot define stdio fields')
})
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the inverse validation case: stdio servers with HTTP fields. The assertTransportConfigCompatible function validates both scenarios (lines 132-148 in src/services/mcp.ts), but only the streamable_http validation is tested. Add a test case that verifies stdio servers reject HTTP fields like url, headers, sessionId, and reconnectionOptions.

Copilot uses AI. Check for mistakes.
Comment on lines +557 to +559
fallbackTransport.onerror = transportErrorHandler
fallbackTransport.onclose = transportCloseHandler

Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When falling back to SSE transport after StreamableHTTPClientTransport fails, the event handlers (transportErrorHandler and transportCloseHandler) are reused from the failed connection but were already assigned to the failed transport's onerror and onclose. If the failed transport's error/close callbacks fire during cleanup, they will log to the wrong transport. Consider creating new handler instances for the fallback client, or clear the event handlers from the failed transport before cleanup.

Suggested change
fallbackTransport.onerror = transportErrorHandler
fallbackTransport.onclose = transportCloseHandler
// Use fresh handler instances for the SSE fallback transport to avoid
// reusing handlers that were attached to the failed transport.
const fallbackTransportErrorHandler = (event: unknown) => {
// Delegate to the original error handler to preserve behavior.
if (typeof transportErrorHandler === 'function') {
transportErrorHandler(event as any)
}
}
const fallbackTransportCloseHandler = (event: unknown) => {
// Delegate to the original close handler to preserve behavior.
if (typeof transportCloseHandler === 'function') {
transportCloseHandler(event as any)
}
}
fallbackTransport.onerror = fallbackTransportErrorHandler
fallbackTransport.onclose = fallbackTransportCloseHandler

Copilot uses AI. Check for mistakes.
Comment on lines +565 to +579
try {
await fallbackClient.connect(fallbackTransport)
this.servers[serverKey].status = 'connected'
log({
level: 'info',
msg: `[${server.name}] Connected successfully using SSE fallback. Will fetch tool list.`
})
return
} catch (fallbackError) {
log({
level: 'error',
msg: `[${server.name}] SSE fallback connection failed.`,
error: fallbackError
})
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the SSE fallback connection attempt fails, the fallbackClient and fallbackTransport are not cleaned up. This could lead to resource leaks if event handlers are still attached. Consider wrapping lines 565-572 in a try-finally block that calls teardownServerConnection or manually cleans up the fallback client/transport on failure.

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +333
const { url, headers, sessionId, reconnectionOptions, ...rest } = server
const normalized: MCPServer = {
...rest,
transportType: 'stdio',
command: server.command,
args,
env
}
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During migration of stdio servers, HTTP-specific fields (url, headers, sessionId, reconnectionOptions) are destructured and discarded when creating the normalized server object. While this is intentional for normalization, if there's any chance these fields were incorrectly set on stdio servers in the database (e.g., due to manual edits or bugs), this data will be silently lost. Consider logging when these fields are being removed from stdio servers, similar to how command/args/env removal is tracked for HTTP servers on line 282-283.

Copilot uses AI. Check for mistakes.
if (!url) {
return { success: false, error: 'Streamable HTTP servers require a url.' }
}
new URL(url)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL validation constructor is called for its side effect (throwing on invalid URL) but the result is not used and the error is not caught. If the URL is invalid, this will throw an uncaught error. Consider wrapping this in a try-catch block and returning a more user-friendly error message, similar to how invalid package names are handled on lines 180-185.

Suggested change
new URL(url)
try {
new URL(url)
} catch {
return {
success: false,
error: `Invalid URL for streamable HTTP server: ${url}`
}
}

Copilot uses AI. Check for mistakes.
throw new Error('Streamable HTTP servers require a url.')
}

new URL(serverData.url)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL validation constructor is called for its side effect (throwing on invalid URL) but the result is not used and the error is not caught. If the URL is invalid, this will throw an uncaught error. Consider wrapping this in a try-catch block to provide a more informative error message to the caller.

Suggested change
new URL(serverData.url)
try {
// Validate URL; throws if invalid
new URL(serverData.url)
} catch (error) {
throw new Error(`Invalid URL for streamable HTTP server '${name}': ${serverData.url}`)
}

Copilot uses AI. Check for mistakes.
if (!url) {
throw new Error('Streamable HTTP servers require a url.')
}
new URL(url)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL validation constructor is called for its side effect (throwing on invalid URL) but the result is not used and the error is not caught. If the URL is invalid, this will throw an uncaught error. Consider wrapping this in a try-catch block to provide a more informative error message to the caller.

Suggested change
new URL(url)
try {
// Validate URL; the constructed value is not used, only its validation side effect.
new URL(url)
} catch (err) {
const message =
err instanceof Error && err.message
? `Invalid URL for streamable HTTP server "${name}": ${err.message}`
: `Invalid URL for streamable HTTP server "${name}".`
throw new Error(message)
}

Copilot uses AI. Check for mistakes.
transportType: 'streamable_http',
command: 'node'
})
).toThrow('Streamable HTTP servers cannot define stdio fields')
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects the error message to start with "Streamable HTTP servers cannot define stdio fields" but the actual error message thrown by assertTransportConfigCompatible includes the full list "(command, args, env)." at the end (line 136 in src/services/mcp.ts). The test should match the complete error message or use a partial match with expect().toMatch() or toContain().

Suggested change
).toThrow('Streamable HTTP servers cannot define stdio fields')
).toThrow(/Streamable HTTP servers cannot define stdio fields/)

Copilot uses AI. Check for mistakes.
@j4ys0n
Copy link
Contributor Author

j4ys0n commented Feb 10, 2026

Automated review 🤖

Summary of Changes
This PR adds support for streaming HTTP (Streamable HTTP) transport to the MCP API, enabling servers to be configured as either stdio (existing) or streamable_http. It introduces a discriminated union for server configuration, updates the connection lifecycle to support both transports, implements SSE fallback for legacy servers, and adds OAuth-aware header injection via webhook callbacks. Backward compatibility is preserved: existing stdio servers default to transportType: 'stdio', and API payloads omitting transportType default to stdio. The package version is bumped to 1.9.0.

Architecture & Organization

  • Introduces a discriminated union (MCPServer) using transportType to enforce mutually exclusive config fields (command/args/env vs url/headers/sessionId), preventing invalid configurations at compile time.
  • Centralizes transport selection in createTransport and createSseTransport factories, decoupling connection logic from server config parsing.
  • Adds buildServerKey to ensure stable server keys independent of transport type, enabling seamless enable/disable/update flows across transports.
  • Introduces MCPServerRecord as a database-optimized union type with optional fields, and normalizeServerRecord to migrate legacy records to the new discriminated shape.
  • Separates stdio-specific logic (e.g., stderr handling, package installation) from generic transport handling, reducing coupling.

Key Changes & Positives

  • Transport abstraction: MCPConnection.transport now uses the SDK’s Transport interface, enabling unified handling of stdio and HTTP transports. 🟢
  • SSE fallback: Automatic fallback to SSEClientTransport when Streamable HTTP initialize POST fails with 400/404/405, preserving compatibility with legacy servers. 🟢
  • Config validation: assertTransportConfigCompatible enforces strict field separation between transport types at runtime, preventing misconfiguration. 🟢
  • OAuth webhook integration: Clear path to inject access tokens into HTTP headers via MissionSquad webhook callbacks, avoiding SDK dependency bloat. 🟢
  • Migration support: migrateServerTransport auto-normalizes legacy servers to include transportType: 'stdio', ensuring seamless upgrade. 🟢

Potential Issues & Recommendations

  1. Issue / Risk: teardownServerConnection calls terminateSession() only for StreamableHTTPClientTransport, but does not handle terminateSession errors gracefully beyond logging.

    • Impact: Unhandled errors during teardown could leave sessions dangling on the server, causing resource leaks or auth conflicts.
    • Recommendation: Add explicit error handling with retry or cleanup of stored session ID in server state.
    • Status: 🟡 Needs review
  2. Issue / Risk: SSE fallback logic (shouldFallbackToSse) parses HTTP status from error messages using regex (/HTTP\s+(\d{3})/), which may be fragile if SDK error message format changes.

    • Impact: Could miss fallback opportunities or incorrectly trigger fallback for non-eligible errors.
    • Recommendation: Prefer SDK-provided error type guards (e.g., StreamableHTTPError.code) where available, and add unit tests for error message variations.
    • Status: 🟡 Needs review
  3. Issue / Risk: PackageService.installPackage skips installation for non-stdio servers, but upgradePackage only upgrades stdio servers. This asymmetry prevents upgrading HTTP-only packages.

    • Impact: Users cannot upgrade HTTP servers installed via transportType: 'streamable_http' if they lack stdio fields.
    • Recommendation: Extend upgradePackage to support streamable_http by skipping stdio-specific command resolution and preserving HTTP config fields.
    • Status: 🔴 Problem
  4. Issue / Risk: No automatic token refresh for OAuth; users must manually re-run the OAuth flow when tokens expire.

    • Impact: HTTP servers will fail after token expiry until manual re-auth, reducing reliability.
    • Recommendation: Document token expiry behavior clearly and consider adding a refresh hook in future.
    • Status: 🟡 Needs review

Language/Framework Checks

  • TypeScript types correctly use discriminated unions (MCPServer, AddServerInput, UpdateServerInput) with transportType discriminant.
  • createTransport and createSseTransport use exact SDK imports (StreamableHTTPClientTransport, SSEClientTransport) and match constructor signatures (e.g., new URL(server.url), requestInit options).
  • Error extraction (extractHttpStatusFromError) correctly handles StreamableHTTPError.code and falls back to regex parsing of Error.message.
  • teardownServerConnection uses type guard isStreamableHTTPTransport to safely call terminateSession.

Security & Privacy

  • OAuth tokens are stored in MissionSquad’s Secrets service and injected into HTTP headers only after successful webhook callback, avoiding exposure in logs or config.
  • assertTransportConfigCompatible prevents accidental mixing of stdio and HTTP fields, reducing risk of misconfigured auth or endpoints.
  • Session IDs (sessionId) and headers are optional in config, minimizing data persistence unless explicitly provided.

Build/CI & Ops

  • No new external dependencies added; uses existing MCP SDK modules (@modelcontextprotocol/sdk/client/streamableHttp.js, sse.js).
  • Migration logic (migrateServerTransport) logs warnings on DB update failures but continues startup, ensuring graceful degradation.
  • Integration test (mcp-streamable-http.integration.spec.ts) simulates SSE disconnect/reconnect and progress notifications, validating core behavior.

Tests

  • Unit tests (mcp-transport.spec.ts) verify buildServerKey stability, createTransport selection, and config validation.
  • Integration test (mcp-streamable-http.integration.spec.ts) validates full lifecycle: initialize, tools/list, tool call with progress, and SSE reconnect via Last-Event-ID.
  • Missing: Regression tests for stdio servers to ensure no behavioral changes; add explicit tests for GET /mcp/servers response shape with both transport types.

Approval Recommendation
Approve with caveats

  • Address teardownServerConnection error handling for terminateSession failures.
  • Fix PackageService.upgradePackage to support streamable_http servers.
  • Add integration tests verifying stdio servers remain fully functional after changes.
  • Document token expiry and manual re-auth process in guide.

@j4ys0n
Copy link
Contributor Author

j4ys0n commented Feb 10, 2026

Automated review 🤖

Summary of Changes
This PR adds support for streaming HTTP MCP servers in addition to the existing stdio transport, following the Model Context Protocol specification. Key changes include:

  • Introduce transportType discriminant (stdio vs streamable_http) and dedicated config types for each.
  • Implement transport factory and connection lifecycle updates to support both transports, including SSE fallback for legacy servers.
  • Update API contracts (AddServerRequest, UpdateServerRequest) and OpenAPI schemas to accept HTTP configuration.
  • Add migration logic to normalize existing servers to transportType: 'stdio' by default.
  • Support package installation and upgrade for both transport types, with validation and rollback safety.
  • Add integration and unit tests for streamable HTTP and SSE fallback behavior.

Architecture & Organization

  • Introduces discriminated union types (MCPServer, StdioServerConfig, StreamableHttpServerConfig) to prevent invalid config combinations.
  • Centralizes transport selection in createTransport and createSseTransport factories, decoupling connection logic from server configuration.
  • Adds buildServerKey to ensure stable keys across transport changes, avoiding duplicate server entries.
  • Introduces MCPServerRecord as an intermediate type for database persistence and normalization, separating storage concerns from runtime types.

Key Changes & Positives

  • Transport abstraction: MCPConnection.transport now uses Transport from the SDK, enabling support for multiple transports without breaking stdio. 🟢
  • SSE fallback: Clients automatically fall back to SSEClientTransport when Streamable HTTP initialize POST fails with 400/404/405, preserving compatibility with legacy servers. 🟢
  • Config validation: assertTransportConfigCompatible enforces strict separation of stdio and HTTP fields at runtime, preventing misconfiguration. 🟢
  • Migration path: Automatic normalization of existing servers to transportType: 'stdio' ensures backward compatibility without manual intervention. 🟢
  • OAuth-ready: HTTP config includes headers, enabling static token storage for OAuth flows via MissionSquad webhook callbacks. 🟢

Potential Issues & Recommendations

  1. Issue / Risk: The normalizeServerRecord function catches errors during migration but logs warnings instead of failing fast, potentially leaving servers in an invalid state.

    • Impact: Servers may fail to start silently if migration logic encounters unexpected data.
    • Recommendation: Add a health check endpoint or startup validation to surface migration failures explicitly.
    • Status: 🟡 Needs review
  2. Issue / Risk: SSE fallback logic reuses StreamableHttpServerConfig fields (url, headers) to construct SSEClientTransport, but the SDK expects a URL-only constructor.

    • Impact: Headers may not be applied correctly during fallback, causing authentication failures.
    • Recommendation: Explicitly pass requestInit.headers to SSEClientTransport and verify header propagation in tests.
    • Status: 🟡 Needs review
  3. Issue / Risk: teardownServerConnection calls terminateSession for all StreamableHTTPClientTransport instances, but the SDK may throw if the session is already expired.

    • Impact: Unhandled errors during shutdown could prevent clean server restarts.
    • Recommendation: Wrap terminateSession in a try/catch and log only warnings for expired sessions.
    • Status: 🟡 Needs review
  4. Issue / Risk: Package upgrade logic for stdio servers assumes transportType remains 'stdio', but the UpdateServerInput allows changing transportType.

    • Impact: Upgrading a streamable HTTP server via PackageService could incorrectly attempt stdio-specific command resolution.
    • Recommendation: Add a transport-type guard in PackageService.upgradePackage to skip upgrades for non-stdio servers.
    • Status: 🔴 Problem

Language/Framework Checks

  • TypeScript: All new types (MCPTransportType, StdioServerConfig, StreamableHttpServerConfig, AddServerInput, UpdateServerInput) are properly discriminated unions.
  • SDK usage: Correctly imports StreamableHTTPClientTransport, SSEClientTransport, and Transport from @modelcontextprotocol/sdk.
  • Error handling: Uses StreamableHTTPError and regex parsing to extract HTTP status codes for fallback decisions.
  • File: src/services/mcp.ts lines 1–23, 38–270, 215–220, 591–600, 1032–1040, 1110–1120, 1132–1140, 1150–1160.

Security & Privacy

  • No new secrets handling or credential storage; HTTP headers are stored in plaintext in the database (same as stdio env).
  • Session IDs are managed by the SDK and must be handled securely by clients per the MCP spec.
  • No changes to auth flows; OAuth support is enabled via headers but not implemented in this PR.

Build/CI & Ops

  • Package version bumped to 1.9.0.
  • New integration test (test/mcp-streamable-http.integration.spec.ts) validates full lifecycle including SSE reconnect.
  • Unit tests (test/mcp-transport.spec.ts) cover transport selection and config validation.
  • Migration logic ensures existing servers start without manual intervention.

Tests

  • Coverage needs:
    • Add tests for normalizeServerRecord with malformed records (missing command/url).
    • Add tests for SSE fallback with mocked StreamableHTTPError (400/404/405).
    • Add tests for teardownServerConnection with expired sessions.
    • Add integration tests for package upgrade of streamable HTTP servers (should skip).
  • New tests added:
    • test/mcp-streamable-http.integration.spec.ts: Full HTTP/SSE integration test.
    • test/mcp-transport.spec.ts: Transport selection and validation tests.

Approval Recommendation
Approve with caveats

  • Address issue disable security scan for now #4: Add transport-type guard in PackageService.upgradePackage to skip non-stdio servers.
  • Review and confirm SSE fallback header propagation in createSseTransport.
  • Add explicit error handling for expired sessions in teardownServerConnection.
  • Add migration validation tests to surface failures early.

@j4ys0n
Copy link
Contributor Author

j4ys0n commented Feb 11, 2026

Automated review 🤖

Summary of Changes
This PR adds support for streaming HTTP transport (Streamable HTTP and SSE fallback) to the MCP API, alongside the existing stdio transport. Key changes include:

  • Introduce transportType (stdio or streamable_http) as a first-class server configuration discriminant with dedicated config types.
  • Implement transport factories and connection lifecycle updates to support both transports, including session persistence and 404 recovery for Streamable HTTP.
  • Add OAuth token storage and OAuthClientProvider integration for automatic token refresh on 401/403.
  • Update API contracts (controller types, OpenAPI schema, README) to accept HTTP-specific fields and preserve backward compatibility (defaults to stdio).
  • Add integration tests for Streamable HTTP and unit tests for transport selection and config validation.
  • Bump package version to 1.9.0.

Architecture & Organization

  • Transport selection is driven by a discriminated union (MCPServer) with transportType as the discriminant, preventing invalid config combinations.
  • createTransport and createSseTransport factories centralize transport instantiation, enabling clean separation between stdio and HTTP logic.
  • Server key construction (buildServerKey) is stable across transport changes, ensuring lifecycle operations (enable/disable/update) work consistently.
  • OAuth token storage (McpOAuthTokens) is isolated in its own service, with encrypted fields and a dedicated McpOAuthClientProvider implementation.

Key Changes & Positives

  • Support for Streamable HTTP transport with optional SSE fallback on 400/404/405 errors.
  • Session ID persistence and automatic 404 recovery (clear session and retry once without it).
  • OAuth token storage and refresh via OAuthClientProvider, enabling scheduled tasks to remain authenticated.
  • Backward compatible: existing stdio servers continue to work; transportType defaults to stdio.
  • Transport-specific config validation (assertTransportConfigCompatible) prevents mixing stdio and HTTP fields. 🟢

Potential Issues & Recommendations

  1. Issue / Risk: The updateServerOAuth endpoint does not validate that the server is enabled before updating tokens.
    • Impact: Tokens may be updated for disabled servers, leading to confusion or stale state.
    • Recommendation: Add a check to ensure the server exists and is enabled before proceeding.
    • Status: 🟡 Needs review
  2. Issue / Risk: The updateServer flow restarts the server for any config change, but HTTP-only changes (e.g., headers, sessionId) do not require a restart.
    • Impact: Unnecessary restarts for HTTP-only updates, causing downtime.
    • Recommendation: Only restart the server when transport-type-specific fields change (e.g., command, url).
    • Status: 🟡 Needs review
  3. Issue / Risk: The teardownServerConnection cleanup for stdio servers does not remove the stderrDataHandler listener if stderr is null, potentially causing memory leaks.
    • Impact: Event listener accumulation over time.
    • Recommendation: Guard stderr handler removal with a null check before calling removeListener.
    • Status: 🟡 Needs review
  4. Issue / Risk: The shouldFallbackToSse logic relies on parsing error messages for HTTP status codes, which is fragile and may break with SDK changes.
    • Impact: Fallback may not trigger for some error cases, leaving users without a working transport.
    • Recommendation: Use SDK error types or status codes directly if available, or add explicit error handling in the SDK layer.
    • Status: 🔴 Problem

Language/Framework Checks

  • TypeScript: All new types (MCPTransportType, StdioServerConfig, StreamableHttpServerConfig, MCPServer, AddServerInput, UpdateServerInput) are correctly defined and used.
  • SDK usage: Correctly imports StreamableHTTPClientTransport, SSEClientTransport, and OAuthClientProvider from @modelcontextprotocol/sdk.
  • Express: Controller types (AddServerRequest, UpdateServerRequest, UpdateServerOAuthRequest) align with service inputs.
  • MongoDB: MCPServerRecord and MCPServer types ensure correct schema migration and persistence.
  • Zod: Integration test uses z.string() for schema validation, consistent with SDK types.

Security & Privacy

  • OAuth tokens and client secrets are encrypted using SecretEncryptor with SECRETS_KEY.
  • Authorization headers are stripped from static headers when OAuth tokens are present to avoid conflicts.
  • Session IDs are securely generated (UUID) and stored encrypted.
  • assertTransportConfigCompatible prevents mixing stdio and HTTP fields, reducing config errors.

Build/CI & Ops

  • No build or CI changes required beyond running npm run build and npm test as per the implementation guide.
  • Docker deployment is unchanged; the new features are runtime-only.

Tests

  • Unit tests added for buildServerKey, createTransport, and assertTransportConfigCompatible.
  • Integration test for Streamable HTTP with SSE fallback, progress notifications, and reconnect behavior.
  • Tests cover stdio and HTTP transport selection, config validation, and 404 recovery.
  • Tests should be run with npm test after implementation.

Approval Recommendation
Approve with caveats

  • Add validation in updateServerOAuth to ensure the server is enabled before updating tokens.
  • Modify updateServer to only restart the server when transport-type-specific fields change (e.g., command, url).
  • Guard stderr handler removal in teardownServerConnection with a null check to prevent memory leaks.
  • Review shouldFallbackToSse error parsing logic for fragility and consider SDK-level error handling.

@j4ys0n
Copy link
Contributor Author

j4ys0n commented Feb 11, 2026

Automated review 🤖

Summary of Changes
This PR adds support for Streamable HTTP transport in addition to the existing stdio transport for MCP servers. Key changes include:

  • Introduce transportType as a discriminator (stdio vs streamable_http) across data models, API schemas, and configuration.
  • Implement transport factories and connection lifecycle management for both stdio and Streamable HTTP, including SSE fallback on legacy HTTP status codes.
  • Add OAuth token storage and OAuthClientProvider integration for automatic token refresh in Streamable HTTP servers.
  • Update OpenAPI and README to reflect new transport options, session persistence, and OAuth behavior.
  • Add integration and unit tests for transport selection and Streamable HTTP behavior.

Architecture & Organization

  • Introduce discriminated union types (MCPServer, StdioServerConfig, StreamableHttpServerConfig) to enforce valid configuration per transport.
  • Centralize transport creation in createTransport and server key building in buildServerKey to ensure consistent lifecycle handling.
  • Decouple transport concerns from core service logic while preserving stdio-specific stderr handling.
  • Add McpOAuthTokens service and McpOAuthClientProvider to manage encrypted OAuth tokens and refresh flows.

Key Changes & Positives

  • Transport abstraction: MCPConnection.transport now uses Transport instead of StdioClientTransport, enabling multiple transport types. 🟢
  • Session persistence: Streamable HTTP sessions are persisted and resumed across restarts via sessionId. 🟢
  • OAuth integration: Automatic token refresh for Streamable HTTP servers using OAuthClientProvider. 🟢
  • Backward compatibility: Existing stdio servers default to transportType: 'stdio' and continue to work without changes. 🟢
  • SSE fallback: Clients automatically fall back to SSE transport when Streamable HTTP initialization fails with 400/404/405. 🟢

Potential Issues & Recommendations

  1. Issue / Risk: Streamable HTTP servers require a valid URL; validation occurs at addServer/updateServer, but runtime URL changes are not prevented after startup.

    • Impact: Misconfigured servers may fail at connection time with unclear error messages.
    • Recommendation: Add runtime health checks or validation hooks before connection attempts.
    • Status: 🟡 Needs review
  2. Issue / Risk: The SSE fallback logic reuses the same MCPServer instance but swaps its connection.transport. If event handlers are not fully detached, residual listeners could cause leaks.

    • Impact: Memory leaks or duplicate notifications on reconnect.
    • Recommendation: Ensure teardownServerConnection fully detaches handlers before fallback transport creation.
    • Status: 🟡 Needs review
  3. Issue / Risk: Session ID clearing on 404 is transport-specific and only applies to Streamable HTTP; stdio servers are unaffected. The guide implies this is intentional, but the behavior is not surfaced in logs.

    • Impact: Operators may find it difficult to diagnose session expiration issues without explicit logging.
    • Recommendation: Add a log entry when clearing sessionId to aid debugging.
    • Status: 🟡 Needs review

Language/Framework Checks

  • TypeScript types enforce transportType discrimination and reject incompatible config fields via assertTransportConfigCompatible.
  • createTransport correctly instantiates StdioClientTransport or StreamableHTTPClientTransport based on transportType.
  • OAuth provider integration uses SDK types (OAuthClientProvider, OAuthTokens) and encrypts secrets using SecretEncryptor.
  • OpenAPI schemas updated to reflect transportType, url, headers, sessionId, and reconnectionOptions as conditional fields.

Security & Privacy

  • OAuth tokens and client secrets are encrypted using SecretEncryptor and stored in a dedicated mcpOAuthTokens collection.
  • Static Authorization headers are stripped when OAuth tokens are present to avoid conflicts.
  • Session IDs are cryptographically generated UUIDs and handled securely per spec.
  • Stdio servers continue to use environment variables for secrets, preserving existing isolation.

Build/CI & Ops

  • No new external dependencies; uses existing MCP SDK packages (@modelcontextprotocol/sdk).
  • Migration logic normalizes legacy servers to include transportType: 'stdio' and cleans up old fields.
  • PackageService.installPackage and upgradePackage now support both transport types, preserving HTTP config during upgrades.

Tests

  • New integration test (mcp-streamable-http.integration.spec.ts) validates initialize, tools/list, tool call with progress, and SSE reconnect behavior.
  • New unit test (mcp-transport.spec.ts) verifies transport selection, key stability, and config validation.
  • Regression tests for stdio servers are implied by existing tests and preserved by defaulting to stdio.

Approval Recommendation
Approve with caveats

  • Address SSE fallback handler detachment to prevent listener leaks (see Issue improvements to packages service #2).
  • Add explicit logging when clearing sessionId on 404 for operational visibility.
  • Consider runtime validation or health checks for Streamable HTTP URLs to surface misconfiguration earlier.

@j4ys0n j4ys0n merged commit db7a0cf into main Feb 12, 2026
1 check passed
@j4ys0n j4ys0n deleted the streaming-mcp branch February 12, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants