@coop-tech/mcp-sidecar 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ // src/sidecar.ts
2
+ import { createRequire } from "module";
3
+ import WebSocket from "ws";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ var require2 = createRequire(import.meta.url);
8
+ var { version: packageVersion } = require2("../package.json");
9
+ var DEFAULT_URL = "wss://coop.tech/mcp-sidecar";
10
+ var DEFAULT_RECONNECT_INTERVAL_MS = 5e3;
11
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
12
+ function truncate(str, max) {
13
+ if (str.length <= max) return str;
14
+ return str.slice(0, max) + "...";
15
+ }
16
+ var Sidecar = class {
17
+ config;
18
+ logLevel;
19
+ mcpClient = null;
20
+ ws = null;
21
+ heartbeatTimer = null;
22
+ reconnectTimer = null;
23
+ tools = [];
24
+ serverName = "";
25
+ serverVersion = "";
26
+ closed = false;
27
+ constructor(config) {
28
+ if (!config.token) throw new Error("token is required");
29
+ if (!config.server && !config.command) throw new Error("Either server or command is required");
30
+ if (!config.description) throw new Error("description is required");
31
+ this.config = {
32
+ ...config,
33
+ url: config.url ?? DEFAULT_URL,
34
+ reconnect: config.reconnect ?? true,
35
+ reconnectIntervalMs: config.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS,
36
+ heartbeatIntervalMs: config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS
37
+ };
38
+ this.logLevel = config.logLevel ?? "normal";
39
+ }
40
+ async connect() {
41
+ await this.connectMcpServer();
42
+ await this.connectWebSocket();
43
+ }
44
+ async close() {
45
+ this.closed = true;
46
+ this.stopHeartbeat();
47
+ if (this.reconnectTimer) {
48
+ clearTimeout(this.reconnectTimer);
49
+ this.reconnectTimer = null;
50
+ }
51
+ if (this.ws) {
52
+ this.ws.close(1e3, "Client closing");
53
+ this.ws = null;
54
+ }
55
+ if (this.mcpClient) {
56
+ await this.mcpClient.close().catch(() => {
57
+ });
58
+ this.mcpClient = null;
59
+ }
60
+ }
61
+ async connectMcpServer() {
62
+ this.mcpClient = new Client({ name: "coop-sidecar", version: packageVersion });
63
+ if (this.config.server) {
64
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
65
+ await this.config.server.connect(serverTransport);
66
+ await this.mcpClient.connect(clientTransport);
67
+ } else if (this.config.command) {
68
+ const transport = new StdioClientTransport({
69
+ command: this.config.command,
70
+ args: this.config.args,
71
+ env: this.config.env ? { ...process.env, ...this.config.env } : void 0,
72
+ cwd: this.config.cwd
73
+ });
74
+ await this.mcpClient.connect(transport);
75
+ }
76
+ const mcpServerVersion = this.mcpClient.getServerVersion();
77
+ this.serverName = this.config.name ?? mcpServerVersion?.name ?? "";
78
+ this.serverVersion = this.config.version ?? mcpServerVersion?.version ?? "";
79
+ if (!this.serverName) {
80
+ throw new Error("MCP server must return a name in its initialize response (serverInfo.name), or provide a name via config");
81
+ }
82
+ const result = await this.mcpClient.listTools();
83
+ this.tools = result.tools.map((t) => ({
84
+ name: t.name,
85
+ description: t.description,
86
+ inputSchema: t.inputSchema
87
+ }));
88
+ if (this.logLevel !== "quiet") {
89
+ console.log(`[coop-sidecar] Discovered ${this.tools.length} tool(s) from MCP server "${this.serverName}"`);
90
+ }
91
+ }
92
+ connectWebSocket() {
93
+ return new Promise((resolve, reject) => {
94
+ const ws = new WebSocket(this.config.url);
95
+ this.ws = ws;
96
+ let authenticated = false;
97
+ ws.on("open", () => {
98
+ ws.send(JSON.stringify({
99
+ type: "auth",
100
+ token: this.config.token,
101
+ serverName: this.serverName,
102
+ description: this.config.description,
103
+ instructions: this.config.instructions,
104
+ tools: this.tools,
105
+ version: this.serverVersion,
106
+ sdkVersion: packageVersion
107
+ }));
108
+ });
109
+ ws.on("message", (rawData) => {
110
+ let msg;
111
+ try {
112
+ const text = typeof rawData === "string" ? rawData : Buffer.from(rawData).toString();
113
+ msg = JSON.parse(text);
114
+ } catch {
115
+ return;
116
+ }
117
+ if (msg.type === "auth_ok") {
118
+ authenticated = true;
119
+ this.startHeartbeat();
120
+ if (this.logLevel !== "quiet") {
121
+ console.log(`[coop-sidecar] Connected as "${this.serverName}" (id: ${msg.serverId})`);
122
+ }
123
+ this.config.onConnect?.();
124
+ resolve();
125
+ return;
126
+ }
127
+ if (msg.type === "auth_error") {
128
+ const err = new Error(`Authentication failed: ${msg.message}`);
129
+ this.config.onError?.(err);
130
+ reject(err);
131
+ return;
132
+ }
133
+ if (msg.type === "tool_call") {
134
+ void this.handleToolCall(msg);
135
+ return;
136
+ }
137
+ if (msg.type === "pong") {
138
+ return;
139
+ }
140
+ });
141
+ ws.on("close", () => {
142
+ this.stopHeartbeat();
143
+ this.config.onDisconnect?.();
144
+ if (!this.closed && this.config.reconnect && authenticated) {
145
+ if (this.logLevel !== "quiet") {
146
+ console.log(`[coop-sidecar] Disconnected. Reconnecting in ${this.config.reconnectIntervalMs}ms...`);
147
+ }
148
+ this.reconnectTimer = setTimeout(() => {
149
+ this.connectWebSocket().catch((err) => {
150
+ this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
151
+ });
152
+ }, this.config.reconnectIntervalMs);
153
+ }
154
+ });
155
+ ws.on("error", (err) => {
156
+ this.config.onError?.(err);
157
+ if (!authenticated) {
158
+ reject(err);
159
+ }
160
+ });
161
+ });
162
+ }
163
+ async handleToolCall(msg) {
164
+ if (!this.mcpClient || !this.ws) return;
165
+ const maxLen = this.logLevel === "verbose" ? Infinity : 100;
166
+ if (this.logLevel !== "quiet") {
167
+ const argsStr = truncate(JSON.stringify(msg.arguments), maxLen);
168
+ console.log(`[coop-sidecar] Tool call: ${msg.name} args=${argsStr}`);
169
+ }
170
+ try {
171
+ const result = await this.mcpClient.callTool({
172
+ name: msg.name,
173
+ arguments: msg.arguments
174
+ });
175
+ const content = Array.isArray(result.content) ? result.content : [{ type: "text", text: String(result.content) }];
176
+ if (this.logLevel !== "quiet") {
177
+ const contentStr = truncate(JSON.stringify(content), maxLen);
178
+ console.log(`[coop-sidecar] Tool result: ${msg.name} isError=${Boolean(result.isError)} content=${contentStr}`);
179
+ }
180
+ this.ws.send(JSON.stringify({
181
+ type: "tool_result",
182
+ requestId: msg.requestId,
183
+ result: { content, isError: result.isError }
184
+ }));
185
+ } catch (error) {
186
+ const errorMessage = error instanceof Error ? error.message : String(error);
187
+ if (this.logLevel !== "quiet") {
188
+ console.log(`[coop-sidecar] Tool error: ${msg.name} ${errorMessage}`);
189
+ }
190
+ this.ws?.send(JSON.stringify({
191
+ type: "tool_result",
192
+ requestId: msg.requestId,
193
+ result: {
194
+ content: [{ type: "text", text: `Error: ${errorMessage}` }],
195
+ isError: true
196
+ }
197
+ }));
198
+ }
199
+ }
200
+ startHeartbeat() {
201
+ this.stopHeartbeat();
202
+ this.heartbeatTimer = setInterval(() => {
203
+ if (this.ws?.readyState === WebSocket.OPEN) {
204
+ this.ws.send(JSON.stringify({ type: "ping" }));
205
+ }
206
+ }, this.config.heartbeatIntervalMs);
207
+ }
208
+ stopHeartbeat() {
209
+ if (this.heartbeatTimer) {
210
+ clearInterval(this.heartbeatTimer);
211
+ this.heartbeatTimer = null;
212
+ }
213
+ }
214
+ };
215
+
216
+ // src/index.ts
217
+ async function connectSidecar(config) {
218
+ const sidecar = new Sidecar(config);
219
+ await sidecar.connect();
220
+ return sidecar;
221
+ }
222
+
223
+ export {
224
+ Sidecar,
225
+ connectSidecar
226
+ };
package/dist/cli.d.ts CHANGED
@@ -1,2 +1 @@
1
1
  #!/usr/bin/env node
2
- export {};
package/dist/cli.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from 'node:util';
3
- import { connectSidecar } from './index';
2
+ import {
3
+ connectSidecar
4
+ } from "./chunk-57A7H4EJ.js";
5
+
6
+ // src/cli.ts
7
+ import { parseArgs } from "util";
4
8
  function printUsage() {
5
- console.log(`Usage: coop-mcp-sidecar [options] [-- command [args...]]
9
+ console.log(`Usage: coop-mcp-sidecar [options] [-- command [args...]]
6
10
 
7
11
  Options:
8
12
  --token <token> Personal access token (or set COOP_TECH_TOKEN env var)
@@ -30,77 +34,80 @@ Examples:
30
34
  COOP_TECH_TOKEN=YOUR_TOKEN coop-mcp-sidecar --description "My server" -- python my_server.py`);
31
35
  }
32
36
  async function main() {
33
- const { values, positionals } = parseArgs({
34
- allowPositionals: true,
35
- options: {
36
- token: { type: 'string' },
37
- url: { type: 'string' },
38
- command: { type: 'string' },
39
- description: { type: 'string' },
40
- instructions: { type: 'string' },
41
- name: { type: 'string' },
42
- version: { type: 'string' },
43
- verbose: { type: 'boolean' },
44
- quiet: { type: 'boolean' },
45
- help: { type: 'boolean', short: 'h' },
46
- },
47
- });
48
- if (values.help) {
49
- printUsage();
50
- process.exit(0);
51
- }
52
- const token = values.token ?? process.env.COOP_TECH_TOKEN;
53
- const url = values.url;
54
- let command = values.command;
55
- let commandArgs = [];
56
- if (positionals.length > 0) {
57
- command = positionals[0];
58
- commandArgs = positionals.slice(1);
59
- }
60
- if (!token) {
61
- console.error('Error: --token is required (or set COOP_TECH_TOKEN environment variable)');
62
- process.exit(1);
63
- }
64
- if (!command) {
65
- console.error('Error: --command is required (or use -- separator)');
66
- printUsage();
67
- process.exit(1);
68
- }
69
- const description = values.description;
70
- if (!description) {
71
- console.error('Error: --description is required');
72
- printUsage();
73
- process.exit(1);
37
+ const { values, positionals } = parseArgs({
38
+ allowPositionals: true,
39
+ options: {
40
+ token: { type: "string" },
41
+ url: { type: "string" },
42
+ command: { type: "string" },
43
+ description: { type: "string" },
44
+ instructions: { type: "string" },
45
+ name: { type: "string" },
46
+ version: { type: "string" },
47
+ verbose: { type: "boolean" },
48
+ quiet: { type: "boolean" },
49
+ help: { type: "boolean", short: "h" }
74
50
  }
75
- const logLevel = values.quiet ? 'quiet' : values.verbose ? 'verbose' : 'normal';
76
- if (logLevel !== 'quiet') {
77
- console.log(`[coop-sidecar] Starting MCP server: ${command} ${commandArgs.join(' ')}`);
78
- }
79
- const sidecar = await connectSidecar({
80
- token,
81
- url,
82
- command,
83
- args: commandArgs,
84
- description,
85
- instructions: values.instructions,
86
- name: values.name,
87
- version: values.version,
88
- logLevel,
89
- reconnect: true,
90
- onConnect: () => console.log('[coop-sidecar] Connected to coop.tech'),
91
- onDisconnect: () => console.log('[coop-sidecar] Disconnected from coop.tech'),
92
- onError: (err) => console.error('[coop-sidecar] Error:', err.message),
93
- });
94
- // Handle graceful shutdown
95
- const shutdown = async () => {
96
- console.log('\n[coop-sidecar] Shutting down...');
97
- await sidecar.close();
98
- process.exit(0);
99
- };
100
- process.on('SIGINT', () => { void shutdown(); });
101
- process.on('SIGTERM', () => { void shutdown(); });
51
+ });
52
+ if (values.help) {
53
+ printUsage();
54
+ process.exit(0);
55
+ }
56
+ const token = values.token ?? process.env.COOP_TECH_TOKEN;
57
+ const url = values.url;
58
+ let command = values.command;
59
+ let commandArgs = [];
60
+ if (positionals.length > 0) {
61
+ command = positionals[0];
62
+ commandArgs = positionals.slice(1);
63
+ }
64
+ if (!token) {
65
+ console.error("Error: --token is required (or set COOP_TECH_TOKEN environment variable)");
66
+ process.exit(1);
67
+ }
68
+ if (!command) {
69
+ console.error("Error: --command is required (or use -- separator)");
70
+ printUsage();
71
+ process.exit(1);
72
+ }
73
+ const description = values.description;
74
+ if (!description) {
75
+ console.error("Error: --description is required");
76
+ printUsage();
77
+ process.exit(1);
78
+ }
79
+ const logLevel = values.quiet ? "quiet" : values.verbose ? "verbose" : "normal";
80
+ if (logLevel !== "quiet") {
81
+ console.log(`[coop-sidecar] Starting MCP server: ${command} ${commandArgs.join(" ")}`);
82
+ }
83
+ const sidecar = await connectSidecar({
84
+ token,
85
+ url,
86
+ command,
87
+ args: commandArgs,
88
+ description,
89
+ instructions: values.instructions,
90
+ name: values.name,
91
+ version: values.version,
92
+ logLevel,
93
+ reconnect: true,
94
+ onConnect: () => console.log("[coop-sidecar] Connected to coop.tech"),
95
+ onDisconnect: () => console.log("[coop-sidecar] Disconnected from coop.tech"),
96
+ onError: (err) => console.error("[coop-sidecar] Error:", err.message)
97
+ });
98
+ const shutdown = async () => {
99
+ console.log("\n[coop-sidecar] Shutting down...");
100
+ await sidecar.close();
101
+ process.exit(0);
102
+ };
103
+ process.on("SIGINT", () => {
104
+ void shutdown();
105
+ });
106
+ process.on("SIGTERM", () => {
107
+ void shutdown();
108
+ });
102
109
  }
103
110
  main().catch((err) => {
104
- console.error('[coop-sidecar] Fatal error:', err);
105
- process.exit(1);
111
+ console.error("[coop-sidecar] Fatal error:", err);
112
+ process.exit(1);
106
113
  });
package/dist/index.d.ts CHANGED
@@ -1,8 +1,94 @@
1
- export { Sidecar } from './sidecar';
2
- export type { SidecarConfig } from './sidecar';
3
- export * from './types';
4
- import type { SidecarConfig } from './sidecar';
5
- import { Sidecar } from './sidecar';
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ interface SidecarConfig {
4
+ token: string;
5
+ url?: string;
6
+ server?: McpServer;
7
+ command?: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ cwd?: string;
11
+ description: string;
12
+ name?: string;
13
+ version?: string;
14
+ instructions?: string;
15
+ logLevel?: 'normal' | 'verbose' | 'quiet';
16
+ reconnect?: boolean;
17
+ reconnectIntervalMs?: number;
18
+ heartbeatIntervalMs?: number;
19
+ onConnect?: () => void;
20
+ onDisconnect?: () => void;
21
+ onError?: (error: Error) => void;
22
+ }
23
+ declare class Sidecar {
24
+ private config;
25
+ private logLevel;
26
+ private mcpClient;
27
+ private ws;
28
+ private heartbeatTimer;
29
+ private reconnectTimer;
30
+ private tools;
31
+ private serverName;
32
+ private serverVersion;
33
+ private closed;
34
+ constructor(config: SidecarConfig);
35
+ connect(): Promise<void>;
36
+ close(): Promise<void>;
37
+ private connectMcpServer;
38
+ private connectWebSocket;
39
+ private handleToolCall;
40
+ private startHeartbeat;
41
+ private stopHeartbeat;
42
+ }
43
+
44
+ interface AuthMessage {
45
+ type: 'auth';
46
+ token: string;
47
+ serverName: string;
48
+ description: string;
49
+ instructions?: string;
50
+ tools: ToolInfo[];
51
+ version: string;
52
+ sdkVersion: string;
53
+ }
54
+ interface ToolResultMessage {
55
+ type: 'tool_result';
56
+ requestId: string;
57
+ result: {
58
+ content: Array<{
59
+ type: string;
60
+ text: string;
61
+ }>;
62
+ isError?: boolean;
63
+ };
64
+ }
65
+ interface PingMessage {
66
+ type: 'ping';
67
+ }
68
+ interface AuthOkMessage {
69
+ type: 'auth_ok';
70
+ serverId: string;
71
+ }
72
+ interface AuthErrorMessage {
73
+ type: 'auth_error';
74
+ message: string;
75
+ }
76
+ interface ToolCallMessage {
77
+ type: 'tool_call';
78
+ requestId: string;
79
+ name: string;
80
+ arguments: Record<string, unknown>;
81
+ }
82
+ interface PongMessage {
83
+ type: 'pong';
84
+ }
85
+ interface ToolInfo {
86
+ name: string;
87
+ description?: string;
88
+ inputSchema: Record<string, unknown>;
89
+ }
90
+ type ServerMessage = AuthOkMessage | AuthErrorMessage | ToolCallMessage | PongMessage;
91
+
6
92
  /**
7
93
  * Connect an MCP server to coop.tech as a sidecar.
8
94
  *
@@ -29,4 +115,6 @@ import { Sidecar } from './sidecar';
29
115
  * })
30
116
  * ```
31
117
  */
32
- export declare function connectSidecar(config: SidecarConfig): Promise<Sidecar>;
118
+ declare function connectSidecar(config: SidecarConfig): Promise<Sidecar>;
119
+
120
+ export { type AuthErrorMessage, type AuthMessage, type AuthOkMessage, type PingMessage, type PongMessage, type ServerMessage, Sidecar, type SidecarConfig, type ToolCallMessage, type ToolInfo, type ToolResultMessage, connectSidecar };
package/dist/index.js CHANGED
@@ -1,34 +1,8 @@
1
- export { Sidecar } from './sidecar';
2
- export * from './types';
3
- import { Sidecar } from './sidecar';
4
- /**
5
- * Connect an MCP server to coop.tech as a sidecar.
6
- *
7
- * @example Library mode (in-process McpServer):
8
- * ```ts
9
- * import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
10
- * import { connectSidecar } from '@coop-tech/mcp-sidecar'
11
- *
12
- * const server = new McpServer({ name: 'my-tools', version: '1.0.0' })
13
- * server.tool('analyze', 'Analyze data', { query: z.string() }, async ({ query }) => { ... })
14
- *
15
- * await connectSidecar({
16
- * server,
17
- * token: process.env.COOP_TECH_TOKEN!,
18
- * })
19
- * ```
20
- *
21
- * @example Stdio mode (spawn external process):
22
- * ```ts
23
- * await connectSidecar({
24
- * command: 'python',
25
- * args: ['my_mcp_server.py'],
26
- * token: process.env.COOP_TECH_TOKEN!,
27
- * })
28
- * ```
29
- */
30
- export async function connectSidecar(config) {
31
- const sidecar = new Sidecar(config);
32
- await sidecar.connect();
33
- return sidecar;
34
- }
1
+ import {
2
+ Sidecar,
3
+ connectSidecar
4
+ } from "./chunk-57A7H4EJ.js";
5
+ export {
6
+ Sidecar,
7
+ connectSidecar
8
+ };
package/package.json CHANGED
@@ -1,10 +1,16 @@
1
1
  {
2
2
  "name": "@coop-tech/mcp-sidecar",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Connect MCP servers to coop.tech as sidecars",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
8
14
  "bin": {
9
15
  "coop-mcp-sidecar": "dist/cli.js"
10
16
  },
@@ -12,7 +18,7 @@
12
18
  "dist"
13
19
  ],
14
20
  "scripts": {
15
- "build": "tsc",
21
+ "build": "tsup",
16
22
  "prepublishOnly": "pnpm build"
17
23
  },
18
24
  "peerDependencies": {
@@ -24,6 +30,7 @@
24
30
  "devDependencies": {
25
31
  "@modelcontextprotocol/sdk": "1.27.1",
26
32
  "@types/ws": "8.18.1",
33
+ "tsup": "8.5.1",
27
34
  "typescript": "5.9.3"
28
35
  },
29
36
  "keywords": [
package/dist/sidecar.d.ts DELETED
@@ -1,41 +0,0 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export interface SidecarConfig {
3
- token: string;
4
- url?: string;
5
- server?: McpServer;
6
- command?: string;
7
- args?: string[];
8
- env?: Record<string, string>;
9
- cwd?: string;
10
- description: string;
11
- name?: string;
12
- version?: string;
13
- instructions?: string;
14
- logLevel?: 'normal' | 'verbose' | 'quiet';
15
- reconnect?: boolean;
16
- reconnectIntervalMs?: number;
17
- heartbeatIntervalMs?: number;
18
- onConnect?: () => void;
19
- onDisconnect?: () => void;
20
- onError?: (error: Error) => void;
21
- }
22
- export declare class Sidecar {
23
- private config;
24
- private logLevel;
25
- private mcpClient;
26
- private ws;
27
- private heartbeatTimer;
28
- private reconnectTimer;
29
- private tools;
30
- private serverName;
31
- private serverVersion;
32
- private closed;
33
- constructor(config: SidecarConfig);
34
- connect(): Promise<void>;
35
- close(): Promise<void>;
36
- private connectMcpServer;
37
- private connectWebSocket;
38
- private handleToolCall;
39
- private startHeartbeat;
40
- private stopHeartbeat;
41
- }
package/dist/sidecar.js DELETED
@@ -1,229 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import WebSocket from 'ws';
3
- import { Client } from '@modelcontextprotocol/sdk/client/index.js';
4
- import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
5
- import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
- const require = createRequire(import.meta.url);
7
- const { version: packageVersion } = require('../package.json');
8
- const DEFAULT_URL = 'wss://coop.tech/mcp-sidecar';
9
- const DEFAULT_RECONNECT_INTERVAL_MS = 5000;
10
- const DEFAULT_HEARTBEAT_INTERVAL_MS = 30000;
11
- function truncate(str, max) {
12
- if (str.length <= max)
13
- return str;
14
- return str.slice(0, max) + '...';
15
- }
16
- export class Sidecar {
17
- config;
18
- logLevel;
19
- mcpClient = null;
20
- ws = null;
21
- heartbeatTimer = null;
22
- reconnectTimer = null;
23
- tools = [];
24
- serverName = '';
25
- serverVersion = '';
26
- closed = false;
27
- constructor(config) {
28
- if (!config.token)
29
- throw new Error('token is required');
30
- if (!config.server && !config.command)
31
- throw new Error('Either server or command is required');
32
- if (!config.description)
33
- throw new Error('description is required');
34
- this.config = {
35
- ...config,
36
- url: config.url ?? DEFAULT_URL,
37
- reconnect: config.reconnect ?? true,
38
- reconnectIntervalMs: config.reconnectIntervalMs ?? DEFAULT_RECONNECT_INTERVAL_MS,
39
- heartbeatIntervalMs: config.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
40
- };
41
- this.logLevel = config.logLevel ?? 'normal';
42
- }
43
- async connect() {
44
- // Step 1: Connect to MCP server and discover tools
45
- await this.connectMcpServer();
46
- // Step 2: Connect to coop.tech WebSocket
47
- await this.connectWebSocket();
48
- }
49
- async close() {
50
- this.closed = true;
51
- this.stopHeartbeat();
52
- if (this.reconnectTimer) {
53
- clearTimeout(this.reconnectTimer);
54
- this.reconnectTimer = null;
55
- }
56
- if (this.ws) {
57
- this.ws.close(1000, 'Client closing');
58
- this.ws = null;
59
- }
60
- if (this.mcpClient) {
61
- await this.mcpClient.close().catch(() => { });
62
- this.mcpClient = null;
63
- }
64
- }
65
- async connectMcpServer() {
66
- this.mcpClient = new Client({ name: 'coop-sidecar', version: packageVersion });
67
- if (this.config.server) {
68
- // Library mode: in-process McpServer
69
- const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
70
- await this.config.server.connect(serverTransport);
71
- await this.mcpClient.connect(clientTransport);
72
- }
73
- else if (this.config.command) {
74
- // Stdio mode: spawn external process
75
- const transport = new StdioClientTransport({
76
- command: this.config.command,
77
- args: this.config.args,
78
- env: this.config.env ? { ...process.env, ...this.config.env } : undefined,
79
- cwd: this.config.cwd,
80
- });
81
- await this.mcpClient.connect(transport);
82
- }
83
- const mcpServerVersion = this.mcpClient.getServerVersion();
84
- // Use config overrides or fall back to MCP server values
85
- this.serverName = this.config.name ?? mcpServerVersion?.name ?? '';
86
- this.serverVersion = this.config.version ?? mcpServerVersion?.version ?? '';
87
- if (!this.serverName) {
88
- throw new Error('MCP server must return a name in its initialize response (serverInfo.name), or provide a name via config');
89
- }
90
- // Discover tools
91
- const result = await this.mcpClient.listTools();
92
- this.tools = result.tools.map((t) => ({
93
- name: t.name,
94
- description: t.description,
95
- inputSchema: t.inputSchema,
96
- }));
97
- if (this.logLevel !== 'quiet') {
98
- console.log(`[coop-sidecar] Discovered ${this.tools.length} tool(s) from MCP server "${this.serverName}"`);
99
- }
100
- }
101
- connectWebSocket() {
102
- return new Promise((resolve, reject) => {
103
- const ws = new WebSocket(this.config.url);
104
- this.ws = ws;
105
- let authenticated = false;
106
- ws.on('open', () => {
107
- // Send auth message
108
- ws.send(JSON.stringify({
109
- type: 'auth',
110
- token: this.config.token,
111
- serverName: this.serverName,
112
- description: this.config.description,
113
- instructions: this.config.instructions,
114
- tools: this.tools,
115
- version: this.serverVersion,
116
- sdkVersion: packageVersion,
117
- }));
118
- });
119
- ws.on('message', (rawData) => {
120
- let msg;
121
- try {
122
- const text = typeof rawData === 'string' ? rawData : Buffer.from(rawData).toString();
123
- msg = JSON.parse(text);
124
- }
125
- catch {
126
- return;
127
- }
128
- if (msg.type === 'auth_ok') {
129
- authenticated = true;
130
- this.startHeartbeat();
131
- if (this.logLevel !== 'quiet') {
132
- console.log(`[coop-sidecar] Connected as "${this.serverName}" (id: ${msg.serverId})`);
133
- }
134
- this.config.onConnect?.();
135
- resolve();
136
- return;
137
- }
138
- if (msg.type === 'auth_error') {
139
- const err = new Error(`Authentication failed: ${msg.message}`);
140
- this.config.onError?.(err);
141
- reject(err);
142
- return;
143
- }
144
- if (msg.type === 'tool_call') {
145
- void this.handleToolCall(msg);
146
- return;
147
- }
148
- if (msg.type === 'pong') {
149
- return;
150
- }
151
- });
152
- ws.on('close', () => {
153
- this.stopHeartbeat();
154
- this.config.onDisconnect?.();
155
- if (!this.closed && this.config.reconnect && authenticated) {
156
- if (this.logLevel !== 'quiet') {
157
- console.log(`[coop-sidecar] Disconnected. Reconnecting in ${this.config.reconnectIntervalMs}ms...`);
158
- }
159
- this.reconnectTimer = setTimeout(() => {
160
- this.connectWebSocket().catch((err) => {
161
- this.config.onError?.(err instanceof Error ? err : new Error(String(err)));
162
- });
163
- }, this.config.reconnectIntervalMs);
164
- }
165
- });
166
- ws.on('error', (err) => {
167
- this.config.onError?.(err);
168
- if (!authenticated) {
169
- reject(err);
170
- }
171
- });
172
- });
173
- }
174
- async handleToolCall(msg) {
175
- if (!this.mcpClient || !this.ws)
176
- return;
177
- const maxLen = this.logLevel === 'verbose' ? Infinity : 100;
178
- if (this.logLevel !== 'quiet') {
179
- const argsStr = truncate(JSON.stringify(msg.arguments), maxLen);
180
- console.log(`[coop-sidecar] Tool call: ${msg.name} args=${argsStr}`);
181
- }
182
- try {
183
- const result = await this.mcpClient.callTool({
184
- name: msg.name,
185
- arguments: msg.arguments,
186
- });
187
- const content = Array.isArray(result.content)
188
- ? result.content
189
- : [{ type: 'text', text: String(result.content) }];
190
- if (this.logLevel !== 'quiet') {
191
- const contentStr = truncate(JSON.stringify(content), maxLen);
192
- console.log(`[coop-sidecar] Tool result: ${msg.name} isError=${Boolean(result.isError)} content=${contentStr}`);
193
- }
194
- this.ws.send(JSON.stringify({
195
- type: 'tool_result',
196
- requestId: msg.requestId,
197
- result: { content, isError: result.isError },
198
- }));
199
- }
200
- catch (error) {
201
- const errorMessage = error instanceof Error ? error.message : String(error);
202
- if (this.logLevel !== 'quiet') {
203
- console.log(`[coop-sidecar] Tool error: ${msg.name} ${errorMessage}`);
204
- }
205
- this.ws?.send(JSON.stringify({
206
- type: 'tool_result',
207
- requestId: msg.requestId,
208
- result: {
209
- content: [{ type: 'text', text: `Error: ${errorMessage}` }],
210
- isError: true,
211
- },
212
- }));
213
- }
214
- }
215
- startHeartbeat() {
216
- this.stopHeartbeat();
217
- this.heartbeatTimer = setInterval(() => {
218
- if (this.ws?.readyState === WebSocket.OPEN) {
219
- this.ws.send(JSON.stringify({ type: 'ping' }));
220
- }
221
- }, this.config.heartbeatIntervalMs);
222
- }
223
- stopHeartbeat() {
224
- if (this.heartbeatTimer) {
225
- clearInterval(this.heartbeatTimer);
226
- this.heartbeatTimer = null;
227
- }
228
- }
229
- }
package/dist/types.d.ts DELETED
@@ -1,47 +0,0 @@
1
- export interface AuthMessage {
2
- type: 'auth';
3
- token: string;
4
- serverName: string;
5
- description: string;
6
- instructions?: string;
7
- tools: ToolInfo[];
8
- version: string;
9
- sdkVersion: string;
10
- }
11
- export interface ToolResultMessage {
12
- type: 'tool_result';
13
- requestId: string;
14
- result: {
15
- content: Array<{
16
- type: string;
17
- text: string;
18
- }>;
19
- isError?: boolean;
20
- };
21
- }
22
- export interface PingMessage {
23
- type: 'ping';
24
- }
25
- export interface AuthOkMessage {
26
- type: 'auth_ok';
27
- serverId: string;
28
- }
29
- export interface AuthErrorMessage {
30
- type: 'auth_error';
31
- message: string;
32
- }
33
- export interface ToolCallMessage {
34
- type: 'tool_call';
35
- requestId: string;
36
- name: string;
37
- arguments: Record<string, unknown>;
38
- }
39
- export interface PongMessage {
40
- type: 'pong';
41
- }
42
- export interface ToolInfo {
43
- name: string;
44
- description?: string;
45
- inputSchema: Record<string, unknown>;
46
- }
47
- export type ServerMessage = AuthOkMessage | AuthErrorMessage | ToolCallMessage | PongMessage;
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};