ai
Version:
AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript
158 lines (130 loc) • 3.82 kB
text/typescript
import type { ChildProcess, IOType } from 'node:child_process';
import { Stream } from 'node:stream';
import {
JSONRPCMessage,
JSONRPCMessageSchema,
} from '../core/tool/mcp/json-rpc-message';
import { MCPTransport } from '../core/tool/mcp/mcp-transport';
import { MCPClientError } from '../errors';
import { createChildProcess } from './create-child-process';
export interface StdioConfig {
command: string;
args?: string[];
env?: Record<string, string>;
stderr?: IOType | Stream | number;
cwd?: string;
}
export class StdioMCPTransport implements MCPTransport {
private process?: ChildProcess;
private abortController: AbortController = new AbortController();
private readBuffer: ReadBuffer = new ReadBuffer();
private serverParams: StdioConfig;
onclose?: () => void;
onerror?: (error: unknown) => void;
onmessage?: (message: JSONRPCMessage) => void;
constructor(server: StdioConfig) {
this.serverParams = server;
}
async start(): Promise<void> {
if (this.process) {
throw new MCPClientError({
message: 'StdioMCPTransport already started.',
});
}
return new Promise(async (resolve, reject) => {
try {
const process = await createChildProcess(
this.serverParams,
this.abortController.signal,
);
this.process = process;
this.process.on('error', error => {
if (error.name === 'AbortError') {
this.onclose?.();
return;
}
reject(error);
this.onerror?.(error);
});
this.process.on('spawn', () => {
resolve();
});
this.process.on('close', _code => {
this.process = undefined;
this.onclose?.();
});
this.process.stdin?.on('error', error => {
this.onerror?.(error);
});
this.process.stdout?.on('data', chunk => {
this.readBuffer.append(chunk);
this.processReadBuffer();
});
this.process.stdout?.on('error', error => {
this.onerror?.(error);
});
} catch (error) {
reject(error);
this.onerror?.(error);
}
});
}
private processReadBuffer() {
while (true) {
try {
const message = this.readBuffer.readMessage();
if (message === null) {
break;
}
this.onmessage?.(message);
} catch (error) {
this.onerror?.(error as Error);
}
}
}
async close(): Promise<void> {
this.abortController.abort();
this.process = undefined;
this.readBuffer.clear();
}
send(message: JSONRPCMessage): Promise<void> {
return new Promise(resolve => {
if (!this.process?.stdin) {
throw new MCPClientError({
message: 'StdioClientTransport not connected',
});
}
const json = serializeMessage(message);
if (this.process.stdin.write(json)) {
resolve();
} else {
this.process.stdin.once('drain', resolve);
}
});
}
}
class ReadBuffer {
private buffer?: Buffer;
append(chunk: Buffer): void {
this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk;
}
readMessage(): JSONRPCMessage | null {
if (!this.buffer) return null;
const index = this.buffer.indexOf('\n');
if (index === -1) {
return null;
}
const line = this.buffer.toString('utf8', 0, index);
this.buffer = this.buffer.subarray(index + 1);
return deserializeMessage(line);
}
clear(): void {
this.buffer = undefined;
}
}
function serializeMessage(message: JSONRPCMessage): string {
return JSON.stringify(message) + '\n';
}
export function deserializeMessage(line: string): JSONRPCMessage {
return JSONRPCMessageSchema.parse(JSON.parse(line));
}