@nanocollective/nanocoder
Version:
A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter
263 lines (257 loc) • 10.3 kB
JavaScript
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
import { execFileSync } from 'child_process';
import { accessSync, constants as fsConstants } from 'fs';
import { logWarning } from '../utils/message-queue.js';
/**
* Installation instructions for common MCP server dependencies
*/
const COMMAND_INSTALL_HINTS = {
uvx: `'uvx' is part of the 'uv' Python package manager.
Install uv:
• macOS/Linux: curl -LsSf https://astral.sh/uv/install.sh | sh
• Windows: powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
• pip: pip install uv
• Homebrew: brew install uv
After installation, restart your terminal and try again.`,
npx: `'npx' is part of Node.js.
Install Node.js from: https://nodejs.org/
Or use a version manager like nvm, fnm, or volta.`,
node: `'node' is not installed.
Install Node.js from: https://nodejs.org/
Or use a version manager like nvm, fnm, or volta.`,
python: `'python' is not installed.
Install Python from: https://python.org/downloads/
Or use a version manager like pyenv.`,
python3: `'python3' is not installed.
Install Python from: https://python.org/downloads/
Or use a version manager like pyenv.`,
};
/**
* Checks if a command exists in the system PATH or as an executable path.
* Uses execFileSync with separate arguments to prevent shell injection.
* Handles both PATH lookups and direct path references (./bin/cmd, /usr/bin/cmd).
*/
function commandExists(command) {
// Check if command is a path (contains path separators)
if (command.includes('/') || command.includes('\\')) {
try {
// Check if file exists and is executable
accessSync(command, fsConstants.X_OK);
return true;
}
catch {
return false;
}
}
// PATH lookup using which/where
try {
const checkCmd = process.platform === 'win32' ? 'where' : 'which';
execFileSync(checkCmd, [command], { stdio: 'ignore' });
return true;
}
catch {
return false;
}
}
/**
* Gets installation hint for a missing command
*/
function getInstallHint(command) {
return (COMMAND_INSTALL_HINTS[command] ||
`'${command}' is not installed or not in your PATH.`);
}
/**
* Factory for creating MCP client transports based on server configuration
*/
export class TransportFactory {
/**
* Creates a transport instance for the given MCP server configuration
*/
static createTransport(server) {
switch (server.transport) {
case 'stdio':
return this.createStdioTransport(server);
case 'websocket':
return this.createWebSocketTransport(server);
case 'http':
return this.createHTTPTransport(server);
default: {
const _exhaustiveCheck = server.transport;
throw new Error(`Unsupported transport type: ${_exhaustiveCheck}`);
}
}
}
/**
* Creates a stdio transport for local MCP servers
*/
static createStdioTransport(server) {
if (!server.command) {
throw new Error(`MCP server "${server.name}" missing command for stdio transport`);
}
// For uvx commands, prepend --native-tls to use system certificates
// This fixes TLS issues in corporate proxy environments (issue #272)
let args = server.args || [];
if (server.command === 'uvx' && !args.includes('--native-tls')) {
args = ['--native-tls', ...args];
}
return new StdioClientTransport({
command: server.command,
args,
env: server.env
? { ...process.env, ...server.env }
: undefined,
});
}
/**
* Creates a WebSocket transport for remote MCP servers
*/
static createWebSocketTransport(server) {
if (!server.url) {
throw new Error(`MCP server "${server.name}" missing URL for websocket transport`);
}
const url = new URL(server.url);
// Validate WebSocket URL
if (!url.protocol.startsWith('ws')) {
throw new Error(`Invalid WebSocket URL protocol: ${url.protocol}. Expected ws:// or wss://`);
}
const transport = new WebSocketClientTransport(url);
// Note: The WebSocketClientTransport doesn't directly support headers in the current SDK
// Authentication would need to be handled at the protocol level or via URL parameters
if (server.auth) {
logWarning('WebSocket transport has unsupported auth config', true, {
context: {
serverName: server.name,
transportType: 'websocket',
reason: 'Current SDK does not support headers for WebSocket transport',
},
});
}
return transport;
}
/**
* Creates an HTTP transport for remote MCP servers
*/
static createHTTPTransport(server) {
if (!server.url) {
throw new Error(`MCP server "${server.name}" missing URL for http transport`);
}
const url = new URL(server.url);
// Validate HTTP URL
if (!url.protocol.startsWith('http')) {
throw new Error(`Invalid HTTP URL protocol: ${url.protocol}. Expected http:// or https://`);
}
// Create transport with headers if provided
const transportOptions = server.headers
? { requestInit: { headers: server.headers } }
: undefined;
const transport = new StreamableHTTPClientTransport(url, transportOptions);
if (server.auth) {
logWarning('HTTP transport has unsupported auth config', true, {
context: {
serverName: server.name,
transportType: 'http',
reason: 'Current SDK does not support custom headers for HTTP transport',
},
});
}
// Check if headers are specified but cannot be used
if (server.headers) {
const headerKeys = Object.keys(server.headers);
if (headerKeys.length > 0) {
// Headers are now being used, so don't show the warning anymore
// console.warn(...) - Commented out since headers are now supported
}
}
return transport;
}
/**
* Validates the server configuration for the given transport type
*/
static validateServerConfig(server) {
const errors = [];
switch (server.transport) {
case 'stdio':
if (!server.command) {
errors.push('stdio transport requires a command');
}
else if (!commandExists(server.command)) {
const hint = getInstallHint(server.command);
errors.push(`Command '${server.command}' not found.\n\n${hint}`);
}
break;
case 'websocket':
if (!server.url) {
errors.push('websocket transport requires a URL');
}
else {
try {
const url = new URL(server.url);
if (!url.protocol.startsWith('ws')) {
errors.push('websocket URL must use ws:// or wss:// protocol'); // nosemgrep
}
}
catch {
errors.push('websocket URL is invalid');
}
}
break;
case 'http':
if (!server.url) {
errors.push('http transport requires a URL');
}
else {
try {
const url = new URL(server.url);
if (!url.protocol.startsWith('http')) {
errors.push('http URL must use http:// or https:// protocol');
}
}
catch {
errors.push('http URL is invalid');
}
}
// Headers are now supported, so we don't need to warn about them being ignored
// The actual warning logic has been moved to the createHTTPTransport method
break;
}
return {
valid: errors.length === 0,
errors,
};
}
/**
* Gets transport-specific configuration tips for users
*/
static getTransportTips(transportType) {
switch (transportType) {
case 'stdio':
return [
'Stdio transport spawns a local process',
'Requires a command and optional arguments',
'Environment variables can be passed to the process',
'Best for local MCP servers and tools',
];
case 'websocket':
return [
'WebSocket transport connects to remote MCP servers',
'Requires a ws:// or wss:// URL', // nosemgrep
'Supports real-time bidirectional communication',
'Best for interactive remote services',
'Note: Custom headers are not currently supported by the SDK and will be ignored',
];
case 'http':
return [
'HTTP transport connects to remote MCP servers via REST API',
'Requires an http:// or https:// URL',
'Uses the StreamableHTTP protocol from MCP specification',
'Best for stateless remote services and APIs',
'Custom headers are now supported for authentication',
];
default:
return ['Unknown transport type'];
}
}
}
//# sourceMappingURL=transport-factory.js.map