@animalabs/agent-framework 0.1.0 → 0.3.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/dist/src/framework.d.ts +13 -0
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +133 -10
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +5 -0
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +104 -6
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/types.d.ts +31 -0
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +16 -2
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +101 -6
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +1 -1
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +17 -2
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +66 -3
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-policy.d.ts +24 -0
- package/dist/src/mcpl/tool-policy.d.ts.map +1 -0
- package/dist/src/mcpl/tool-policy.js +46 -0
- package/dist/src/mcpl/tool-policy.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +31 -0
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +43 -1
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +246 -32
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +7 -2
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +2 -2
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +55 -3
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js +8 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +40 -3
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +80 -13
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/types/trace.d.ts +20 -0
- package/dist/src/types/trace.d.ts.map +1 -1
- package/dist/src/usage/index.d.ts +3 -0
- package/dist/src/usage/index.d.ts.map +1 -0
- package/dist/src/usage/index.js +2 -0
- package/dist/src/usage/index.js.map +1 -0
- package/dist/src/usage/types.d.ts +28 -0
- package/dist/src/usage/types.d.ts.map +1 -0
- package/dist/src/usage/types.js +2 -0
- package/dist/src/usage/types.js.map +1 -0
- package/dist/src/usage/usage-tracker.d.ts +33 -0
- package/dist/src/usage/usage-tracker.d.ts.map +1 -0
- package/dist/src/usage/usage-tracker.js +115 -0
- package/dist/src/usage/usage-tracker.js.map +1 -0
- package/dist/test/channel-registry-typing.test.d.ts +2 -0
- package/dist/test/channel-registry-typing.test.d.ts.map +1 -0
- package/dist/test/channel-registry-typing.test.js +82 -0
- package/dist/test/channel-registry-typing.test.js.map +1 -0
- package/dist/test/event-gate.test.js +135 -16
- package/dist/test/event-gate.test.js.map +1 -1
- package/dist/test/mcpl-gate-roundtrip.test.d.ts +2 -0
- package/dist/test/mcpl-gate-roundtrip.test.d.ts.map +1 -0
- package/dist/test/mcpl-gate-roundtrip.test.js +140 -0
- package/dist/test/mcpl-gate-roundtrip.test.js.map +1 -0
- package/dist/test/tool-policy.test.d.ts +2 -0
- package/dist/test/tool-policy.test.d.ts.map +1 -0
- package/dist/test/tool-policy.test.js +51 -0
- package/dist/test/tool-policy.test.js.map +1 -0
- package/dist/test/usage-tracker.test.d.ts +2 -0
- package/dist/test/usage-tracker.test.d.ts.map +1 -0
- package/dist/test/usage-tracker.test.js +129 -0
- package/dist/test/usage-tracker.test.js.map +1 -0
- package/dist/test/workspace-watcher.test.d.ts +2 -0
- package/dist/test/workspace-watcher.test.d.ts.map +1 -0
- package/dist/test/workspace-watcher.test.js +117 -0
- package/dist/test/workspace-watcher.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/framework.ts +148 -10
- package/src/gate/event-gate.ts +106 -6
- package/src/gate/types.ts +28 -0
- package/src/index.ts +7 -0
- package/src/mcpl/channel-registry.ts +120 -8
- package/src/mcpl/push-handler.ts +1 -1
- package/src/mcpl/server-connection.ts +78 -3
- package/src/mcpl/tool-policy.ts +52 -0
- package/src/mcpl/types.ts +34 -0
- package/src/modules/workspace/index.ts +269 -38
- package/src/modules/workspace/sync.ts +11 -4
- package/src/modules/workspace/types.ts +70 -3
- package/src/modules/workspace/watcher.ts +104 -15
- package/src/types/trace.ts +26 -1
- package/src/usage/index.ts +2 -0
- package/src/usage/types.ts +27 -0
- package/src/usage/usage-tracker.ts +141 -0
|
@@ -38,6 +38,21 @@ import type { ToolDefinition, ToolResult, ProcessEvent } from '../types/index.js
|
|
|
38
38
|
|
|
39
39
|
const TYPING_INTERVAL_MS = 7_000;
|
|
40
40
|
|
|
41
|
+
function shallowEqualRecord(
|
|
42
|
+
a: Record<string, unknown> | undefined,
|
|
43
|
+
b: Record<string, unknown> | undefined,
|
|
44
|
+
): boolean {
|
|
45
|
+
if (a === b) return true;
|
|
46
|
+
if (!a || !b) return false;
|
|
47
|
+
const keysA = Object.keys(a);
|
|
48
|
+
const keysB = Object.keys(b);
|
|
49
|
+
if (keysA.length !== keysB.length) return false;
|
|
50
|
+
for (const k of keysA) {
|
|
51
|
+
if (a[k] !== b[k]) return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
41
56
|
// ============================================================================
|
|
42
57
|
// Internal Types
|
|
43
58
|
// ============================================================================
|
|
@@ -179,7 +194,12 @@ export class ChannelRegistry {
|
|
|
179
194
|
private featureSetManager: FeatureSetManager;
|
|
180
195
|
private pushEventFn: (event: ProcessEvent) => void;
|
|
181
196
|
private emitTraceFn: (event: { type: string; [key: string]: unknown }) => void;
|
|
182
|
-
private sendTypingFn?: (
|
|
197
|
+
private sendTypingFn?: (
|
|
198
|
+
serverId: string,
|
|
199
|
+
channelId: string,
|
|
200
|
+
metadata?: Record<string, unknown>,
|
|
201
|
+
op?: 'start' | 'stop',
|
|
202
|
+
) => void;
|
|
183
203
|
private shouldTriggerInference?: (content: string, metadata: Record<string, unknown>) => boolean;
|
|
184
204
|
|
|
185
205
|
/** Registered channels, keyed by `{serverId}:{channelId}`. */
|
|
@@ -195,13 +215,29 @@ export class ChannelRegistry {
|
|
|
195
215
|
/** Per-channel typing indicator timers. */
|
|
196
216
|
private typingIntervals = new Map<string, ReturnType<typeof setInterval>>();
|
|
197
217
|
|
|
218
|
+
/** Per-channel typing metadata — carried on the 7s refresh so the target
|
|
219
|
+
* server keeps getting the same routing hints (e.g. Zulip topic). */
|
|
220
|
+
private typingMetadata = new Map<string, Record<string, unknown>>();
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Per-server channel auto-open policy. Populated by the framework from
|
|
224
|
+
* each McplServerConfig at connect time; default ('auto') applies when
|
|
225
|
+
* unset.
|
|
226
|
+
*/
|
|
227
|
+
private subscriptionPolicies = new Map<string, 'auto' | 'manual' | string[]>();
|
|
228
|
+
|
|
198
229
|
constructor(
|
|
199
230
|
serverRegistry: McplServerRegistry,
|
|
200
231
|
featureSetManager: FeatureSetManager,
|
|
201
232
|
pushEventFn: (event: ProcessEvent) => void,
|
|
202
233
|
emitTraceFn: (event: { type: string; [key: string]: unknown }) => void,
|
|
203
234
|
options?: ChannelRegistryOptions & {
|
|
204
|
-
sendTypingFn?: (
|
|
235
|
+
sendTypingFn?: (
|
|
236
|
+
serverId: string,
|
|
237
|
+
channelId: string,
|
|
238
|
+
metadata?: Record<string, unknown>,
|
|
239
|
+
op?: 'start' | 'stop',
|
|
240
|
+
) => void;
|
|
205
241
|
},
|
|
206
242
|
) {
|
|
207
243
|
this.serverRegistry = serverRegistry;
|
|
@@ -212,6 +248,14 @@ export class ChannelRegistry {
|
|
|
212
248
|
this.shouldTriggerInference = options?.shouldTriggerInference;
|
|
213
249
|
}
|
|
214
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Set the channel auto-open policy for a server. Called by the framework
|
|
253
|
+
* when each MCPL server is registered.
|
|
254
|
+
*/
|
|
255
|
+
setSubscriptionPolicy(serverId: string, policy: 'auto' | 'manual' | string[]): void {
|
|
256
|
+
this.subscriptionPolicies.set(serverId, policy);
|
|
257
|
+
}
|
|
258
|
+
|
|
215
259
|
// ==========================================================================
|
|
216
260
|
// Handler Methods (called from framework.ts wireMcplEvents)
|
|
217
261
|
// ==========================================================================
|
|
@@ -339,7 +383,7 @@ export class ChannelRegistry {
|
|
|
339
383
|
textContent,
|
|
340
384
|
{
|
|
341
385
|
...message.metadata,
|
|
342
|
-
eventType: 'channel
|
|
386
|
+
eventType: 'mcpl:channel-incoming',
|
|
343
387
|
serverId,
|
|
344
388
|
channelId: message.channelId,
|
|
345
389
|
messageId: message.messageId,
|
|
@@ -398,9 +442,27 @@ export class ChannelRegistry {
|
|
|
398
442
|
*
|
|
399
443
|
* No-op if already typing on this channel.
|
|
400
444
|
*/
|
|
401
|
-
startTyping(channelId: string): void {
|
|
445
|
+
startTyping(channelId: string, metadata?: Record<string, unknown>): void {
|
|
446
|
+
const metadataChanged =
|
|
447
|
+
metadata !== undefined &&
|
|
448
|
+
!shallowEqualRecord(this.typingMetadata.get(channelId), metadata);
|
|
449
|
+
if (metadata) {
|
|
450
|
+
this.typingMetadata.set(channelId, metadata);
|
|
451
|
+
}
|
|
452
|
+
|
|
402
453
|
if (this.typingIntervals.has(channelId)) {
|
|
403
|
-
|
|
454
|
+
// Already typing. If the caller supplied new routing metadata (e.g. the
|
|
455
|
+
// relevant Zulip topic just moved because a newer message arrived),
|
|
456
|
+
// dispatch an immediate refresh so the server sees the new routing
|
|
457
|
+
// within this request instead of waiting up to TYPING_INTERVAL_MS for
|
|
458
|
+
// the next tick.
|
|
459
|
+
if (metadataChanged) {
|
|
460
|
+
const entry = this.findChannelEntry(channelId);
|
|
461
|
+
if (entry) {
|
|
462
|
+
this.sendTypingNotification(entry.serverId, channelId);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
404
466
|
}
|
|
405
467
|
|
|
406
468
|
// Find the channel entry and its server
|
|
@@ -412,7 +474,9 @@ export class ChannelRegistry {
|
|
|
412
474
|
// Send typing immediately
|
|
413
475
|
this.sendTypingNotification(entry.serverId, channelId);
|
|
414
476
|
|
|
415
|
-
// Set up interval
|
|
477
|
+
// Set up interval — pulls the latest metadata on each tick so mid-stream
|
|
478
|
+
// updates (e.g. a newer incoming message switching the relevant topic)
|
|
479
|
+
// take effect on the next refresh.
|
|
416
480
|
const interval = setInterval(() => {
|
|
417
481
|
this.sendTypingNotification(entry.serverId, channelId);
|
|
418
482
|
}, TYPING_INTERVAL_MS);
|
|
@@ -432,13 +496,32 @@ export class ChannelRegistry {
|
|
|
432
496
|
if (interval) {
|
|
433
497
|
clearInterval(interval);
|
|
434
498
|
this.typingIntervals.delete(channelId);
|
|
499
|
+
// Dispatch an explicit 'stop' so servers that support it (e.g. Zulip)
|
|
500
|
+
// clear the indicator immediately rather than waiting for auto-expire.
|
|
501
|
+
// Metadata still carries the routing hint so the stop hits the same
|
|
502
|
+
// topic/thread as the start. Guarded by `interval`: matches the
|
|
503
|
+
// global-clear branch's semantics, and keeps defensive stopTyping(ch)
|
|
504
|
+
// calls from spamming stops at a server that never saw a start.
|
|
505
|
+
const entry = this.findChannelEntry(channelId);
|
|
506
|
+
if (entry && this.sendTypingFn) {
|
|
507
|
+
this.sendTypingFn(entry.serverId, channelId, this.typingMetadata.get(channelId), 'stop');
|
|
508
|
+
}
|
|
435
509
|
}
|
|
510
|
+
this.typingMetadata.delete(channelId);
|
|
436
511
|
} else {
|
|
437
|
-
// Clear all typing intervals
|
|
512
|
+
// Clear all typing intervals and dispatch stop for each known channel
|
|
513
|
+
const channels = Array.from(this.typingIntervals.keys());
|
|
438
514
|
for (const interval of this.typingIntervals.values()) {
|
|
439
515
|
clearInterval(interval);
|
|
440
516
|
}
|
|
441
517
|
this.typingIntervals.clear();
|
|
518
|
+
if (this.sendTypingFn) {
|
|
519
|
+
for (const id of channels) {
|
|
520
|
+
const entry = this.findChannelEntry(id);
|
|
521
|
+
if (entry) this.sendTypingFn(entry.serverId, id, this.typingMetadata.get(id), 'stop');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
this.typingMetadata.clear();
|
|
442
525
|
}
|
|
443
526
|
}
|
|
444
527
|
|
|
@@ -574,7 +657,23 @@ export class ChannelRegistry {
|
|
|
574
657
|
const server = this.serverRegistry.getServer(serverId);
|
|
575
658
|
if (!server) return;
|
|
576
659
|
|
|
660
|
+
const policy = this.subscriptionPolicies.get(serverId) ?? 'auto';
|
|
661
|
+
if (policy === 'manual') {
|
|
662
|
+
this.emitTraceFn({
|
|
663
|
+
type: 'mcpl:channels-auto-open-skipped',
|
|
664
|
+
serverId,
|
|
665
|
+
reason: 'manual',
|
|
666
|
+
count: channels.length,
|
|
667
|
+
});
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const allowList = Array.isArray(policy) ? new Set(policy) : null;
|
|
672
|
+
let opened = 0;
|
|
673
|
+
|
|
577
674
|
for (const channel of channels) {
|
|
675
|
+
if (allowList && !allowList.has(channel.id)) continue;
|
|
676
|
+
|
|
578
677
|
const key = `${serverId}:${channel.id}`;
|
|
579
678
|
try {
|
|
580
679
|
await server.sendChannelsOpen({
|
|
@@ -585,6 +684,7 @@ export class ChannelRegistry {
|
|
|
585
684
|
if (entry) {
|
|
586
685
|
entry.open = true;
|
|
587
686
|
}
|
|
687
|
+
opened++;
|
|
588
688
|
} catch (err) {
|
|
589
689
|
this.emitTraceFn({
|
|
590
690
|
type: 'mcpl:channel-open-failed',
|
|
@@ -594,6 +694,18 @@ export class ChannelRegistry {
|
|
|
594
694
|
});
|
|
595
695
|
}
|
|
596
696
|
}
|
|
697
|
+
|
|
698
|
+
// Surface the common typo failure mode: allow-list set but nothing
|
|
699
|
+
// matched. Without this trace a one-char typo in a channel id looks
|
|
700
|
+
// identical to "MCPL didn't register the channel".
|
|
701
|
+
if (allowList && opened === 0 && channels.length > 0) {
|
|
702
|
+
this.emitTraceFn({
|
|
703
|
+
type: 'mcpl:channels-allow-list-empty',
|
|
704
|
+
serverId,
|
|
705
|
+
allowList: [...allowList],
|
|
706
|
+
registered: channels.map(c => c.id),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
597
709
|
}
|
|
598
710
|
|
|
599
711
|
// ==========================================================================
|
|
@@ -608,7 +720,7 @@ export class ChannelRegistry {
|
|
|
608
720
|
*/
|
|
609
721
|
private sendTypingNotification(serverId: string, channelId: string): void {
|
|
610
722
|
if (this.sendTypingFn) {
|
|
611
|
-
this.sendTypingFn(serverId, channelId);
|
|
723
|
+
this.sendTypingFn(serverId, channelId, this.typingMetadata.get(channelId));
|
|
612
724
|
}
|
|
613
725
|
// TODO: When server-connection exposes a public sendNotification or
|
|
614
726
|
// sendTyping method, wire it here directly instead of using a callback.
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -195,7 +195,7 @@ export class PushHandler {
|
|
|
195
195
|
serverId,
|
|
196
196
|
featureSet: params.featureSet,
|
|
197
197
|
eventId: params.eventId,
|
|
198
|
-
eventType: 'push
|
|
198
|
+
eventType: 'mcpl:push-event',
|
|
199
199
|
...(params.origin ?? {}),
|
|
200
200
|
};
|
|
201
201
|
triggerInference = this.shouldTriggerInference(textContent, metadata);
|
|
@@ -95,6 +95,9 @@ export class McplServerConnection extends EventEmitter {
|
|
|
95
95
|
private reconnectIntervalMs = 5000;
|
|
96
96
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
97
97
|
|
|
98
|
+
/** Swaps the line handler on the current child's stderr pipe. Re-bound on reconnect. */
|
|
99
|
+
private rebindStderr: ((next: (line: string) => void) => void) | null = null;
|
|
100
|
+
|
|
98
101
|
/**
|
|
99
102
|
* Private constructor. Use `McplServerConnection.connect()` instead.
|
|
100
103
|
*/
|
|
@@ -151,6 +154,33 @@ export class McplServerConnection extends EventEmitter {
|
|
|
151
154
|
// Static factory
|
|
152
155
|
// ==========================================================================
|
|
153
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Wire stderr forwarding on a child process. Lines are forwarded to the
|
|
159
|
+
* supplied callback. The callback can be swapped at any time via `rebind`
|
|
160
|
+
* (used during reconnect, when we want already-wired pipes to target a new
|
|
161
|
+
* emitter).
|
|
162
|
+
*/
|
|
163
|
+
private static wireStderrForwarding(
|
|
164
|
+
child: ChildProcess,
|
|
165
|
+
initialLineHandler: (line: string) => void,
|
|
166
|
+
): { rebind: (next: (line: string) => void) => void } {
|
|
167
|
+
let onLine = initialLineHandler;
|
|
168
|
+
let carry = '';
|
|
169
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
170
|
+
const text = carry + chunk.toString('utf8');
|
|
171
|
+
const lines = text.split('\n');
|
|
172
|
+
carry = lines.pop() ?? '';
|
|
173
|
+
for (const line of lines) {
|
|
174
|
+
if (line.length > 0) onLine(line);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
rebind(next) {
|
|
179
|
+
onLine = next;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
154
184
|
/**
|
|
155
185
|
* Spawn a server process, perform the MCP initialize handshake with MCPL
|
|
156
186
|
* capability negotiation, and return a ready-to-use connection.
|
|
@@ -166,6 +196,14 @@ export class McplServerConnection extends EventEmitter {
|
|
|
166
196
|
env: { ...process.env, ...config.env },
|
|
167
197
|
});
|
|
168
198
|
|
|
199
|
+
// Capture child stderr so it isn't silently dropped. During the handshake
|
|
200
|
+
// window (before the instance exists) lines are buffered; the buffer is
|
|
201
|
+
// drained once we have an instance to emit 'stderr' events on.
|
|
202
|
+
const earlyStderrBuffer: string[] = [];
|
|
203
|
+
const attachStderr = McplServerConnection.wireStderrForwarding(child, (line) =>
|
|
204
|
+
earlyStderrBuffer.push(line),
|
|
205
|
+
);
|
|
206
|
+
|
|
169
207
|
// Fail fast if the process exits before the handshake completes
|
|
170
208
|
const earlyExitPromise = new Promise<never>((_resolve, reject) => {
|
|
171
209
|
child.on('error', (err) => {
|
|
@@ -251,6 +289,16 @@ export class McplServerConnection extends EventEmitter {
|
|
|
251
289
|
|
|
252
290
|
const connection = new McplServerConnection(config.id, mcplCaps, child, rl);
|
|
253
291
|
|
|
292
|
+
// Swap the stderr handler to emit events on the instance, then drain
|
|
293
|
+
// anything that arrived during the handshake window. Keep the rebind
|
|
294
|
+
// handle on the instance so reconnect can re-target it later.
|
|
295
|
+
connection.rebindStderr = attachStderr.rebind;
|
|
296
|
+
attachStderr.rebind((line: string) => connection.emit('stderr', { line }));
|
|
297
|
+
for (const line of earlyStderrBuffer) {
|
|
298
|
+
connection.emit('stderr', { line });
|
|
299
|
+
}
|
|
300
|
+
earlyStderrBuffer.length = 0;
|
|
301
|
+
|
|
254
302
|
// Store config for reconnection
|
|
255
303
|
if (config.reconnect) {
|
|
256
304
|
connection.config = config;
|
|
@@ -395,9 +443,21 @@ export class McplServerConnection extends EventEmitter {
|
|
|
395
443
|
this.sendNotification(McplMethod.ChannelsOutgoingComplete, params as unknown as Record<string, unknown>);
|
|
396
444
|
}
|
|
397
445
|
|
|
398
|
-
/** Send `channels/typing` notification (best-effort).
|
|
399
|
-
|
|
400
|
-
|
|
446
|
+
/** Send `channels/typing` notification (best-effort).
|
|
447
|
+
* `metadata` is opaque routing hints for the server — e.g. Zulip uses
|
|
448
|
+
* `topic` to target a specific thread; other servers ignore what they don't
|
|
449
|
+
* recognize. Typically sourced from the most recent incoming message's
|
|
450
|
+
* metadata on the same channel.
|
|
451
|
+
* `op` defaults to 'start'; pass 'stop' to clear an active indicator
|
|
452
|
+
* immediately rather than waiting for server-side auto-expire. */
|
|
453
|
+
sendChannelsTyping(
|
|
454
|
+
channelId: string,
|
|
455
|
+
metadata?: Record<string, unknown>,
|
|
456
|
+
op: 'start' | 'stop' = 'start',
|
|
457
|
+
): void {
|
|
458
|
+
const params: Record<string, unknown> = { channelId, op };
|
|
459
|
+
if (metadata) params.metadata = metadata;
|
|
460
|
+
this.sendNotification(McplMethod.ChannelsTyping, params);
|
|
401
461
|
}
|
|
402
462
|
|
|
403
463
|
// ==========================================================================
|
|
@@ -499,6 +559,21 @@ export class McplServerConnection extends EventEmitter {
|
|
|
499
559
|
this.nextRequestId = 1;
|
|
500
560
|
this.pendingRequests.clear();
|
|
501
561
|
|
|
562
|
+
// Re-target stderr forwarding from the throwaway `fresh` instance to `this`.
|
|
563
|
+
//
|
|
564
|
+
// Note: this follows the same "transfer internals, leak listeners on the
|
|
565
|
+
// dead instance" pattern as the message-routing and lifecycle rewiring
|
|
566
|
+
// below — `fresh`'s own readline 'line' handler and process 'exit'
|
|
567
|
+
// handler stay registered on the transferred child, short-circuited by
|
|
568
|
+
// `fresh.closed = true` set in extractInternals(). Rebinding fresh's
|
|
569
|
+
// stderr listener (rather than removing it) keeps that invariant: if we
|
|
570
|
+
// ever clean up the message-routing duplication, fresh's stderr data
|
|
571
|
+
// listener closure-holds onLine and would need the same teardown.
|
|
572
|
+
if (fresh.rebindStderr) {
|
|
573
|
+
fresh.rebindStderr((line: string) => this.emit('stderr', { line }));
|
|
574
|
+
this.rebindStderr = fresh.rebindStderr;
|
|
575
|
+
}
|
|
576
|
+
|
|
502
577
|
// Re-wire message routing and lifecycle on the new process
|
|
503
578
|
this.setupMessageRouting();
|
|
504
579
|
this.setupLifecycle();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool allow/deny policy for MCPL servers.
|
|
3
|
+
*
|
|
4
|
+
* `enabledTools` and `disabledTools` on McplServerConfig hold bare tool names
|
|
5
|
+
* (no toolPrefix) with `*` substring wildcards. This module owns the predicate
|
|
6
|
+
* used at both tool-list time and dispatch time.
|
|
7
|
+
*
|
|
8
|
+
* Semantics:
|
|
9
|
+
* - No fields set → all tools allowed.
|
|
10
|
+
* - Only enabledTools set → tool must match at least one pattern.
|
|
11
|
+
* - Only disabledTools set → tool must NOT match any pattern.
|
|
12
|
+
* - Both set → must match enabledTools AND not match disabledTools.
|
|
13
|
+
* (deny wins on overlap.)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compile a pattern with `*` substring wildcards into a regex.
|
|
18
|
+
* `*` matches any run of characters (including empty); other characters are literal.
|
|
19
|
+
*/
|
|
20
|
+
function compilePattern(pattern: string): RegExp {
|
|
21
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
22
|
+
return new RegExp(`^${escaped}$`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function anyMatch(patterns: string[], name: string): boolean {
|
|
26
|
+
for (const p of patterns) {
|
|
27
|
+
if (compilePattern(p).test(name)) return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ToolPolicy {
|
|
33
|
+
enabledTools?: string[];
|
|
34
|
+
disabledTools?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if `bareToolName` (server-native name, no prefix) is allowed
|
|
39
|
+
* under the supplied policy.
|
|
40
|
+
*/
|
|
41
|
+
export function isToolAllowed(bareToolName: string, policy: ToolPolicy | undefined): boolean {
|
|
42
|
+
if (!policy) return true;
|
|
43
|
+
const { enabledTools, disabledTools } = policy;
|
|
44
|
+
|
|
45
|
+
if (disabledTools && disabledTools.length > 0 && anyMatch(disabledTools, bareToolName)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (enabledTools && enabledTools.length > 0) {
|
|
49
|
+
return anyMatch(enabledTools, bareToolName);
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
package/src/mcpl/types.ts
CHANGED
|
@@ -180,6 +180,24 @@ export interface McplServerConfig {
|
|
|
180
180
|
/** Feature sets to explicitly disable on connect */
|
|
181
181
|
disabledFeatureSets?: string[];
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Tool allow-list (bare tool names as the server exports them, no toolPrefix).
|
|
185
|
+
* Supports `*` as a substring wildcard (e.g. `read_*`, `*_file`, `*`).
|
|
186
|
+
* If set, only tools matching at least one pattern are exposed.
|
|
187
|
+
* `disabledTools` takes precedence on conflict.
|
|
188
|
+
*
|
|
189
|
+
* Filter is applied in two places: at tool-list time (model never sees the
|
|
190
|
+
* tool) and at dispatch time (call rejected with a tool-result error, in
|
|
191
|
+
* case the model imitates a prior call from message history).
|
|
192
|
+
*/
|
|
193
|
+
enabledTools?: string[];
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Tool deny-list (bare tool names, same wildcard syntax as enabledTools).
|
|
197
|
+
* Wins over enabledTools on conflict.
|
|
198
|
+
*/
|
|
199
|
+
disabledTools?: string[];
|
|
200
|
+
|
|
183
201
|
/** Scope configurations per feature set */
|
|
184
202
|
scopes?: Record<string, ScopeConfig>;
|
|
185
203
|
|
|
@@ -222,6 +240,22 @@ export interface McplServerConfig {
|
|
|
222
240
|
* Must not collide with any module name or another server's prefix.
|
|
223
241
|
*/
|
|
224
242
|
toolPrefix?: string;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* How channels registered by this server are opened.
|
|
246
|
+
* - `'auto'` (default) — every registered channel is auto-opened; the host
|
|
247
|
+
* receives all traffic. Matches prior behavior. Right for agents that
|
|
248
|
+
* should listen passively (e.g. a frontdesk clerk).
|
|
249
|
+
* - `'manual'` — registered channels are listed but not opened. The agent
|
|
250
|
+
* must call the `channel_open` tool to start receiving messages. Right
|
|
251
|
+
* for focused agents that should not be flooded with chatter from
|
|
252
|
+
* servers that advertise many public channels (e.g. Zulip's full stream
|
|
253
|
+
* list).
|
|
254
|
+
* - `string[]` — allow-list of channel ids; only matching channels are
|
|
255
|
+
* auto-opened. Ids are server-scoped (the raw id the server registers,
|
|
256
|
+
* e.g. `"zulip:tracker-miner-f"`).
|
|
257
|
+
*/
|
|
258
|
+
channelSubscription?: 'auto' | 'manual' | string[];
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
// ============================================================================
|