@darkiceinteractive/mcp-conductor 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/LICENSE +21 -0
- package/README.md +558 -0
- package/dist/bin/cli.d.ts +8 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +940 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bridge/http-server.d.ts +161 -0
- package/dist/bridge/http-server.d.ts.map +1 -0
- package/dist/bridge/http-server.js +367 -0
- package/dist/bridge/http-server.js.map +1 -0
- package/dist/bridge/index.d.ts +5 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +5 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/config/defaults.d.ts +29 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +60 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/loader.d.ts +49 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +272 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +93 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +5 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/hub/index.d.ts +5 -0
- package/dist/hub/index.d.ts.map +1 -0
- package/dist/hub/index.js +5 -0
- package/dist/hub/index.js.map +1 -0
- package/dist/hub/mcp-hub.d.ts +176 -0
- package/dist/hub/mcp-hub.d.ts.map +1 -0
- package/dist/hub/mcp-hub.js +550 -0
- package/dist/hub/mcp-hub.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +5 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +5 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/metrics-collector.d.ts +211 -0
- package/dist/metrics/metrics-collector.d.ts.map +1 -0
- package/dist/metrics/metrics-collector.js +437 -0
- package/dist/metrics/metrics-collector.js.map +1 -0
- package/dist/modes/index.d.ts +5 -0
- package/dist/modes/index.d.ts.map +1 -0
- package/dist/modes/index.js +5 -0
- package/dist/modes/index.js.map +1 -0
- package/dist/modes/mode-handler.d.ts +132 -0
- package/dist/modes/mode-handler.d.ts.map +1 -0
- package/dist/modes/mode-handler.js +252 -0
- package/dist/modes/mode-handler.js.map +1 -0
- package/dist/runtime/executor.d.ts +57 -0
- package/dist/runtime/executor.d.ts.map +1 -0
- package/dist/runtime/executor.js +700 -0
- package/dist/runtime/executor.js.map +1 -0
- package/dist/runtime/index.d.ts +5 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +5 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp-server.d.ts +62 -0
- package/dist/server/mcp-server.d.ts.map +1 -0
- package/dist/server/mcp-server.js +1272 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/skills/index.d.ts +5 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +5 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/skills-engine.d.ts +157 -0
- package/dist/skills/skills-engine.d.ts.map +1 -0
- package/dist/skills/skills-engine.js +405 -0
- package/dist/skills/skills-engine.js.map +1 -0
- package/dist/streaming/execution-stream.d.ts +158 -0
- package/dist/streaming/execution-stream.d.ts.map +1 -0
- package/dist/streaming/execution-stream.js +320 -0
- package/dist/streaming/execution-stream.js.map +1 -0
- package/dist/streaming/index.d.ts +5 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +5 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/utils/errors.d.ts +36 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +68 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/helpers.d.ts +44 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +95 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/index.d.ts +9 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +9 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +48 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/permissions.d.ts +97 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +165 -0
- package/dist/utils/permissions.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +87 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +187 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/watcher/config-watcher.d.ts +67 -0
- package/dist/watcher/config-watcher.d.ts.map +1 -0
- package/dist/watcher/config-watcher.js +150 -0
- package/dist/watcher/config-watcher.js.map +1 -0
- package/dist/watcher/index.d.ts +5 -0
- package/dist/watcher/index.d.ts.map +1 -0
- package/dist/watcher/index.js +5 -0
- package/dist/watcher/index.js.map +1 -0
- package/package.json +86 -0
- package/templates/CLAUDE.md +137 -0
- package/templates/skill-mcp-conductor.md +64 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token bucket rate limiter for MCP servers
|
|
3
|
+
*/
|
|
4
|
+
import { EventEmitter } from 'node:events';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Token bucket rate limiter with queue and reject modes
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimiter extends EventEmitter {
|
|
10
|
+
serverName;
|
|
11
|
+
requestsPerSecond;
|
|
12
|
+
maxTokens;
|
|
13
|
+
mode;
|
|
14
|
+
maxQueueTimeMs;
|
|
15
|
+
tokens;
|
|
16
|
+
lastRefill;
|
|
17
|
+
queue = [];
|
|
18
|
+
refillInterval = null;
|
|
19
|
+
// Stats
|
|
20
|
+
totalRequests = 0;
|
|
21
|
+
totalWaited = 0;
|
|
22
|
+
totalRejected = 0;
|
|
23
|
+
constructor(config, serverName) {
|
|
24
|
+
super();
|
|
25
|
+
this.serverName = serverName;
|
|
26
|
+
this.requestsPerSecond = config.requestsPerSecond;
|
|
27
|
+
this.maxTokens = config.burstSize ?? config.requestsPerSecond;
|
|
28
|
+
this.mode = config.onLimitExceeded ?? 'queue';
|
|
29
|
+
this.maxQueueTimeMs = config.maxQueueTimeMs ?? 30000;
|
|
30
|
+
// Start with full bucket
|
|
31
|
+
this.tokens = this.maxTokens;
|
|
32
|
+
this.lastRefill = Date.now();
|
|
33
|
+
// Start background token refill
|
|
34
|
+
this.startRefillTimer();
|
|
35
|
+
logger.debug(`RateLimiter created for ${serverName}`, {
|
|
36
|
+
requestsPerSecond: this.requestsPerSecond,
|
|
37
|
+
maxTokens: this.maxTokens,
|
|
38
|
+
mode: this.mode,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Acquire a token before making a request
|
|
43
|
+
* In queue mode: waits until a token is available
|
|
44
|
+
* In reject mode: throws immediately if no token available
|
|
45
|
+
*/
|
|
46
|
+
async acquire() {
|
|
47
|
+
this.totalRequests++;
|
|
48
|
+
// Try to get a token immediately
|
|
49
|
+
if (this.tryAcquire()) {
|
|
50
|
+
this.emit('acquired', { serverName: this.serverName, waitedMs: 0 });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
// No token available
|
|
54
|
+
if (this.mode === 'reject') {
|
|
55
|
+
this.totalRejected++;
|
|
56
|
+
const error = new Error(`Rate limit exceeded for ${this.serverName}: ${this.requestsPerSecond} req/s`);
|
|
57
|
+
this.emit('rejected', { serverName: this.serverName, reason: 'no tokens available' });
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
// Queue mode: wait for a token
|
|
61
|
+
return this.enqueue();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Release a token back (for cancelled requests before they actually execute)
|
|
65
|
+
*/
|
|
66
|
+
release() {
|
|
67
|
+
this.tokens = Math.min(this.tokens + 1, this.maxTokens);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Get current rate limiter stats
|
|
71
|
+
*/
|
|
72
|
+
getStats() {
|
|
73
|
+
return {
|
|
74
|
+
serverName: this.serverName,
|
|
75
|
+
availableTokens: this.tokens,
|
|
76
|
+
maxTokens: this.maxTokens,
|
|
77
|
+
requestsPerSecond: this.requestsPerSecond,
|
|
78
|
+
queueLength: this.queue.length,
|
|
79
|
+
totalRequests: this.totalRequests,
|
|
80
|
+
totalWaited: this.totalWaited,
|
|
81
|
+
totalRejected: this.totalRejected,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Clean up resources
|
|
86
|
+
*/
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this.refillInterval) {
|
|
89
|
+
clearInterval(this.refillInterval);
|
|
90
|
+
this.refillInterval = null;
|
|
91
|
+
}
|
|
92
|
+
// Reject all queued requests
|
|
93
|
+
for (const request of this.queue) {
|
|
94
|
+
clearTimeout(request.timeout);
|
|
95
|
+
request.reject(new Error(`RateLimiter for ${this.serverName} destroyed`));
|
|
96
|
+
}
|
|
97
|
+
this.queue = [];
|
|
98
|
+
logger.debug(`RateLimiter destroyed for ${this.serverName}`);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Try to acquire a token without waiting
|
|
102
|
+
*/
|
|
103
|
+
tryAcquire() {
|
|
104
|
+
this.refillTokens();
|
|
105
|
+
if (this.tokens >= 1) {
|
|
106
|
+
this.tokens--;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Enqueue a request to wait for a token
|
|
113
|
+
*/
|
|
114
|
+
enqueue() {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const queuePosition = this.queue.length + 1;
|
|
117
|
+
const estimatedWaitMs = Math.ceil((queuePosition / this.requestsPerSecond) * 1000);
|
|
118
|
+
this.emit('waiting', {
|
|
119
|
+
serverName: this.serverName,
|
|
120
|
+
queuePosition,
|
|
121
|
+
estimatedWaitMs,
|
|
122
|
+
});
|
|
123
|
+
logger.debug(`Request queued for ${this.serverName}`, {
|
|
124
|
+
queuePosition,
|
|
125
|
+
estimatedWaitMs,
|
|
126
|
+
});
|
|
127
|
+
const timeout = setTimeout(() => {
|
|
128
|
+
// Remove from queue
|
|
129
|
+
const index = this.queue.findIndex((r) => r.timeout === timeout);
|
|
130
|
+
if (index !== -1) {
|
|
131
|
+
this.queue.splice(index, 1);
|
|
132
|
+
}
|
|
133
|
+
this.totalRejected++;
|
|
134
|
+
const error = new Error(`Rate limit queue timeout for ${this.serverName} after ${this.maxQueueTimeMs}ms`);
|
|
135
|
+
this.emit('rejected', { serverName: this.serverName, reason: 'queue timeout' });
|
|
136
|
+
reject(error);
|
|
137
|
+
}, this.maxQueueTimeMs);
|
|
138
|
+
this.queue.push({
|
|
139
|
+
resolve,
|
|
140
|
+
reject,
|
|
141
|
+
timeout,
|
|
142
|
+
enqueuedAt: Date.now(),
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Refill tokens based on elapsed time
|
|
148
|
+
*/
|
|
149
|
+
refillTokens() {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
const elapsed = now - this.lastRefill;
|
|
152
|
+
// Calculate tokens to add based on elapsed time
|
|
153
|
+
const tokensToAdd = (elapsed / 1000) * this.requestsPerSecond;
|
|
154
|
+
if (tokensToAdd >= 1) {
|
|
155
|
+
this.tokens = Math.min(this.tokens + Math.floor(tokensToAdd), this.maxTokens);
|
|
156
|
+
this.lastRefill = now;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Start background timer to process queued requests
|
|
161
|
+
*/
|
|
162
|
+
startRefillTimer() {
|
|
163
|
+
// Check queue every 50ms
|
|
164
|
+
this.refillInterval = setInterval(() => {
|
|
165
|
+
this.processQueue();
|
|
166
|
+
}, 50);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Process queued requests when tokens become available
|
|
170
|
+
*/
|
|
171
|
+
processQueue() {
|
|
172
|
+
this.refillTokens();
|
|
173
|
+
while (this.queue.length > 0 && this.tokens >= 1) {
|
|
174
|
+
const request = this.queue.shift();
|
|
175
|
+
if (request) {
|
|
176
|
+
clearTimeout(request.timeout);
|
|
177
|
+
this.tokens--;
|
|
178
|
+
this.totalWaited++;
|
|
179
|
+
const waitedMs = Date.now() - request.enqueuedAt;
|
|
180
|
+
this.emit('acquired', { serverName: this.serverName, waitedMs });
|
|
181
|
+
logger.debug(`Request dequeued for ${this.serverName}`, { waitedMs });
|
|
182
|
+
request.resolve();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=rate-limiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config File Watcher
|
|
3
|
+
*
|
|
4
|
+
* Watches Claude config files for changes and triggers hot-reload
|
|
5
|
+
* of MCP server connections.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from 'node:events';
|
|
8
|
+
export interface WatcherConfig {
|
|
9
|
+
/** Path to watch (auto-detect if not specified) */
|
|
10
|
+
configPath?: string;
|
|
11
|
+
/** Debounce delay in milliseconds */
|
|
12
|
+
debounceMs?: number;
|
|
13
|
+
/** Whether to watch for changes */
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WatcherEvents {
|
|
17
|
+
configChanged: (path: string) => void;
|
|
18
|
+
watcherError: (error: Error) => void;
|
|
19
|
+
watcherStarted: (path: string) => void;
|
|
20
|
+
watcherStopped: () => void;
|
|
21
|
+
}
|
|
22
|
+
export declare class ConfigWatcher extends EventEmitter {
|
|
23
|
+
private config;
|
|
24
|
+
private watcher;
|
|
25
|
+
private debounceTimer;
|
|
26
|
+
private watchedPath;
|
|
27
|
+
private isRunning;
|
|
28
|
+
constructor(config?: WatcherConfig);
|
|
29
|
+
/**
|
|
30
|
+
* Start watching the config file
|
|
31
|
+
*/
|
|
32
|
+
start(): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Stop watching the config file
|
|
35
|
+
*/
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Handle file change event with debouncing
|
|
39
|
+
*/
|
|
40
|
+
private handleChange;
|
|
41
|
+
/**
|
|
42
|
+
* Check if watcher is running
|
|
43
|
+
*/
|
|
44
|
+
isWatching(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Get the path being watched
|
|
47
|
+
*/
|
|
48
|
+
getWatchedPath(): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Update the debounce delay
|
|
51
|
+
*/
|
|
52
|
+
setDebounceMs(ms: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Trigger a manual reload (without file change)
|
|
55
|
+
*/
|
|
56
|
+
triggerReload(): void;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a config watcher that integrates with an MCP Hub
|
|
60
|
+
*/
|
|
61
|
+
export declare function createHubWatcher(hub: {
|
|
62
|
+
reload: () => Promise<{
|
|
63
|
+
added: string[];
|
|
64
|
+
removed: string[];
|
|
65
|
+
}>;
|
|
66
|
+
}, config?: WatcherConfig): ConfigWatcher;
|
|
67
|
+
//# sourceMappingURL=config-watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.d.ts","sourceRoot":"","sources":["../../src/watcher/config-watcher.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,cAAc,EAAE,MAAM,IAAI,CAAC;CAC5B;AAQD,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,GAAE,aAAkB;IAKtC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC;IAkD/B;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAkB3B;;OAEG;IACH,OAAO,CAAC,YAAY;IAcpB;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,aAAa,IAAI,IAAI;CAMtB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE;IAAE,MAAM,EAAE,MAAM,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,EACtE,MAAM,GAAE,aAAkB,GACzB,aAAa,CAgBf"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config File Watcher
|
|
3
|
+
*
|
|
4
|
+
* Watches Claude config files for changes and triggers hot-reload
|
|
5
|
+
* of MCP server connections.
|
|
6
|
+
*/
|
|
7
|
+
import { watch } from 'chokidar';
|
|
8
|
+
import { EventEmitter } from 'node:events';
|
|
9
|
+
import { logger } from '../utils/index.js';
|
|
10
|
+
import { findClaudeConfig } from '../config/loader.js';
|
|
11
|
+
const DEFAULT_WATCHER_CONFIG = {
|
|
12
|
+
configPath: '',
|
|
13
|
+
debounceMs: 500,
|
|
14
|
+
enabled: true,
|
|
15
|
+
};
|
|
16
|
+
export class ConfigWatcher extends EventEmitter {
|
|
17
|
+
config;
|
|
18
|
+
watcher = null;
|
|
19
|
+
debounceTimer = null;
|
|
20
|
+
watchedPath = null;
|
|
21
|
+
isRunning = false;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.config = { ...DEFAULT_WATCHER_CONFIG, ...config };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Start watching the config file
|
|
28
|
+
*/
|
|
29
|
+
async start() {
|
|
30
|
+
if (!this.config.enabled) {
|
|
31
|
+
logger.debug('Config watcher disabled');
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (this.isRunning) {
|
|
35
|
+
logger.warn('Config watcher already running');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Find the config file to watch
|
|
39
|
+
const configPath = this.config.configPath || findClaudeConfig();
|
|
40
|
+
if (!configPath) {
|
|
41
|
+
logger.warn('No Claude config file found to watch');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
this.watchedPath = configPath;
|
|
45
|
+
try {
|
|
46
|
+
this.watcher = watch(configPath, {
|
|
47
|
+
persistent: true,
|
|
48
|
+
ignoreInitial: true,
|
|
49
|
+
awaitWriteFinish: {
|
|
50
|
+
stabilityThreshold: this.config.debounceMs,
|
|
51
|
+
pollInterval: 100,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
this.watcher.on('change', (path) => {
|
|
55
|
+
this.handleChange(path);
|
|
56
|
+
});
|
|
57
|
+
this.watcher.on('error', (error) => {
|
|
58
|
+
logger.error('Config watcher error', { error: String(error) });
|
|
59
|
+
this.emit('watcherError', error);
|
|
60
|
+
});
|
|
61
|
+
this.isRunning = true;
|
|
62
|
+
logger.info(`Config watcher started`, { path: configPath });
|
|
63
|
+
this.emit('watcherStarted', configPath);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
logger.error('Failed to start config watcher', { error: String(error) });
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Stop watching the config file
|
|
73
|
+
*/
|
|
74
|
+
async stop() {
|
|
75
|
+
if (this.debounceTimer) {
|
|
76
|
+
clearTimeout(this.debounceTimer);
|
|
77
|
+
this.debounceTimer = null;
|
|
78
|
+
}
|
|
79
|
+
if (this.watcher) {
|
|
80
|
+
await this.watcher.close();
|
|
81
|
+
this.watcher = null;
|
|
82
|
+
}
|
|
83
|
+
this.isRunning = false;
|
|
84
|
+
this.watchedPath = null;
|
|
85
|
+
logger.info('Config watcher stopped');
|
|
86
|
+
this.emit('watcherStopped');
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Handle file change event with debouncing
|
|
90
|
+
*/
|
|
91
|
+
handleChange(path) {
|
|
92
|
+
// Clear any existing debounce timer
|
|
93
|
+
if (this.debounceTimer) {
|
|
94
|
+
clearTimeout(this.debounceTimer);
|
|
95
|
+
}
|
|
96
|
+
// Set up debounced callback
|
|
97
|
+
this.debounceTimer = setTimeout(() => {
|
|
98
|
+
logger.info('Config file changed, triggering reload', { path });
|
|
99
|
+
this.emit('configChanged', path);
|
|
100
|
+
this.debounceTimer = null;
|
|
101
|
+
}, this.config.debounceMs);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Check if watcher is running
|
|
105
|
+
*/
|
|
106
|
+
isWatching() {
|
|
107
|
+
return this.isRunning;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the path being watched
|
|
111
|
+
*/
|
|
112
|
+
getWatchedPath() {
|
|
113
|
+
return this.watchedPath;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Update the debounce delay
|
|
117
|
+
*/
|
|
118
|
+
setDebounceMs(ms) {
|
|
119
|
+
this.config.debounceMs = ms;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Trigger a manual reload (without file change)
|
|
123
|
+
*/
|
|
124
|
+
triggerReload() {
|
|
125
|
+
if (this.watchedPath) {
|
|
126
|
+
logger.info('Manual reload triggered', { path: this.watchedPath });
|
|
127
|
+
this.emit('configChanged', this.watchedPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Create a config watcher that integrates with an MCP Hub
|
|
133
|
+
*/
|
|
134
|
+
export function createHubWatcher(hub, config = {}) {
|
|
135
|
+
const watcher = new ConfigWatcher(config);
|
|
136
|
+
watcher.on('configChanged', async () => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await hub.reload();
|
|
139
|
+
logger.info('Hub reloaded after config change', {
|
|
140
|
+
added: result.added,
|
|
141
|
+
removed: result.removed,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
logger.error('Failed to reload hub after config change', { error: String(error) });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return watcher;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=config-watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-watcher.js","sourceRoot":"","sources":["../../src/watcher/config-watcher.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/watcher/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/watcher/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EACL,aAAa,EACb,gBAAgB,GAGjB,MAAM,qBAAqB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@darkiceinteractive/mcp-conductor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that orchestrates code execution in a sandboxed Deno environment with access to all MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-conductor": "./dist/bin/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"templates",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"setup": "./scripts/setup.sh",
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsx watch src/index.ts",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"test": "vitest",
|
|
23
|
+
"test:run": "vitest run",
|
|
24
|
+
"test:unit": "vitest run test/unit",
|
|
25
|
+
"test:integration": "vitest run test/integration",
|
|
26
|
+
"test:e2e": "vitest run test/e2e",
|
|
27
|
+
"test:benchmark": "vitest run test/benchmark",
|
|
28
|
+
"test:coverage": "vitest run --coverage",
|
|
29
|
+
"test:all": "vitest run",
|
|
30
|
+
"lint": "eslint src --ext .ts",
|
|
31
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
32
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
33
|
+
"clean": "rm -rf dist",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"mcp",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"claude",
|
|
40
|
+
"code-execution",
|
|
41
|
+
"sandbox",
|
|
42
|
+
"deno"
|
|
43
|
+
],
|
|
44
|
+
"author": "DarkIce Interactive",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/darkiceinteractive/mcp-conductor.git"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/darkiceinteractive/mcp-conductor#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/darkiceinteractive/mcp-conductor/issues"
|
|
53
|
+
},
|
|
54
|
+
"exports": {
|
|
55
|
+
".": {
|
|
56
|
+
"types": "./dist/index.d.ts",
|
|
57
|
+
"import": "./dist/index.js"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
65
|
+
"chalk": "^5.3.0",
|
|
66
|
+
"chokidar": "^3.5.3",
|
|
67
|
+
"commander": "^12.1.0",
|
|
68
|
+
"conf": "^13.0.1",
|
|
69
|
+
"gray-matter": "^4.0.3",
|
|
70
|
+
"ora": "^8.1.1",
|
|
71
|
+
"uuid": "^10.0.0",
|
|
72
|
+
"zod": "^3.23.8"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@types/node": "^22.10.2",
|
|
76
|
+
"@types/uuid": "^10.0.0",
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
|
78
|
+
"@typescript-eslint/parser": "^8.18.0",
|
|
79
|
+
"@vitest/coverage-v8": "^2.1.8",
|
|
80
|
+
"eslint": "^9.17.0",
|
|
81
|
+
"prettier": "^3.4.2",
|
|
82
|
+
"tsx": "^4.19.2",
|
|
83
|
+
"typescript": "^5.7.2",
|
|
84
|
+
"vitest": "^2.1.8"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# MCP Conductor - Project Instructions
|
|
2
|
+
|
|
3
|
+
This project has MCP Conductor configured for token-efficient MCP operations.
|
|
4
|
+
|
|
5
|
+
## Critical: Use execute_code for MCP Operations
|
|
6
|
+
|
|
7
|
+
**ALWAYS use `mcp-conductor`'s `execute_code` tool** instead of making direct MCP tool calls when:
|
|
8
|
+
|
|
9
|
+
1. **Multiple tool calls needed** - Batch operations into a single execute_code call
|
|
10
|
+
2. **Processing data** - Filter, transform, or aggregate results in the sandbox
|
|
11
|
+
3. **Iterating over items** - Files, repos, search results, etc.
|
|
12
|
+
4. **Large datasets** - Process data server-side, return only summaries
|
|
13
|
+
|
|
14
|
+
### Why This Matters
|
|
15
|
+
|
|
16
|
+
Direct MCP calls consume tokens for BOTH request AND response in Claude's context. With execute_code:
|
|
17
|
+
- Write code once (small token cost)
|
|
18
|
+
- All intermediate data stays in sandbox (zero token cost)
|
|
19
|
+
- Only final return value enters context (minimal tokens)
|
|
20
|
+
|
|
21
|
+
**Token savings: 90%+ for multi-step operations**
|
|
22
|
+
|
|
23
|
+
### Example: Reading Multiple Files
|
|
24
|
+
|
|
25
|
+
**BAD - Direct calls (high token usage):**
|
|
26
|
+
```
|
|
27
|
+
1. filesystem.list_directory → full response in context
|
|
28
|
+
2. filesystem.read_file → full file in context
|
|
29
|
+
3. filesystem.read_file → full file in context
|
|
30
|
+
4. filesystem.read_file → full file in context
|
|
31
|
+
... (each response consumes tokens)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**GOOD - execute_code (minimal tokens):**
|
|
35
|
+
```typescript
|
|
36
|
+
// Use mcp-conductor's execute_code tool with this code:
|
|
37
|
+
const fs = mcp.server('filesystem');
|
|
38
|
+
const files = await fs.call('list_directory', { path: '/src' });
|
|
39
|
+
const tsFiles = files.entries.filter(f => f.name.endsWith('.ts'));
|
|
40
|
+
const contents = await Promise.all(
|
|
41
|
+
tsFiles.map(f => fs.call('read_file', { path: '/src/' + f.name }))
|
|
42
|
+
);
|
|
43
|
+
// Only this summary enters Claude's context:
|
|
44
|
+
return {
|
|
45
|
+
count: tsFiles.length,
|
|
46
|
+
totalLines: contents.reduce((sum, c) => sum + c.split('\n').length, 0)
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### When Direct MCP Calls Are OK
|
|
51
|
+
|
|
52
|
+
- Single, simple tool call with no processing
|
|
53
|
+
- Debugging or interactive exploration
|
|
54
|
+
- When you need streaming feedback
|
|
55
|
+
|
|
56
|
+
## Web Search Optimisation (Optional)
|
|
57
|
+
|
|
58
|
+
If `brave-search` MCP server is configured, use `mcp.batchSearch()` for efficient batched searches:
|
|
59
|
+
|
|
60
|
+
**Native WebSearch (sequential, high tokens):**
|
|
61
|
+
```
|
|
62
|
+
1. WebSearch("query 1") → full results in context
|
|
63
|
+
2. WebSearch("query 2") → full results in context
|
|
64
|
+
3. WebSearch("query 3") → full results in context
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**mcp.batchSearch() (auto rate-limit handling, filtered):**
|
|
68
|
+
```typescript
|
|
69
|
+
// Simple: auto handles rate limits, parses results, returns top 3 per query
|
|
70
|
+
const results = await mcp.batchSearch([
|
|
71
|
+
'React hooks best practices',
|
|
72
|
+
'Vue composition API tutorial',
|
|
73
|
+
'Svelte vs React performance'
|
|
74
|
+
], { topN: 3 });
|
|
75
|
+
|
|
76
|
+
return results;
|
|
77
|
+
// Returns: { "React hooks...": [{title, url, description}, ...], ... }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**How it works:**
|
|
81
|
+
- Attempts parallel execution first (fastest)
|
|
82
|
+
- Auto-detects rate limits from errors
|
|
83
|
+
- Falls back to sequential with delays if needed
|
|
84
|
+
- Caches rate limit status for session
|
|
85
|
+
- Parses text response into structured data
|
|
86
|
+
|
|
87
|
+
**For other rate-limited APIs, use mcp.batch():**
|
|
88
|
+
```typescript
|
|
89
|
+
// Generic batch with auto rate limit handling
|
|
90
|
+
const results = await mcp.batch([
|
|
91
|
+
{ server: 'github', tool: 'search_repositories', params: { query: 'ai' } },
|
|
92
|
+
{ server: 'github', tool: 'search_repositories', params: { query: 'ml' } },
|
|
93
|
+
]);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Rate limit warnings (displayed in logs):**
|
|
97
|
+
```
|
|
98
|
+
⚠️ RATE LIMITED: brave-search - Free tier limit hit. Retrying with delays...
|
|
99
|
+
💡 TIP: Upgrade your API plan for parallel execution: https://brave.com/search/api/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Benefits:**
|
|
103
|
+
- Automatic rate limit detection and handling
|
|
104
|
+
- No manual delays needed
|
|
105
|
+
- Parallel when possible, sequential when required
|
|
106
|
+
- ~80%+ token savings
|
|
107
|
+
|
|
108
|
+
**After upgrading API plan, use `forceParallel` to test:**
|
|
109
|
+
```typescript
|
|
110
|
+
const results = await mcp.batchSearch([
|
|
111
|
+
'query 1', 'query 2', 'query 3', 'query 4', 'query 5'
|
|
112
|
+
], { topN: 3, forceParallel: true });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note:** Use native WebSearch for single searches or when brave-search isn't configured.
|
|
116
|
+
|
|
117
|
+
### Quick Reference
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Get a server client
|
|
121
|
+
const github = mcp.server('github');
|
|
122
|
+
// Or use attribute style
|
|
123
|
+
const github = mcp.github;
|
|
124
|
+
|
|
125
|
+
// Call a tool
|
|
126
|
+
const repos = await github.call('search_repositories', { query: 'ai' });
|
|
127
|
+
|
|
128
|
+
// Search for tools across all servers
|
|
129
|
+
const tools = await mcp.searchTools('file');
|
|
130
|
+
|
|
131
|
+
// Log (captured in response)
|
|
132
|
+
mcp.log('Processing...');
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Available MCP Servers
|
|
136
|
+
|
|
137
|
+
Run `mcp-conductor - list_servers` to see all connected servers and their tools.
|