diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c56163..8854188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.1] - 2026-02-03 + +### Added + +- **SSE Transport Support** - Support for Server-Sent Events transport protocol + - Requires `type: "sse"` in server config + - Enables connection to MCP servers using SSE (e.g. LangChain, remote servers) + ## [0.3.0] - 2026-01-22 ### Added diff --git a/README.md b/README.md index 4d1c6a3..315bddc 100644 --- a/README.md +++ b/README.md @@ -299,6 +299,24 @@ The CLI uses `mcp_servers.json`, compatible with Claude Desktop, Gemini or VS Co } ``` +### SSE Transport (Legacy) + +For servers using the deprecated SSE transport protocol, add `"type": "sse"` to the configuration: + +```json +{ + "mcpServers": { + "legacy-server": { + "url": "http://localhost:8000", + "type": "sse" + } + } +} +``` + +> [!NOTE] +> SSE transport is deprecated. New servers should use Streamable HTTP transport. + **Environment Variable Substitution:** Use `${VAR_NAME}` syntax anywhere in the config. Values are substituted at load time. By default, missing environment variables cause an error with a clear message. Set `MCP_STRICT_ENV=false` to use empty values instead (with a warning). ### Tool Filtering diff --git a/package.json b/package.json index a1985d7..fcec25c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mcp-cli", - "version": "0.3.0", + "version": "0.3.1", "description": "A lightweight CLI for interacting with MCP (Model Context Protocol) servers", "type": "module", "main": "src/index.ts", diff --git a/src/client.ts b/src/client.ts index 50731cf..0e36207 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,12 +3,14 @@ */ import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import type { Tool } from '@modelcontextprotocol/sdk/types.js'; import { type HttpServerConfig, type ServerConfig, + type SseServerConfig, type StdioServerConfig, debug, filterTools, @@ -18,6 +20,7 @@ import { getTimeoutMs, isDaemonEnabled, isHttpServer, + isSseServer, isToolAllowed, } from './config.js'; import { @@ -236,9 +239,14 @@ export async function connectToServer( }, ); - let transport: StdioClientTransport | StreamableHTTPClientTransport; + let transport: + | StdioClientTransport + | StreamableHTTPClientTransport + | SSEClientTransport; - if (isHttpServer(config)) { + if (isSseServer(config)) { + transport = createSseTransport(config); + } else if (isHttpServer(config)) { transport = createHttpTransport(config); } else { transport = createStdioTransport(config); @@ -269,7 +277,7 @@ export async function connectToServer( } // For successful connections, forward stderr to console - if (!isHttpServer(config)) { + if (!isHttpServer(config) && !isSseServer(config)) { const stderrStream = (transport as StdioClientTransport).stderr; if (stderrStream) { stderrStream.on('data', (chunk: Buffer) => { @@ -302,6 +310,24 @@ function createHttpTransport( }); } +/** + * Create SSE transport for remote servers + */ +function createSseTransport(config: SseServerConfig): SSEClientTransport { + // SSE transport expects the URL to end with /sse + let urlStr = config.url; + if (!urlStr.endsWith('/sse')) { + urlStr = `${urlStr.replace(/\/$/, '')}/sse`; + } + const url = new URL(urlStr); + + return new SSEClientTransport(url, { + requestInit: { + headers: config.headers, + }, + }); +} + /** * Create stdio transport for local servers * Uses stderr: 'pipe' to capture server output for debugging diff --git a/src/config.ts b/src/config.ts index 99a4e25..dd9108d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,7 +50,20 @@ export interface HttpServerConfig extends BaseServerConfig { timeout?: number; } -export type ServerConfig = StdioServerConfig | HttpServerConfig; +/** + * SSE server configuration (remote) - uses SSE transport + */ +export interface SseServerConfig extends BaseServerConfig { + url: string; + headers?: Record; + timeout?: number; + type: 'sse'; +} + +export type ServerConfig = + | StdioServerConfig + | HttpServerConfig + | SseServerConfig; export interface McpServersConfig { mcpServers: Record; @@ -143,11 +156,18 @@ export function isToolAllowed(toolName: string, config: ServerConfig): boolean { return true; } +/** + * Check if a server config is SSE-based + */ +export function isSseServer(config: ServerConfig): config is SseServerConfig { + return 'url' in config && (config as SseServerConfig).type === 'sse'; +} + /** * Check if a server config is HTTP-based */ export function isHttpServer(config: ServerConfig): config is HttpServerConfig { - return 'url' in config; + return 'url' in config && !isSseServer(config); } /** diff --git a/src/version.ts b/src/version.ts index dfade73..6cbd37a 100644 --- a/src/version.ts +++ b/src/version.ts @@ -2,4 +2,4 @@ * Version constant - single source of truth * This file is auto-updated by scripts/release.sh */ -export const VERSION = '0.3.0'; +export const VERSION = '0.3.1'; diff --git a/tests/config.test.ts b/tests/config.test.ts index a48602c..f2d3f80 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -11,6 +11,7 @@ import { getServerConfig, listServerNames, isHttpServer, + isSseServer, isStdioServer, } from '../src/config'; @@ -42,6 +43,25 @@ describe('config', () => { expect((config.mcpServers.test as any).command).toBe('echo'); }); + test('loads server with sse transport', async () => { + const configPath = join(tempDir, 'sse_config.json'); + await writeFile( + configPath, + JSON.stringify({ + mcpServers: { + sse: { + url: 'http://localhost:3000/sse', + type: 'sse', + }, + }, + }), + ); + + const config = await loadConfig(configPath); + expect(config.mcpServers.sse).toBeDefined(); + expect((config.mcpServers.sse as any).type).toBe('sse'); + }); + test('throws on missing config file', async () => { const configPath = join(tempDir, 'nonexistent.json'); await expect(loadConfig(configPath)).rejects.toThrow('not found'); @@ -233,6 +253,17 @@ describe('config', () => { test('isHttpServer identifies HTTP config', () => { expect(isHttpServer({ url: 'https://example.com' })).toBe(true); expect(isHttpServer({ command: 'echo' })).toBe(false); + expect(isHttpServer({ url: 'https://example.com', type: 'sse' })).toBe( + false, + ); + }); + + test('isSseServer identifies SSE config', () => { + expect(isSseServer({ url: 'https://example.com', type: 'sse' })).toBe( + true, + ); + expect(isSseServer({ url: 'https://example.com' })).toBe(false); + expect(isSseServer({ command: 'echo' })).toBe(false); }); test('isStdioServer identifies stdio config', () => {