mcp-compression-proxy 1.0.0
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/CHANGELOG.md +97 -0
- package/LICENSE +21 -0
- package/README.md +842 -0
- package/dist/cli/commands.d.ts +25 -0
- package/dist/cli/commands.js +152 -0
- package/dist/cli/daemon.d.ts +4 -0
- package/dist/cli/daemon.js +336 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +269 -0
- package/dist/cli/ipc-client.d.ts +11 -0
- package/dist/cli/ipc-client.js +81 -0
- package/dist/cli/payload-interceptor.d.ts +6 -0
- package/dist/cli/payload-interceptor.js +49 -0
- package/dist/config/loader.d.ts +53 -0
- package/dist/config/loader.js +332 -0
- package/dist/config/schema.d.ts +164 -0
- package/dist/config/schema.js +127 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +821 -0
- package/dist/mcp/client-manager.d.ts +65 -0
- package/dist/mcp/client-manager.js +197 -0
- package/dist/services/compression-cache.d.ts +112 -0
- package/dist/services/compression-cache.js +238 -0
- package/dist/services/compression-persistence.d.ts +36 -0
- package/dist/services/compression-persistence.js +111 -0
- package/dist/services/compression-sampler.d.ts +89 -0
- package/dist/services/compression-sampler.js +171 -0
- package/dist/services/session-manager.d.ts +64 -0
- package/dist/services/session-manager.js +160 -0
- package/dist/services/stats-service.d.ts +101 -0
- package/dist/services/stats-service.js +246 -0
- package/dist/types/compression.d.ts +38 -0
- package/dist/types/compression.js +5 -0
- package/dist/types/index.d.ts +108 -0
- package/dist/types/index.js +2 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.js +11 -0
- package/package.json +110 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mcp-cli tools — list all available tools with compressed descriptions
|
|
3
|
+
*/
|
|
4
|
+
export declare function handleTools(socketPath: string): Promise<void>;
|
|
5
|
+
/**
|
|
6
|
+
* mcp-cli search <query> — search tools by name or description
|
|
7
|
+
*/
|
|
8
|
+
export declare function handleSearch(socketPath: string, query: string): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* mcp-cli info <server>/<tool> — get full schema for a single tool
|
|
11
|
+
*/
|
|
12
|
+
export declare function handleInfo(socketPath: string, serverTool: string): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* mcp-cli call <server>/<tool> '<json>' — execute a tool
|
|
15
|
+
*/
|
|
16
|
+
export declare function handleCall(socketPath: string, serverTool: string, jsonPayload: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* mcp-cli stats — get compression/server statistics
|
|
19
|
+
*/
|
|
20
|
+
export declare function handleStats(socketPath: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* mcp-cli daemon status — show daemon status
|
|
23
|
+
*/
|
|
24
|
+
export declare function handleDaemonStatus(socketPath: string): Promise<void>;
|
|
25
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { sendRequest, isDaemonRunning } from './ipc-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format tool entries as aligned plain text:
|
|
4
|
+
* server/tool_name Short description
|
|
5
|
+
*/
|
|
6
|
+
function formatToolList(tools) {
|
|
7
|
+
if (tools.length === 0)
|
|
8
|
+
return 'No tools found.';
|
|
9
|
+
// Calculate column width for alignment
|
|
10
|
+
const names = tools.map(t => `${t.server}/${t.tool}`);
|
|
11
|
+
const maxLen = Math.min(Math.max(...names.map(n => n.length)), 40);
|
|
12
|
+
const lines = tools.map(t => {
|
|
13
|
+
const name = `${t.server}/${t.tool}`;
|
|
14
|
+
const padded = name.padEnd(maxLen + 4);
|
|
15
|
+
return `${padded}${t.description}`;
|
|
16
|
+
});
|
|
17
|
+
return lines.join('\n');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* mcp-cli tools — list all available tools with compressed descriptions
|
|
21
|
+
*/
|
|
22
|
+
export async function handleTools(socketPath) {
|
|
23
|
+
const response = await sendRequest(socketPath, 'tools');
|
|
24
|
+
if (response.error) {
|
|
25
|
+
console.error(`Error: ${response.error.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const result = response.result;
|
|
29
|
+
console.log(formatToolList(result.tools));
|
|
30
|
+
// Summary line
|
|
31
|
+
const serverCount = new Set(result.tools.map(t => t.server)).size;
|
|
32
|
+
console.log(`\n(${result.count} tools across ${serverCount} servers)`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* mcp-cli search <query> — search tools by name or description
|
|
36
|
+
*/
|
|
37
|
+
export async function handleSearch(socketPath, query) {
|
|
38
|
+
if (!query) {
|
|
39
|
+
console.error('Usage: mcp-cli search <query>');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const response = await sendRequest(socketPath, 'search', { query });
|
|
43
|
+
if (response.error) {
|
|
44
|
+
console.error(`Error: ${response.error.message}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const result = response.result;
|
|
48
|
+
if (result.count === 0) {
|
|
49
|
+
console.log(`No tools matching "${query}".`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log(formatToolList(result.tools));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* mcp-cli info <server>/<tool> — get full schema for a single tool
|
|
56
|
+
*/
|
|
57
|
+
export async function handleInfo(socketPath, serverTool) {
|
|
58
|
+
const slashIndex = serverTool.indexOf('/');
|
|
59
|
+
if (slashIndex === -1) {
|
|
60
|
+
console.error('Usage: mcp-cli info <server>/<tool>');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
const server = serverTool.slice(0, slashIndex);
|
|
64
|
+
const tool = serverTool.slice(slashIndex + 1);
|
|
65
|
+
const response = await sendRequest(socketPath, 'info', { server, tool });
|
|
66
|
+
if (response.error) {
|
|
67
|
+
console.error(`Error: ${response.error.message}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
const result = response.result;
|
|
71
|
+
console.log(JSON.stringify(result, null, 2));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* mcp-cli call <server>/<tool> '<json>' — execute a tool
|
|
75
|
+
*/
|
|
76
|
+
export async function handleCall(socketPath, serverTool, jsonPayload) {
|
|
77
|
+
const slashIndex = serverTool.indexOf('/');
|
|
78
|
+
if (slashIndex === -1) {
|
|
79
|
+
console.error('Usage: mcp-cli call <server>/<tool> \'<json_payload>\'');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const server = serverTool.slice(0, slashIndex);
|
|
83
|
+
const tool = serverTool.slice(slashIndex + 1);
|
|
84
|
+
let args = {};
|
|
85
|
+
if (jsonPayload) {
|
|
86
|
+
try {
|
|
87
|
+
args = JSON.parse(jsonPayload);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
console.error('Error: Invalid JSON payload');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const response = await sendRequest(socketPath, 'call', {
|
|
95
|
+
server,
|
|
96
|
+
tool,
|
|
97
|
+
arguments: args,
|
|
98
|
+
});
|
|
99
|
+
if (response.error) {
|
|
100
|
+
console.error(`Error: ${response.error.message}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
const result = response.result;
|
|
104
|
+
if (result.isError) {
|
|
105
|
+
console.error(result.output);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
console.log(result.output);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* mcp-cli stats — get compression/server statistics
|
|
112
|
+
*/
|
|
113
|
+
export async function handleStats(socketPath) {
|
|
114
|
+
const response = await sendRequest(socketPath, 'stats');
|
|
115
|
+
if (response.error) {
|
|
116
|
+
console.error(`Error: ${response.error.message}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.log(JSON.stringify(response.result, null, 2));
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* mcp-cli daemon status — show daemon status
|
|
123
|
+
*/
|
|
124
|
+
export async function handleDaemonStatus(socketPath) {
|
|
125
|
+
const running = await isDaemonRunning(socketPath);
|
|
126
|
+
if (!running) {
|
|
127
|
+
console.log('Daemon is not running.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const response = await sendRequest(socketPath, 'daemon-status');
|
|
131
|
+
if (response.error) {
|
|
132
|
+
console.error(`Error: ${response.error.message}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const status = response.result;
|
|
136
|
+
const hours = Math.floor(status.uptime / 3600);
|
|
137
|
+
const minutes = Math.floor((status.uptime % 3600) / 60);
|
|
138
|
+
const uptimeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
139
|
+
console.log(`Daemon running (PID ${status.pid}, uptime ${uptimeStr})`);
|
|
140
|
+
console.log(`Servers: ${status.connectedServers} connected, ${status.totalServers - status.connectedServers} failed`);
|
|
141
|
+
console.log(`Tools: ${status.cachedToolCount} cached`);
|
|
142
|
+
console.log(`Socket: ${status.socketPath}`);
|
|
143
|
+
// Show failed servers
|
|
144
|
+
const failed = status.servers.filter(s => !s.connected);
|
|
145
|
+
if (failed.length > 0) {
|
|
146
|
+
console.log('\nFailed servers:');
|
|
147
|
+
for (const s of failed) {
|
|
148
|
+
console.log(` - ${s.name}: ${s.lastError || 'unknown error'}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import net from 'net';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import pino from 'pino';
|
|
7
|
+
import { MCPClientManager } from '../mcp/client-manager.js';
|
|
8
|
+
import { CompressionCache } from '../services/compression-cache.js';
|
|
9
|
+
import { SessionManager } from '../services/session-manager.js';
|
|
10
|
+
import { StatsService } from '../services/stats-service.js';
|
|
11
|
+
import { loadJSONServers, matchesIgnorePattern } from '../config/loader.js';
|
|
12
|
+
import { interceptPayload } from './payload-interceptor.js';
|
|
13
|
+
const BASE_DIR = path.join(homedir(), '.mcp-compression-proxy');
|
|
14
|
+
const SOCKET_PATH = path.join(BASE_DIR, 'daemon.sock');
|
|
15
|
+
const PID_FILE = path.join(BASE_DIR, 'daemon.pid');
|
|
16
|
+
const LOG_FILE = path.join(BASE_DIR, 'daemon.log');
|
|
17
|
+
export function getSocketPath() {
|
|
18
|
+
return SOCKET_PATH;
|
|
19
|
+
}
|
|
20
|
+
export function getPidFilePath() {
|
|
21
|
+
return PID_FILE;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Start the MCP CLI daemon process.
|
|
25
|
+
* Maintains warm connections to all backend MCP servers
|
|
26
|
+
* and handles IPC requests from the CLI client.
|
|
27
|
+
*/
|
|
28
|
+
async function startDaemon() {
|
|
29
|
+
// Ensure base directory exists. 0700 rather than the umask default: the
|
|
30
|
+
// control socket in here accepts commands that run downstream MCP tools,
|
|
31
|
+
// so it should not be reachable by other local users.
|
|
32
|
+
fs.mkdirSync(BASE_DIR, { recursive: true, mode: 0o700 });
|
|
33
|
+
// mkdirSync ignores `mode` when the directory already exists, so an
|
|
34
|
+
// upgrade from a previous version still gets tightened.
|
|
35
|
+
fs.chmodSync(BASE_DIR, 0o700);
|
|
36
|
+
const logger = pino({
|
|
37
|
+
name: 'mcp-cli-daemon',
|
|
38
|
+
level: process.env.LOG_LEVEL || 'info',
|
|
39
|
+
transport: {
|
|
40
|
+
target: 'pino-pretty',
|
|
41
|
+
options: {
|
|
42
|
+
colorize: false,
|
|
43
|
+
translateTime: 'HH:MM:ss Z',
|
|
44
|
+
ignore: 'pid,hostname',
|
|
45
|
+
destination: LOG_FILE,
|
|
46
|
+
mkdir: true,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
const startTime = Date.now();
|
|
51
|
+
logger.info({ pid: process.pid }, 'Daemon starting');
|
|
52
|
+
// Write PID file
|
|
53
|
+
fs.writeFileSync(PID_FILE, String(process.pid), 'utf-8');
|
|
54
|
+
// Initialize services (reusing existing components)
|
|
55
|
+
const clientManager = new MCPClientManager(logger);
|
|
56
|
+
const compressionCache = new CompressionCache(logger);
|
|
57
|
+
const sessionManager = new SessionManager(logger);
|
|
58
|
+
const statsService = new StatsService(logger, clientManager, compressionCache, sessionManager);
|
|
59
|
+
// Load compression cache from disk
|
|
60
|
+
try {
|
|
61
|
+
await compressionCache.loadFromDisk();
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
logger.warn({ error }, 'Failed to load compression cache, continuing with empty cache');
|
|
65
|
+
}
|
|
66
|
+
// Load config and initialize backend MCP servers
|
|
67
|
+
const config = loadJSONServers();
|
|
68
|
+
let cliConfig = {};
|
|
69
|
+
if (config) {
|
|
70
|
+
compressionCache.setNoCompressPatterns(config.noCompressPatterns);
|
|
71
|
+
// Parse CLI config if present (cast to access extra fields)
|
|
72
|
+
const rawConfig = config;
|
|
73
|
+
if (rawConfig.cli && typeof rawConfig.cli === 'object') {
|
|
74
|
+
cliConfig = rawConfig.cli;
|
|
75
|
+
}
|
|
76
|
+
const enabledServers = config.servers.filter(s => s.enabled !== false);
|
|
77
|
+
logger.info({ total: config.servers.length, enabled: enabledServers.length }, 'Initializing backend MCP servers');
|
|
78
|
+
try {
|
|
79
|
+
await clientManager.initializeServers(enabledServers, config.defaultTimeout);
|
|
80
|
+
logger.info('Backend MCP servers initialization complete');
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
logger.error({ error }, 'Error during backend server initialization');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
logger.warn('No configuration found. Daemon started with no backend servers.');
|
|
88
|
+
}
|
|
89
|
+
// Clean up stale socket file if it exists
|
|
90
|
+
if (fs.existsSync(SOCKET_PATH)) {
|
|
91
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
92
|
+
}
|
|
93
|
+
// Handle IPC request
|
|
94
|
+
async function handleRequest(request) {
|
|
95
|
+
const { id, method, params } = request;
|
|
96
|
+
const excludePatterns = config?.excludePatterns || [];
|
|
97
|
+
try {
|
|
98
|
+
switch (method) {
|
|
99
|
+
case 'tools': {
|
|
100
|
+
const clients = clientManager.getConnectedClients();
|
|
101
|
+
const toolEntries = [];
|
|
102
|
+
for (const { name, client } of clients) {
|
|
103
|
+
try {
|
|
104
|
+
const result = await client.listTools();
|
|
105
|
+
for (const tool of result.tools) {
|
|
106
|
+
const fullName = `${name}__${tool.name}`;
|
|
107
|
+
if (matchesIgnorePattern(fullName, excludePatterns))
|
|
108
|
+
continue;
|
|
109
|
+
const desc = compressionCache.getCompressedDescription(name, tool.name)
|
|
110
|
+
|| tool.description
|
|
111
|
+
|| '';
|
|
112
|
+
// Truncate to ~60 chars for compact listing
|
|
113
|
+
const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
|
|
114
|
+
toolEntries.push({ server: name, tool: tool.name, description: shortDesc });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error({ server: name, error }, 'Failed to list tools');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { id, result: { tools: toolEntries, count: toolEntries.length } };
|
|
122
|
+
}
|
|
123
|
+
case 'search': {
|
|
124
|
+
const query = String(params?.query || '').toLowerCase();
|
|
125
|
+
const clients = clientManager.getConnectedClients();
|
|
126
|
+
const matches = [];
|
|
127
|
+
for (const { name, client } of clients) {
|
|
128
|
+
try {
|
|
129
|
+
const result = await client.listTools();
|
|
130
|
+
for (const tool of result.tools) {
|
|
131
|
+
const fullName = `${name}__${tool.name}`;
|
|
132
|
+
if (matchesIgnorePattern(fullName, excludePatterns))
|
|
133
|
+
continue;
|
|
134
|
+
const desc = compressionCache.getCompressedDescription(name, tool.name)
|
|
135
|
+
|| tool.description
|
|
136
|
+
|| '';
|
|
137
|
+
const searchText = `${name}/${tool.name} ${desc}`.toLowerCase();
|
|
138
|
+
if (searchText.includes(query)) {
|
|
139
|
+
const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
|
|
140
|
+
matches.push({ server: name, tool: tool.name, description: shortDesc });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.error({ server: name, error }, 'Failed to list tools for search');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { id, result: { tools: matches, count: matches.length } };
|
|
149
|
+
}
|
|
150
|
+
case 'info': {
|
|
151
|
+
const serverName = String(params?.server || '');
|
|
152
|
+
const toolName = String(params?.tool || '');
|
|
153
|
+
const client = clientManager.getClient(serverName);
|
|
154
|
+
if (!client) {
|
|
155
|
+
return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = await client.listTools();
|
|
159
|
+
const tool = result.tools.find(t => t.name === toolName);
|
|
160
|
+
if (!tool) {
|
|
161
|
+
return { id, error: { code: -1, message: `Tool '${toolName}' not found on server '${serverName}'` } };
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
result: {
|
|
166
|
+
name: tool.name,
|
|
167
|
+
server: serverName,
|
|
168
|
+
description: tool.description || '',
|
|
169
|
+
inputSchema: tool.inputSchema,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
175
|
+
return { id, error: { code: -1, message: msg } };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
case 'call': {
|
|
179
|
+
const serverName = String(params?.server || '');
|
|
180
|
+
const toolName = String(params?.tool || '');
|
|
181
|
+
const args = (params?.arguments || {});
|
|
182
|
+
const threshold = cliConfig.payloadThreshold ?? 500;
|
|
183
|
+
const client = clientManager.getClient(serverName);
|
|
184
|
+
if (!client) {
|
|
185
|
+
return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const result = await client.callTool({ name: toolName, arguments: args });
|
|
189
|
+
const content = result.content;
|
|
190
|
+
// Extract text content
|
|
191
|
+
// flatMap rather than filter+map: filter does not narrow the
|
|
192
|
+
// element type, which is why this needed a non-null assertion.
|
|
193
|
+
const textParts = content.flatMap((c) => c.type === 'text' && c.text ? [c.text] : []);
|
|
194
|
+
const fullOutput = textParts.join('\n');
|
|
195
|
+
// Apply payload interception
|
|
196
|
+
const output = interceptPayload(fullOutput, threshold);
|
|
197
|
+
return { id, result: { output, isError: result.isError } };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
201
|
+
return { id, error: { code: -1, message: msg } };
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
case 'stats': {
|
|
205
|
+
const stats = await statsService.getStats({
|
|
206
|
+
serverName: params?.serverName,
|
|
207
|
+
detailLevel: params?.detailLevel || 'summary',
|
|
208
|
+
});
|
|
209
|
+
return { id, result: stats };
|
|
210
|
+
}
|
|
211
|
+
case 'daemon-status': {
|
|
212
|
+
const statuses = clientManager.getServerStatuses();
|
|
213
|
+
const connectedCount = statuses.filter(s => s.connected).length;
|
|
214
|
+
const cacheMetrics = compressionCache.getCacheMetrics();
|
|
215
|
+
return {
|
|
216
|
+
id,
|
|
217
|
+
result: {
|
|
218
|
+
running: true,
|
|
219
|
+
pid: process.pid,
|
|
220
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
221
|
+
servers: statuses,
|
|
222
|
+
cachedToolCount: cacheMetrics.totalCached,
|
|
223
|
+
connectedServers: connectedCount,
|
|
224
|
+
totalServers: statuses.length,
|
|
225
|
+
socketPath: SOCKET_PATH,
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
return { id, error: { code: -1, message: `Unknown method: ${method}` } };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
235
|
+
logger.error({ method, error: msg }, 'Request handler error');
|
|
236
|
+
return { id, error: { code: -1, message: msg } };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Create Unix domain socket server
|
|
240
|
+
const server = net.createServer((socket) => {
|
|
241
|
+
let buffer = '';
|
|
242
|
+
socket.on('data', (data) => {
|
|
243
|
+
buffer += data.toString();
|
|
244
|
+
// Process newline-delimited JSON
|
|
245
|
+
let newlineIndex;
|
|
246
|
+
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
|
|
247
|
+
const line = buffer.slice(0, newlineIndex);
|
|
248
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
249
|
+
if (!line.trim())
|
|
250
|
+
continue;
|
|
251
|
+
try {
|
|
252
|
+
const request = JSON.parse(line);
|
|
253
|
+
handleRequest(request)
|
|
254
|
+
.then((response) => {
|
|
255
|
+
socket.write(JSON.stringify(response) + '\n');
|
|
256
|
+
})
|
|
257
|
+
.catch((error) => {
|
|
258
|
+
const errResponse = {
|
|
259
|
+
id: request.id,
|
|
260
|
+
error: { code: -1, message: error instanceof Error ? error.message : 'Unknown error' },
|
|
261
|
+
};
|
|
262
|
+
socket.write(JSON.stringify(errResponse) + '\n');
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
logger.error({ line }, 'Failed to parse IPC request');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
socket.on('error', (error) => {
|
|
271
|
+
logger.debug({ error: error.message }, 'Socket error');
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
// Without this, a failed listen emits an unhandled 'error' event and kills
|
|
275
|
+
// the daemon. Because it is forked with stdio: 'ignore', the crash goes
|
|
276
|
+
// nowhere: the log simply stops mid-startup and the CLI reports only
|
|
277
|
+
// "Failed to start daemon." Log the cause and leave no stale PID behind.
|
|
278
|
+
server.on('error', (error) => {
|
|
279
|
+
let hint = '';
|
|
280
|
+
if (error.code === 'EADDRINUSE') {
|
|
281
|
+
hint = ' Another daemon may already be running; try "mcp-cli daemon stop".';
|
|
282
|
+
}
|
|
283
|
+
else if (error.code === 'EACCES') {
|
|
284
|
+
hint = ` Check permissions on ${BASE_DIR}.`;
|
|
285
|
+
}
|
|
286
|
+
else if (SOCKET_PATH.length > 100) {
|
|
287
|
+
// Unix domain socket paths are capped near 107 bytes on Linux/macOS.
|
|
288
|
+
hint = ` The socket path is ${SOCKET_PATH.length} characters, which likely exceeds the ~107 byte limit for Unix sockets.`;
|
|
289
|
+
}
|
|
290
|
+
logger.error({ socketPath: SOCKET_PATH, code: error.code, error: error.message }, `Failed to listen on the daemon socket.${hint}`);
|
|
291
|
+
try {
|
|
292
|
+
fs.unlinkSync(PID_FILE);
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore */ }
|
|
295
|
+
process.exit(1);
|
|
296
|
+
});
|
|
297
|
+
server.listen(SOCKET_PATH, () => {
|
|
298
|
+
logger.info({ socketPath: SOCKET_PATH, pid: process.pid }, 'Daemon listening');
|
|
299
|
+
// Signal readiness by writing a ready marker
|
|
300
|
+
const readyFile = path.join(BASE_DIR, 'daemon.ready');
|
|
301
|
+
fs.writeFileSync(readyFile, String(Date.now()), 'utf-8');
|
|
302
|
+
});
|
|
303
|
+
// Graceful shutdown
|
|
304
|
+
function shutdown() {
|
|
305
|
+
logger.info('Daemon shutting down');
|
|
306
|
+
server.close();
|
|
307
|
+
clientManager.disconnectAll().then(() => {
|
|
308
|
+
sessionManager.destroy();
|
|
309
|
+
// Clean up files
|
|
310
|
+
try {
|
|
311
|
+
fs.unlinkSync(SOCKET_PATH);
|
|
312
|
+
}
|
|
313
|
+
catch { /* ignore */ }
|
|
314
|
+
try {
|
|
315
|
+
fs.unlinkSync(PID_FILE);
|
|
316
|
+
}
|
|
317
|
+
catch { /* ignore */ }
|
|
318
|
+
try {
|
|
319
|
+
fs.unlinkSync(path.join(BASE_DIR, 'daemon.ready'));
|
|
320
|
+
}
|
|
321
|
+
catch { /* ignore */ }
|
|
322
|
+
logger.info('Daemon stopped');
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}).catch(() => {
|
|
325
|
+
process.exit(1);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
process.on('SIGTERM', shutdown);
|
|
329
|
+
process.on('SIGINT', shutdown);
|
|
330
|
+
}
|
|
331
|
+
// Entry point when run directly
|
|
332
|
+
startDaemon().catch((error) => {
|
|
333
|
+
console.error('Failed to start daemon:', error);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
});
|
|
336
|
+
//# sourceMappingURL=daemon.js.map
|