dsh-a2a-groups 0.0.0-stage → 0.1.2
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-ui.md +350 -0
- package/README.md +160 -2
- package/bin/agent-groups.js +438 -0
- package/client.js +3782 -0
- package/cordis.patch.yml +17 -0
- package/docs/ARCHITECTURE.md +281 -0
- package/docs/BACKLOG.md +201 -0
- package/docs/CONTRACT.md +592 -0
- package/docs/DEPLOY.md +645 -0
- package/docs/DESIGN.md +390 -0
- package/docs/MCP.md +260 -0
- package/docs/PRODUCT.md +186 -0
- package/docs/RUNBOOK.md +406 -0
- package/lib/core/bridge.js +1167 -0
- package/lib/core/card.js +217 -0
- package/lib/core/colleague-cards.js +149 -0
- package/lib/core/config.js +235 -0
- package/lib/core/endpoint.js +69 -0
- package/lib/core/groups.js +3010 -0
- package/lib/core/invite.js +412 -0
- package/lib/core/memberships.js +366 -0
- package/lib/core/probe.js +219 -0
- package/lib/core/registry.js +344 -0
- package/lib/core/server.js +1304 -0
- package/lib/core/skills-group.js +1164 -0
- package/lib/core/skills-session.js +116 -0
- package/lib/core/skills.js +76 -0
- package/lib/core/wire.js +772 -0
- package/lib/dsh/deliver.js +601 -0
- package/lib/dsh/inbox.js +658 -0
- package/lib/dsh/profile.js +195 -0
- package/lib/dsh/prompt.js +55 -0
- package/lib/dsh/sessions.js +278 -0
- package/lib/dsh/toolkit.js +166 -0
- package/lib/dsh/tools.js +101 -0
- package/lib/index.js +648 -0
- package/lib/mcp/mcp-deps.js +145 -0
- package/lib/mcp-server.js +261 -0
- package/lib/shared/tool-runtime.js +1349 -0
- package/lib/shared/tool-specs.js +1123 -0
- package/lib/types/client.d.ts +144 -0
- package/package.json +84 -4
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependencies for the MCP server: everything the group tools need, wired for a process
|
|
3
|
+
* that has no DSH, no Session, and no local group store.
|
|
4
|
+
*
|
|
5
|
+
* THE SHAPE OF THE PROBLEM
|
|
6
|
+
*
|
|
7
|
+
* In DSH the group tools are wired to a plugin context: a local store for groups this
|
|
8
|
+
* instance hosts, a session service, an inbox. An MCP client has none of that. It has one
|
|
9
|
+
* thing: an address for a remote group host, and a credential to reach it.
|
|
10
|
+
*
|
|
11
|
+
* That is not a limitation, it is the normal case. A colleague's DSH instance talks to the
|
|
12
|
+
* shared host over A2A; this process does exactly the same, just without a DSH around it.
|
|
13
|
+
* So `store` is empty (this instance hosts nothing), `sessions` is absent (there are no
|
|
14
|
+
* Sessions to deliver into), and `bridge` does all the real work.
|
|
15
|
+
*
|
|
16
|
+
* WHAT IS REUSED
|
|
17
|
+
*
|
|
18
|
+
* `bridge.js`, `registry.js`, `memberships.js` and `config.js` are the same modules the
|
|
19
|
+
* plugin uses. That is why the tools behave identically in both hosts: the transport
|
|
20
|
+
* differs, the machinery does not.
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-a2a/mcp-deps
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { createBridge } from '../core/bridge.js';
|
|
26
|
+
import { createMembershipLedger } from '../core/memberships.js';
|
|
27
|
+
import { createRegistry } from '../core/registry.js';
|
|
28
|
+
import { normalizeConfig } from '../core/config.js';
|
|
29
|
+
|
|
30
|
+
/** A logger that writes to stderr: stdout is the MCP channel and must stay clean. */
|
|
31
|
+
function stderrLogger(verbose) {
|
|
32
|
+
const stamp = () => new Date().toISOString();
|
|
33
|
+
return {
|
|
34
|
+
info: (line) => process.stderr.write(`${stamp()} info ${line}\n`),
|
|
35
|
+
warn: (line) => process.stderr.write(`${stamp()} warn ${line}\n`),
|
|
36
|
+
error: (line) => process.stderr.write(`${stamp()} error ${line}\n`),
|
|
37
|
+
debug: (line) => {
|
|
38
|
+
if (verbose) process.stderr.write(`${stamp()} debug ${line}\n`);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A store that refuses to pretend.
|
|
45
|
+
*
|
|
46
|
+
* The group mutators branch on `store.has(groupId)` to choose between a local call and an
|
|
47
|
+
* A2A call. In an MCP process nothing is hosted locally, so `has` is always false and the
|
|
48
|
+
* A2A path is always taken. Every other method throws rather than returning a plausible
|
|
49
|
+
* empty value: a silent empty answer from `list()` would look like "you are in no groups",
|
|
50
|
+
* which is a lie about a remote state this process cannot see.
|
|
51
|
+
*/
|
|
52
|
+
function remoteOnlyStore() {
|
|
53
|
+
const refuse = () => {
|
|
54
|
+
throw new Error('this process hosts no groups; pass `agent` so the call goes to a remote host over A2A');
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
has: () => false,
|
|
58
|
+
list: () => [],
|
|
59
|
+
enabled: () => false,
|
|
60
|
+
create: refuse,
|
|
61
|
+
invite: refuse,
|
|
62
|
+
join: refuse,
|
|
63
|
+
post: refuse,
|
|
64
|
+
read: refuse,
|
|
65
|
+
members: refuse,
|
|
66
|
+
leave: refuse,
|
|
67
|
+
snapshot: refuse,
|
|
68
|
+
authenticate: refuse,
|
|
69
|
+
requestJoin: refuse,
|
|
70
|
+
listRequests: refuse,
|
|
71
|
+
decideRequest: refuse,
|
|
72
|
+
claimJoin: refuse,
|
|
73
|
+
listDirectory: refuse,
|
|
74
|
+
revokeInvite: refuse,
|
|
75
|
+
setProfile: refuse,
|
|
76
|
+
memberView: refuse,
|
|
77
|
+
lastEntry: refuse,
|
|
78
|
+
subscribe: () => () => {},
|
|
79
|
+
flush: async () => {},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the dependency bundle from the environment.
|
|
85
|
+
*
|
|
86
|
+
* Recognised variables, mirroring the host's own naming so one deployment can use the
|
|
87
|
+
* same words on both sides:
|
|
88
|
+
*
|
|
89
|
+
* AGENT_GROUPS_GATEWAY gateway base, e.g. http://gw:3000/a2a
|
|
90
|
+
* AGENT_GROUPS_AGENT the host's agent name in that gateway, e.g. agent-groups
|
|
91
|
+
* AGENT_GROUPS_TOKEN bearer token for the host
|
|
92
|
+
* AGENT_GROUPS_DATA directory for memberships.json (default ~/.agent-groups)
|
|
93
|
+
* AGENT_GROUPS_VERBOSE "1" for debug logging on stderr
|
|
94
|
+
*
|
|
95
|
+
* @param {object} env - usually `process.env`.
|
|
96
|
+
* @returns {object} the deps object `createToolRuntime` expects.
|
|
97
|
+
*/
|
|
98
|
+
export function createHostDepsFromEnv(env = process.env) {
|
|
99
|
+
const gatewayBase = typeof env.AGENT_GROUPS_GATEWAY === 'string' ? env.AGENT_GROUPS_GATEWAY.trim() : '';
|
|
100
|
+
const agentName = typeof env.AGENT_GROUPS_AGENT === 'string' ? env.AGENT_GROUPS_AGENT.trim() : '';
|
|
101
|
+
const token = typeof env.AGENT_GROUPS_TOKEN === 'string' ? env.AGENT_GROUPS_TOKEN.trim() : '';
|
|
102
|
+
const dataDir = typeof env.AGENT_GROUPS_DATA === 'string' && env.AGENT_GROUPS_DATA !== ''
|
|
103
|
+
? env.AGENT_GROUPS_DATA
|
|
104
|
+
: `${process.cwd()}/.agent-groups`;
|
|
105
|
+
const verbose = env.AGENT_GROUPS_VERBOSE === '1';
|
|
106
|
+
|
|
107
|
+
const logger = stderrLogger(verbose);
|
|
108
|
+
const config = normalizeConfig({
|
|
109
|
+
groups: {
|
|
110
|
+
gatewayBase,
|
|
111
|
+
agentName,
|
|
112
|
+
home: dataDir,
|
|
113
|
+
},
|
|
114
|
+
client: {
|
|
115
|
+
apiKey: token,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const clock = () => Date.now();
|
|
120
|
+
const bridge = createBridge({ logger, config, clock });
|
|
121
|
+
const registry = createRegistry({ home: dataDir, logger, clock });
|
|
122
|
+
const identities = createMembershipLedger({ home: dataDir, logger, clock });
|
|
123
|
+
|
|
124
|
+
if (gatewayBase === '' || agentName === '') {
|
|
125
|
+
logger.warn(
|
|
126
|
+
'AGENT_GROUPS_GATEWAY and AGENT_GROUPS_AGENT are not set: tools that need the host will fail. '
|
|
127
|
+
+ 'Set both, e.g. AGENT_GROUPS_GATEWAY=http://gw:3000/a2a AGENT_GROUPS_AGENT=agent-groups',
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
config,
|
|
133
|
+
registry,
|
|
134
|
+
bridge,
|
|
135
|
+
// Nothing is hosted in this process; every group operation goes over A2A.
|
|
136
|
+
store: remoteOnlyStore(),
|
|
137
|
+
identities,
|
|
138
|
+
logger,
|
|
139
|
+
// No Sessions exist here. The group tools that need one pass their own connection id,
|
|
140
|
+
// and the ones that would deliver into a Session are simply never used.
|
|
141
|
+
sessions: undefined,
|
|
142
|
+
server: undefined,
|
|
143
|
+
inbox: undefined,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* An MCP server for collaboration groups, over stdio.
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS IS FOR
|
|
6
|
+
*
|
|
7
|
+
* The DSH plugin covers people who use DSH. Everyone else — CodeBuddy, Claude Code,
|
|
8
|
+
* Cursor, any MCP client — could not reach these groups at all. MCP is the lingua franca
|
|
9
|
+
* for "an agent with tools", so exposing the same nineteen tools here makes the groups
|
|
10
|
+
* usable from any of them.
|
|
11
|
+
*
|
|
12
|
+
* WHAT IT IS NOT
|
|
13
|
+
*
|
|
14
|
+
* There is no UI. MCP has no concept of panels or buttons; it has tools, resources and
|
|
15
|
+
* prompts. So the member panel, the tabs and the @ button exist only in the DSH plugin.
|
|
16
|
+
* This server offers the tools and nothing else, which is the right shape: an agent that
|
|
17
|
+
* can call `a2a_group_post` does not need to see a roster widget.
|
|
18
|
+
*
|
|
19
|
+
* WHY STDIO
|
|
20
|
+
*
|
|
21
|
+
* The host is a remote A2A service; MCP is only how a local agent reaches the tools. stdio
|
|
22
|
+
* means the client spawns this process, so there is no port to open, no TLS to arrange,
|
|
23
|
+
* and no credential to distribute beyond the one this process uses to talk to the host.
|
|
24
|
+
* An HTTP transport can be added later without touching anything below, because the
|
|
25
|
+
* request handling is transport-neutral.
|
|
26
|
+
*
|
|
27
|
+
* PROTOCOL
|
|
28
|
+
*
|
|
29
|
+
* JSON-RPC 2.0, one message per line on stdin, one response per line on stdout. Newline
|
|
30
|
+
* framing only: MCP's stdio transport forbids embedded newlines in a message, which is
|
|
31
|
+
* why every response is serialized compactly.
|
|
32
|
+
*
|
|
33
|
+
* @module dsh-a2a/mcp-server
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { createInterface } from 'node:readline';
|
|
37
|
+
|
|
38
|
+
import { allSpecs, specOf } from './shared/tool-specs.js';
|
|
39
|
+
import { createToolRuntime } from './shared/tool-runtime.js';
|
|
40
|
+
|
|
41
|
+
/** The MCP revision this server implements. */
|
|
42
|
+
export const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
43
|
+
|
|
44
|
+
/** Tools that need a Session id; on MCP the connection supplies one stable id. */
|
|
45
|
+
const CONNECTION_SESSION_ID = 'mcp';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build the request handler.
|
|
49
|
+
*
|
|
50
|
+
* Separated from the transport so a test — or a future HTTP transport — can drive it
|
|
51
|
+
* directly without spawning a child process.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} options - `{ deps, sessionId, version }`.
|
|
54
|
+
* @returns {{handle: (request: object) => Promise<object|null>, listTools: () => object[]}} the handler.
|
|
55
|
+
*/
|
|
56
|
+
export function createMcpHandler({ deps = {}, sessionId = CONNECTION_SESSION_ID, version = '0.1.0' } = {}) {
|
|
57
|
+
const runtime = createToolRuntime(deps);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The tool list MCP clients see.
|
|
61
|
+
*
|
|
62
|
+
* `inputSchema` is the same JSON Schema the DSH plugin registers, which is the whole
|
|
63
|
+
* point of splitting the specs out: a model that learned the tool in one host meets the
|
|
64
|
+
* identical contract in the other.
|
|
65
|
+
*/
|
|
66
|
+
function listTools() {
|
|
67
|
+
return allSpecs().map((spec) => ({
|
|
68
|
+
name: spec.name,
|
|
69
|
+
description: spec.description,
|
|
70
|
+
inputSchema: spec.parameters,
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run one tool and shape the result as MCP content.
|
|
76
|
+
*
|
|
77
|
+
* The spec's own `render` produces the human-readable parts, so the text an MCP client
|
|
78
|
+
* shows is the same text a DSH user sees in a tool card — not a JSON dump that happens
|
|
79
|
+
* to be in the same shape.
|
|
80
|
+
*/
|
|
81
|
+
async function callTool(params) {
|
|
82
|
+
const name = typeof params?.name === 'string' ? params.name : '';
|
|
83
|
+
const spec = specOf(name);
|
|
84
|
+
if (spec === undefined) {
|
|
85
|
+
return {
|
|
86
|
+
isError: true,
|
|
87
|
+
content: [{ type: 'text', text: `unknown tool: ${name}` }],
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const args = params?.arguments;
|
|
91
|
+
try {
|
|
92
|
+
const value = await runtime.call(name, args ?? {}, { sessionId });
|
|
93
|
+
// `render` is the spec's own presenter; using it keeps the two hosts consistent.
|
|
94
|
+
const parts = spec.output.render(args ?? {}, value);
|
|
95
|
+
return {
|
|
96
|
+
content: parts.map((part) => ({
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: typeof part?.text === 'string' ? part.text : JSON.stringify(value),
|
|
99
|
+
})),
|
|
100
|
+
// The structured value rides along for a client that wants to act on it rather
|
|
101
|
+
// than display it. Non-standard but additive: clients that ignore it lose nothing.
|
|
102
|
+
structuredContent: value,
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
const code = error?.code === undefined ? '' : `[${error.code}] `;
|
|
107
|
+
return {
|
|
108
|
+
isError: true,
|
|
109
|
+
content: [{ type: 'text', text: `${code}${message}` }],
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Dispatch one JSON-RPC request.
|
|
116
|
+
*
|
|
117
|
+
* @param {object} request - the parsed message.
|
|
118
|
+
* @returns {Promise<object|null>} the response, or null for a notification.
|
|
119
|
+
*/
|
|
120
|
+
async function handle(request) {
|
|
121
|
+
if (request === null || typeof request !== 'object') {
|
|
122
|
+
return { jsonrpc: '2.0', id: null, error: { code: -32600, message: 'invalid request' } };
|
|
123
|
+
}
|
|
124
|
+
const { id, method, params } = request;
|
|
125
|
+
// A request without an id is a notification: answer nothing, not even an error.
|
|
126
|
+
const isNotification = id === undefined || id === null;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
switch (method) {
|
|
130
|
+
case 'initialize':
|
|
131
|
+
return {
|
|
132
|
+
jsonrpc: '2.0',
|
|
133
|
+
id,
|
|
134
|
+
result: {
|
|
135
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
136
|
+
capabilities: { tools: { listChanged: false } },
|
|
137
|
+
serverInfo: { name: 'agent-groups', version },
|
|
138
|
+
// This preamble is the only place a model learns that it has to ASK. MCP notifications cannot reach
|
|
139
|
+
// it — they land in the client program, and no mainstream client turns one into a turn — so without
|
|
140
|
+
// this sentence a colleague's message sits unread forever and the model has no reason to suspect it
|
|
141
|
+
// exists.
|
|
142
|
+
instructions:
|
|
143
|
+
'Collaboration groups for this machine. THIS SERVER CANNOT REACH YOU: it cannot notify you when '
|
|
144
|
+
+ 'a colleague posts or invites you, so checking is your job.\n\n'
|
|
145
|
+
+ 'Call a2a_pending at the START of a turn, and again every few minutes during long work. It '
|
|
146
|
+
+ 'returns unread group messages plus one-to-one invitations in one call. If it reports nothing, '
|
|
147
|
+
+ 'say so briefly and carry on — do not silently skip it.\n\n'
|
|
148
|
+
+ 'An invitation blocks progress: join with a2a_group_join (the message carries a ready link) or '
|
|
149
|
+
+ 'decline it, then a2a_person_ack so the sender learns it arrived.\n\n'
|
|
150
|
+
+ 'Find groups with a2a_group_directory; join without an invite via a2a_group_request then '
|
|
151
|
+
+ 'a2a_group_claim once approved. Speak with a2a_group_post — add \`mentions\` to summon specific '
|
|
152
|
+
+ 'people, omit it for ordinary progress that only raises an unread count.',
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
case 'notifications/initialized':
|
|
156
|
+
case 'initialized':
|
|
157
|
+
// Acknowledged by doing nothing: the client is telling us it is ready.
|
|
158
|
+
return null;
|
|
159
|
+
case 'ping':
|
|
160
|
+
return { jsonrpc: '2.0', id, result: {} };
|
|
161
|
+
case 'tools/list':
|
|
162
|
+
return { jsonrpc: '2.0', id, result: { tools: listTools() } };
|
|
163
|
+
case 'tools/call':
|
|
164
|
+
return { jsonrpc: '2.0', id, result: await callTool(params) };
|
|
165
|
+
case 'resources/list':
|
|
166
|
+
// No resources: this server exposes actions, not documents. Answering with an
|
|
167
|
+
// empty list is better than method-not-found, because a client that probes will
|
|
168
|
+
// stop asking once it sees the capability is present but empty.
|
|
169
|
+
return { jsonrpc: '2.0', id, result: { resources: [] } };
|
|
170
|
+
case 'prompts/list':
|
|
171
|
+
return { jsonrpc: '2.0', id, result: { prompts: [] } };
|
|
172
|
+
default:
|
|
173
|
+
if (isNotification) return null;
|
|
174
|
+
return {
|
|
175
|
+
jsonrpc: '2.0',
|
|
176
|
+
id,
|
|
177
|
+
error: { code: -32601, message: `method not found: ${String(method)}` },
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (isNotification) return null;
|
|
182
|
+
return {
|
|
183
|
+
jsonrpc: '2.0',
|
|
184
|
+
id,
|
|
185
|
+
error: {
|
|
186
|
+
code: -32603,
|
|
187
|
+
message: error instanceof Error ? error.message : String(error),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { handle, listTools, runtime };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Serve MCP over stdio until stdin closes.
|
|
198
|
+
*
|
|
199
|
+
* @param {object} options - passed through to `createMcpHandler`.
|
|
200
|
+
* @returns {Promise<void>} resolves when the input stream ends.
|
|
201
|
+
*/
|
|
202
|
+
export async function serveStdio(options = {}) {
|
|
203
|
+
const handler = createMcpHandler(options);
|
|
204
|
+
|
|
205
|
+
/** Write one response as a single line. A newline inside the JSON would break framing. */
|
|
206
|
+
const write = (message) => {
|
|
207
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Work through messages in order: tools may touch the store, and interleaving two calls
|
|
211
|
+
// that both mutate a group would be a race the group code does not expect.
|
|
212
|
+
let pending = Promise.resolve();
|
|
213
|
+
const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
214
|
+
|
|
215
|
+
lines.on('line', (line) => {
|
|
216
|
+
const text = line.trim();
|
|
217
|
+
if (text === '') return;
|
|
218
|
+
pending = pending.then(async () => {
|
|
219
|
+
let request;
|
|
220
|
+
try {
|
|
221
|
+
request = JSON.parse(text);
|
|
222
|
+
} catch {
|
|
223
|
+
// A malformed line gets a parse error and the stream continues: killing the
|
|
224
|
+
// server over one bad byte would take down every later call.
|
|
225
|
+
write({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const response = await handler.handle(request);
|
|
229
|
+
if (response !== null) write(response);
|
|
230
|
+
}).catch((error) => {
|
|
231
|
+
write({
|
|
232
|
+
jsonrpc: '2.0',
|
|
233
|
+
id: null,
|
|
234
|
+
error: { code: -32603, message: error instanceof Error ? error.message : String(error) },
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await new Promise((resolve) => {
|
|
240
|
+
lines.on('close', resolve);
|
|
241
|
+
});
|
|
242
|
+
await pending;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Only run when invoked directly: importing this file must have no side effects, because
|
|
246
|
+
// the tests import the handler.
|
|
247
|
+
const invokedDirectly = process.argv[1] !== undefined
|
|
248
|
+
&& /mcp-server\.js$/.test(process.argv[1].replace(/\\/g, '/'));
|
|
249
|
+
|
|
250
|
+
if (invokedDirectly) {
|
|
251
|
+
// The dependencies a real run needs. `deps` is intentionally empty here: the group tools
|
|
252
|
+
// reach a REMOTE host over A2A, so the only state this process needs is its own
|
|
253
|
+
// configuration, which arrives through the environment.
|
|
254
|
+
// Dynamic because importing the deps would construct a bridge, a registry and a ledger
|
|
255
|
+
// at module load — side effects a test that only wants the handler must not inherit.
|
|
256
|
+
const { createHostDepsFromEnv } = await import('./mcp/mcp-deps.js');
|
|
257
|
+
serveStdio({ deps: createHostDepsFromEnv(process.env) }).catch((error) => {
|
|
258
|
+
process.stderr.write(`agent-groups-mcp: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
});
|
|
261
|
+
}
|