@coop-tech/mcp-sidecar 0.0.1
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.
- package/README.md +74 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +106 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +33 -0
- package/dist/sidecar.d.ts +41 -0
- package/dist/sidecar.js +229 -0
- package/dist/types.d.ts +47 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# @coop-tech/mcp-sidecar
|
|
2
|
+
|
|
3
|
+
Connect MCP servers to [coop.tech](https://coop.tech) as sidecars.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @coop-tech/mcp-sidecar
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Library usage
|
|
12
|
+
|
|
13
|
+
Connect an in-process MCP server:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
17
|
+
import { connectSidecar } from '@coop-tech/mcp-sidecar'
|
|
18
|
+
|
|
19
|
+
const server = new McpServer({ name: 'my-tools', version: '1.0.0' })
|
|
20
|
+
server.tool('analyze', 'Analyze data', { query: z.string() }, async ({ query }) => {
|
|
21
|
+
return { content: [{ type: 'text', text: 'result' }] }
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
await connectSidecar({
|
|
25
|
+
server,
|
|
26
|
+
token: process.env.COOP_TECH_TOKEN!,
|
|
27
|
+
description: 'Analyzes data',
|
|
28
|
+
})
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CLI usage
|
|
32
|
+
|
|
33
|
+
Spawn an external MCP server and connect it:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Using --command:
|
|
37
|
+
coop-mcp-sidecar --token YOUR_TOKEN --description "My server" --command "python my_server.py"
|
|
38
|
+
|
|
39
|
+
# Using -- separator:
|
|
40
|
+
coop-mcp-sidecar --token YOUR_TOKEN --description "My server" -- python my_server.py
|
|
41
|
+
|
|
42
|
+
# Using env var:
|
|
43
|
+
COOP_TECH_TOKEN=YOUR_TOKEN coop-mcp-sidecar --description "My server" -- python my_server.py
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### CLI options
|
|
47
|
+
|
|
48
|
+
| Option | Description |
|
|
49
|
+
|--------|-------------|
|
|
50
|
+
| `--token <token>` | Personal access token (or `COOP_TECH_TOKEN` env var) |
|
|
51
|
+
| `--url <url>` | Server URL (default: `wss://coop.tech/mcp-sidecar`) |
|
|
52
|
+
| `--description <text>` | Description of the server (required) |
|
|
53
|
+
| `--instructions <text>` | Additional instructions for the agent |
|
|
54
|
+
| `--name <name>` | Override server name |
|
|
55
|
+
| `--version <version>` | Override server version |
|
|
56
|
+
| `--verbose` | Log full tool call arguments and results |
|
|
57
|
+
| `--quiet` | Suppress all logs |
|
|
58
|
+
|
|
59
|
+
## API
|
|
60
|
+
|
|
61
|
+
### `connectSidecar(config): Promise<Sidecar>`
|
|
62
|
+
|
|
63
|
+
Creates and connects a sidecar. Config options:
|
|
64
|
+
|
|
65
|
+
- **`token`** (required) - coop.tech personal access token
|
|
66
|
+
- **`description`** (required) - description of what the server does
|
|
67
|
+
- **`server`** - in-process `McpServer` instance (library mode)
|
|
68
|
+
- **`command`** / **`args`** / **`env`** / **`cwd`** - external MCP server (stdio mode)
|
|
69
|
+
- **`url`** - coop.tech WebSocket URL (default: `wss://coop.tech/mcp-sidecar`)
|
|
70
|
+
- **`name`** / **`version`** - override server name/version
|
|
71
|
+
- **`instructions`** - additional agent instructions
|
|
72
|
+
- **`logLevel`** - `'normal'` | `'verbose'` | `'quiet'`
|
|
73
|
+
- **`reconnect`** - auto-reconnect on disconnect (default: `true`)
|
|
74
|
+
- **`onConnect`** / **`onDisconnect`** / **`onError`** - lifecycle callbacks
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { connectSidecar } from './index.js';
|
|
4
|
+
function printUsage() {
|
|
5
|
+
console.log(`Usage: coop-mcp-sidecar [options] [-- command [args...]]
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--token <token> Personal access token (or set COOP_TECH_TOKEN env var)
|
|
9
|
+
--url <url> Server URL (default: wss://coop.tech/mcp-sidecar)
|
|
10
|
+
--command <cmd> MCP server command to spawn
|
|
11
|
+
--description <description> Description of what this server does (required)
|
|
12
|
+
--instructions <text> Additional instructions for the agent
|
|
13
|
+
--name <name> Override the server name reported to coop.tech
|
|
14
|
+
--version <version> Override the server version reported to coop.tech
|
|
15
|
+
--verbose Log full tool call arguments and results
|
|
16
|
+
--quiet Suppress all tool call and connection logs
|
|
17
|
+
--help Show this help message
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
# Spawn an MCP server via stdio:
|
|
21
|
+
coop-mcp-sidecar --token YOUR_TOKEN --description "Manages database queries" --command "python my_server.py"
|
|
22
|
+
|
|
23
|
+
# Using -- separator for command with arguments:
|
|
24
|
+
coop-mcp-sidecar --token YOUR_TOKEN --description "Code search agent" -- claude mcp serve
|
|
25
|
+
|
|
26
|
+
# Override the server name and version:
|
|
27
|
+
coop-mcp-sidecar --token YOUR_TOKEN --description "My server" --name my-server --version 2.0.0 -- python my_server.py
|
|
28
|
+
|
|
29
|
+
# Using environment variable:
|
|
30
|
+
COOP_TECH_TOKEN=YOUR_TOKEN coop-mcp-sidecar --description "My server" -- python my_server.py`);
|
|
31
|
+
}
|
|
32
|
+
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);
|
|
74
|
+
}
|
|
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(); });
|
|
102
|
+
}
|
|
103
|
+
main().catch((err) => {
|
|
104
|
+
console.error('[coop-sidecar] Fatal error:', err);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { Sidecar } from './sidecar.js';
|
|
2
|
+
export type { SidecarConfig } from './sidecar.js';
|
|
3
|
+
export type { ToolInfo } from './types.js';
|
|
4
|
+
import type { SidecarConfig } from './sidecar.js';
|
|
5
|
+
import { Sidecar } from './sidecar.js';
|
|
6
|
+
/**
|
|
7
|
+
* Connect an MCP server to coop.tech as a sidecar.
|
|
8
|
+
*
|
|
9
|
+
* @example Library mode (in-process McpServer):
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
12
|
+
* import { connectSidecar } from '@coop-tech/mcp-sidecar'
|
|
13
|
+
*
|
|
14
|
+
* const server = new McpServer({ name: 'my-tools', version: '1.0.0' })
|
|
15
|
+
* server.tool('analyze', 'Analyze data', { query: z.string() }, async ({ query }) => { ... })
|
|
16
|
+
*
|
|
17
|
+
* await connectSidecar({
|
|
18
|
+
* server,
|
|
19
|
+
* token: process.env.COOP_TECH_TOKEN!,
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @example Stdio mode (spawn external process):
|
|
24
|
+
* ```ts
|
|
25
|
+
* await connectSidecar({
|
|
26
|
+
* command: 'python',
|
|
27
|
+
* args: ['my_mcp_server.py'],
|
|
28
|
+
* token: process.env.COOP_TECH_TOKEN!,
|
|
29
|
+
* })
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export declare function connectSidecar(config: SidecarConfig): Promise<Sidecar>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export { Sidecar } from './sidecar.js';
|
|
2
|
+
import { Sidecar } from './sidecar.js';
|
|
3
|
+
/**
|
|
4
|
+
* Connect an MCP server to coop.tech as a sidecar.
|
|
5
|
+
*
|
|
6
|
+
* @example Library mode (in-process McpServer):
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
9
|
+
* import { connectSidecar } from '@coop-tech/mcp-sidecar'
|
|
10
|
+
*
|
|
11
|
+
* const server = new McpServer({ name: 'my-tools', version: '1.0.0' })
|
|
12
|
+
* server.tool('analyze', 'Analyze data', { query: z.string() }, async ({ query }) => { ... })
|
|
13
|
+
*
|
|
14
|
+
* await connectSidecar({
|
|
15
|
+
* server,
|
|
16
|
+
* token: process.env.COOP_TECH_TOKEN!,
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example Stdio mode (spawn external process):
|
|
21
|
+
* ```ts
|
|
22
|
+
* await connectSidecar({
|
|
23
|
+
* command: 'python',
|
|
24
|
+
* args: ['my_mcp_server.py'],
|
|
25
|
+
* token: process.env.COOP_TECH_TOKEN!,
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export async function connectSidecar(config) {
|
|
30
|
+
const sidecar = new Sidecar(config);
|
|
31
|
+
await sidecar.connect();
|
|
32
|
+
return sidecar;
|
|
33
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
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
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coop-tech/mcp-sidecar",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Connect MCP servers to coop.tech as sidecars",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"coop-mcp-sidecar": "./dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepublishOnly": "pnpm build"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@modelcontextprotocol/sdk": ">=1.9.0"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ws": "8.19.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "1.27.1",
|
|
26
|
+
"@types/ws": "8.18.1",
|
|
27
|
+
"typescript": "5.9.3"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"sidecar",
|
|
32
|
+
"coop"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT"
|
|
35
|
+
}
|