@agentmesh-p2p/sdk 0.1.1 → 0.2.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/README.md +60 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +117 -0
- package/dist/gen/agentmesh_pb.d.ts +133 -2
- package/dist/gen/agentmesh_pb.js +111 -72
- package/dist/gen/datalog.d.ts +2 -0
- package/dist/gen/datalog.js +9 -0
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.js +305 -0
- package/dist/mcp.js +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
// The local MCP surface for SDK members. All remote operations use the
|
|
15
|
+
// session's authenticated streams and the member's own credential.
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { BASELINE_DATALOG } from "./gen/datalog.js";
|
|
20
|
+
import { AuthRejectedError } from "./auth.js";
|
|
21
|
+
import { canonicalPeerId } from "./identity.js";
|
|
22
|
+
const MAX_PROVIDERS = 64;
|
|
23
|
+
const MAX_SERVICES = 256;
|
|
24
|
+
const MAX_TOOLS = 256;
|
|
25
|
+
const MAX_RESULTS = 1024;
|
|
26
|
+
// Bounds enrollment, directory lookups and single-peer queries.
|
|
27
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
28
|
+
// Match agentmesh-node: each provider in a mesh-wide fan-out gets its own
|
|
29
|
+
// deadline, at most 8 at a time, and a slow one only costs its own row.
|
|
30
|
+
const PEER_TIMEOUT_MS = 5_000;
|
|
31
|
+
const FANOUT = 8;
|
|
32
|
+
// Opening a call stream is bounded like the node's dial; the call itself is
|
|
33
|
+
// bounded by the MCP client's cancellation, as tools may run for minutes.
|
|
34
|
+
const OPEN_TIMEOUT_MS = 15_000;
|
|
35
|
+
const CALL_TIMEOUT_MS = 30 * 60_000;
|
|
36
|
+
const serviceSchema = z.object({
|
|
37
|
+
type: z.number().int(), name: z.string().min(1), description: z.string().optional(),
|
|
38
|
+
});
|
|
39
|
+
const types = { mcp: 1, inference: 2, a2a: 3 };
|
|
40
|
+
function text(value) {
|
|
41
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
42
|
+
}
|
|
43
|
+
function serviceTarget(name, type = "mcp") {
|
|
44
|
+
const target = name.includes("://") ? name : `${type}://${name}`;
|
|
45
|
+
if (!target.startsWith(`${type}://`) || target.slice(type.length + 3).length === 0) {
|
|
46
|
+
throw new Error(`expected a ${type} service name`);
|
|
47
|
+
}
|
|
48
|
+
return target;
|
|
49
|
+
}
|
|
50
|
+
function toolTarget(name) {
|
|
51
|
+
// Split at the first path slash: MCP tool names themselves can contain '/',
|
|
52
|
+
// '?' or '#'. Only the service name is a path segment on the wire.
|
|
53
|
+
const match = /^mcp:\/\/([^/?#]+)\/(.+)$/s.exec(name);
|
|
54
|
+
if (!match)
|
|
55
|
+
throw new Error("tool_name must be mcp://<service>/<tool>");
|
|
56
|
+
return { service: `mcp://${match[1]}`, tool: match[2] };
|
|
57
|
+
}
|
|
58
|
+
function requiredLabels(value) {
|
|
59
|
+
const labels = Object.create(null);
|
|
60
|
+
if (value.trim() === "")
|
|
61
|
+
return labels;
|
|
62
|
+
for (const pair of value.split(",")) {
|
|
63
|
+
if (pair.trim() === "")
|
|
64
|
+
continue;
|
|
65
|
+
const parts = pair.trim().split("=");
|
|
66
|
+
if (parts.length !== 2 || !parts[0]?.trim() || !parts[1]?.trim()) {
|
|
67
|
+
throw new Error("required_labels must contain comma-separated key=value pairs");
|
|
68
|
+
}
|
|
69
|
+
const key = parts[0].trim(), val = parts[1].trim();
|
|
70
|
+
// Match api.ValidateLabelKey / ValidateLabelValue and the node parser.
|
|
71
|
+
if (!/^[a-zA-Z0-9_.-]{1,63}$/.test(key) || new TextEncoder().encode(val).length > 255 || /[\p{Cc}"\\]/u.test(val)) {
|
|
72
|
+
throw new Error("invalid required label key or value");
|
|
73
|
+
}
|
|
74
|
+
if (Object.hasOwn(labels, key))
|
|
75
|
+
throw new Error(`duplicate required label: ${key}`);
|
|
76
|
+
labels[key] = val;
|
|
77
|
+
}
|
|
78
|
+
if (Object.keys(labels).length === 0)
|
|
79
|
+
throw new Error("expected at least one required label");
|
|
80
|
+
return labels;
|
|
81
|
+
}
|
|
82
|
+
async function call(session, peer, service, name, args, options, request = { signal: options.signal }) {
|
|
83
|
+
const remote = await session.openMCP(peer, service, options);
|
|
84
|
+
try {
|
|
85
|
+
return CallToolResultSchema.parse(await remote.client.callTool({ name, arguments: args }, undefined, request));
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await remote.close();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function catalog(session, peer, type, signal) {
|
|
92
|
+
const result = await call(session, peer, `system://${BASELINE_DATALOG.catalog_target}`, "list_local_services", { type }, { signal });
|
|
93
|
+
const content = result.content[0];
|
|
94
|
+
if (result.isError || content?.type !== "text")
|
|
95
|
+
throw new Error("provider refused its service catalog");
|
|
96
|
+
return z.array(serviceSchema).max(MAX_SERVICES).parse(JSON.parse(content.text)).filter((s) => s.type === types[type]);
|
|
97
|
+
}
|
|
98
|
+
// Follow MCP cursors, but never accept an unbounded or looping catalog.
|
|
99
|
+
async function tools(session, peer, service, signal) {
|
|
100
|
+
const remote = await session.openMCP(peer, service, { signal });
|
|
101
|
+
try {
|
|
102
|
+
const result = [];
|
|
103
|
+
const cursors = new Set();
|
|
104
|
+
let cursor;
|
|
105
|
+
do {
|
|
106
|
+
signal.throwIfAborted();
|
|
107
|
+
const page = await remote.client.listTools(cursor === undefined ? {} : { cursor }, { signal });
|
|
108
|
+
result.push(...page.tools);
|
|
109
|
+
if (result.length > MAX_TOOLS)
|
|
110
|
+
throw new Error(`provider exceeds ${MAX_TOOLS} tools per service`);
|
|
111
|
+
cursor = page.nextCursor;
|
|
112
|
+
if (cursor !== undefined) {
|
|
113
|
+
if (cursors.has(cursor) || cursors.size >= MAX_TOOLS)
|
|
114
|
+
throw new Error("provider returned an unbounded tool catalog");
|
|
115
|
+
cursors.add(cursor);
|
|
116
|
+
}
|
|
117
|
+
} while (cursor !== undefined);
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await remote.close();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Do not keep a cancelled MCP request waiting for enrollment or directory lookup.
|
|
125
|
+
async function withSignal(value, signal) {
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
return new Promise((resolve, reject) => {
|
|
128
|
+
const abort = () => { reject(signal.reason); };
|
|
129
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
130
|
+
Promise.resolve(value).then(resolve, reject).finally(() => { signal.removeEventListener("abort", abort); });
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
class ResultLimitError extends Error {
|
|
134
|
+
constructor() { super("too many tools; narrow the search by peer or service"); }
|
|
135
|
+
}
|
|
136
|
+
// Runs fn for each peer, FANOUT at a time, each under its own deadline. Only
|
|
137
|
+
// the caller's cancellation aborts the whole fan-out.
|
|
138
|
+
async function fanOut(peers, signal, fn) {
|
|
139
|
+
const results = [];
|
|
140
|
+
let next = 0;
|
|
141
|
+
const worker = async () => {
|
|
142
|
+
for (let i = next++; i < peers.length; i = next++) {
|
|
143
|
+
signal.throwIfAborted();
|
|
144
|
+
try {
|
|
145
|
+
results[i] = { status: "fulfilled", value: await fn(peers[i], AbortSignal.any([signal, AbortSignal.timeout(PEER_TIMEOUT_MS)])) };
|
|
146
|
+
}
|
|
147
|
+
catch (reason) {
|
|
148
|
+
results[i] = { status: "rejected", reason };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
await Promise.all(Array.from({ length: Math.min(FANOUT, peers.length) }, worker));
|
|
153
|
+
signal.throwIfAborted();
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
/** The same five caller tools as agentmesh-node; an SDK member hosts no services. */
|
|
157
|
+
export function createMeshMCPServer(ready, version) {
|
|
158
|
+
const server = new McpServer({ name: "agentmesh", version }, { instructions: "Use find_remote_tools, then describe_remote_tool to learn the input schema, then call_remote_tool. " +
|
|
159
|
+
"Calls use this SDK member's identity and mesh policy. This stdio server exposes no local HTTP proxy, inference endpoint or hosted services.",
|
|
160
|
+
});
|
|
161
|
+
server.registerTool("get_mesh_info", { description: "Get information about the mesh network", inputSchema: {} }, async (_args, extra) => {
|
|
162
|
+
const signal = AbortSignal.any([extra.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
163
|
+
const s = await withSignal(ready, signal);
|
|
164
|
+
const routers = s.routers.filter((r) => s.isHeldRouter(r.peerId)).map((r) => r.peerId).sort();
|
|
165
|
+
return text({ peer_id: s.peerId, connected_peers: s.node.getPeers().map((p) => p.toString()).sort(), attached_routers: routers, router_peer_id: routers[0] ?? "" });
|
|
166
|
+
});
|
|
167
|
+
server.registerTool("discover_remote_services", {
|
|
168
|
+
description: "Discover remote services by type and optional name. Returns peer_id, srv_name and srv_description. This SDK member has no local_proxy_url; inference and A2A services require an SDK client or an agentmesh-node HTTP proxy.",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
type: z.string().describe("Required. One of: mcp, inference, a2a."),
|
|
171
|
+
name: z.string().optional().describe("Optional service name. Omit to list all services of the given type."),
|
|
172
|
+
limit: z.number().int().optional().describe("Optional limit for pagination. Defaults to 20."),
|
|
173
|
+
offset: z.number().int().optional().describe("Optional offset for pagination. Defaults to 0."),
|
|
174
|
+
},
|
|
175
|
+
}, async (args, extra) => {
|
|
176
|
+
const setup = AbortSignal.any([extra.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
177
|
+
const s = await withSignal(ready, setup);
|
|
178
|
+
const type = args.type.toLowerCase();
|
|
179
|
+
if (types[type] === undefined)
|
|
180
|
+
throw new Error("type must be mcp, inference or a2a");
|
|
181
|
+
const name = args.name ? serviceTarget(args.name, type).slice(type.length + 3) : undefined;
|
|
182
|
+
const peers = await withSignal(s.discover(type, name, { limit: MAX_PROVIDERS }), setup);
|
|
183
|
+
const rows = [];
|
|
184
|
+
if (name !== undefined) {
|
|
185
|
+
// As in agentmesh-node, a named lookup is answered by the directory
|
|
186
|
+
// alone; no provider is contacted, so none can fail or stall it.
|
|
187
|
+
for (const peer of peers)
|
|
188
|
+
rows.push({ peer_id: peer.peerId, srv_name: `${type}://${name}`, srv_description: "" });
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// A provider that fails or stalls is dropped, as in agentmesh-node.
|
|
192
|
+
const catalogs = await fanOut(peers, extra.signal, (peer, signal) => catalog(s, peer, type, signal));
|
|
193
|
+
catalogs.forEach((result, i) => {
|
|
194
|
+
if (result.status === "rejected") {
|
|
195
|
+
console.error(`agentmesh: catalog from ${peers[i].peerId} failed: ${JSON.stringify(String(result.reason))}`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const svc of result.value) {
|
|
199
|
+
rows.push({ peer_id: peers[i].peerId, srv_name: serviceTarget(svc.name, type), srv_description: svc.description ?? "" });
|
|
200
|
+
if (rows.length > MAX_RESULTS)
|
|
201
|
+
throw new Error("too many services; narrow the discovery by name");
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
rows.sort((a, b) => a.peer_id.localeCompare(b.peer_id) || a.srv_name.localeCompare(b.srv_name));
|
|
206
|
+
const offset = Math.max(0, args.offset ?? 0), limit = (args.limit ?? 0) > 0 ? args.limit : 20;
|
|
207
|
+
return text(rows.slice(offset, offset + limit));
|
|
208
|
+
});
|
|
209
|
+
server.registerTool("find_remote_tools", {
|
|
210
|
+
description: "Discover MCP tools available on hosted services across the mesh. Optionally narrow by peer_id, service_name, or exact tool_name. intent is accepted but ignored, as in agentmesh-node.",
|
|
211
|
+
inputSchema: {
|
|
212
|
+
intent: z.string().optional().describe("Natural-language description of what the caller is looking for. Reserved for future semantic ranking; currently accepted but ignored."),
|
|
213
|
+
peer_id: z.string().optional().describe("Restrict the search to a single peer. Empty means search the whole mesh."),
|
|
214
|
+
service_name: z.string().optional().describe("Restrict results to tools whose name starts with this service prefix (e.g. 'code-reviewer'). Empty means no service filter."),
|
|
215
|
+
tool_name: z.string().optional().describe("Exact (non-namespaced) tool name to locate across the mesh, e.g. 'review_pr'. Empty means list all tools."),
|
|
216
|
+
},
|
|
217
|
+
}, async (args, extra) => {
|
|
218
|
+
const setup = AbortSignal.any([extra.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
219
|
+
const s = await withSignal(ready, setup);
|
|
220
|
+
const filter = args.service_name ? serviceTarget(args.service_name) : undefined;
|
|
221
|
+
const peerId = args.peer_id ? canonicalPeerId(args.peer_id) : undefined;
|
|
222
|
+
if (peerId === s.peerId)
|
|
223
|
+
throw new Error("cross-mesh discovery cannot target self");
|
|
224
|
+
const peers = peerId ? [{ peerId, addrs: [] }] : await withSignal(s.discover("mcp", undefined, { limit: MAX_PROVIDERS }), setup);
|
|
225
|
+
const rows = [];
|
|
226
|
+
const append = (row) => {
|
|
227
|
+
if (rows.length >= MAX_RESULTS)
|
|
228
|
+
throw new ResultLimitError();
|
|
229
|
+
rows.push(row);
|
|
230
|
+
};
|
|
231
|
+
const results = await fanOut(peers, extra.signal, async (peer, signal) => {
|
|
232
|
+
const found = [];
|
|
233
|
+
for (const svc of await catalog(s, peer, "mcp", signal)) {
|
|
234
|
+
const target = serviceTarget(svc.name);
|
|
235
|
+
if (filter !== undefined && target !== filter)
|
|
236
|
+
continue;
|
|
237
|
+
try {
|
|
238
|
+
for (const tool of await tools(s, peer, target, signal)) {
|
|
239
|
+
if (!args.tool_name || tool.name === args.tool_name) {
|
|
240
|
+
found.push({ peer_id: peer.peerId, tool_name: `${target}/${tool.name}`, description: tool.description ?? "" });
|
|
241
|
+
if (found.length > MAX_RESULTS)
|
|
242
|
+
throw new ResultLimitError();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
// The peer's deadline ends the peer, not just this service.
|
|
248
|
+
signal.throwIfAborted();
|
|
249
|
+
if (err instanceof ResultLimitError)
|
|
250
|
+
throw err;
|
|
251
|
+
if (err instanceof AuthRejectedError)
|
|
252
|
+
console.error(`agentmesh: discovery refused: ${JSON.stringify(err.message)}`);
|
|
253
|
+
else
|
|
254
|
+
found.push({ peer_id: peer.peerId, tool_name: target, error: String(err) });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return found;
|
|
258
|
+
});
|
|
259
|
+
results.forEach((result, i) => {
|
|
260
|
+
if (result.status === "fulfilled") {
|
|
261
|
+
result.value.forEach(append);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const err = result.reason;
|
|
265
|
+
if (err instanceof ResultLimitError)
|
|
266
|
+
throw err;
|
|
267
|
+
if (peerId !== undefined)
|
|
268
|
+
throw err;
|
|
269
|
+
if (err instanceof AuthRejectedError)
|
|
270
|
+
console.error(`agentmesh: discovery refused: ${JSON.stringify(err.message)}`);
|
|
271
|
+
else
|
|
272
|
+
append({ peer_id: peers[i].peerId, error: String(err) });
|
|
273
|
+
});
|
|
274
|
+
return text(rows.filter((row) => !args.tool_name || !row.error).sort((a, b) => a.peer_id.localeCompare(b.peer_id) || (a.tool_name ?? "").localeCompare(b.tool_name ?? "")));
|
|
275
|
+
});
|
|
276
|
+
server.registerTool("describe_remote_tool", {
|
|
277
|
+
description: "Return the description, input schema, and output schema for a specific MCP tool on a specific peer.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
peer_id: z.string().describe("Peer ID of the node hosting the server. Required."),
|
|
280
|
+
tool_name: z.string().describe("Namespaced server name as returned by find_remote_tools (e.g. 'mcp://code-reviewer/review_pr'). Required."),
|
|
281
|
+
},
|
|
282
|
+
}, async (args, extra) => {
|
|
283
|
+
const signal = AbortSignal.any([extra.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]);
|
|
284
|
+
const s = await withSignal(ready, signal), peer = canonicalPeerId(args.peer_id), target = toolTarget(args.tool_name);
|
|
285
|
+
const tool = (await tools(s, peer, target.service, signal)).find((t) => t.name === target.tool);
|
|
286
|
+
if (tool === undefined)
|
|
287
|
+
throw new Error("tool not found on peer");
|
|
288
|
+
return text({ peer_id: peer, tool_name: args.tool_name, description: tool.description ?? "", input_schema: tool.inputSchema, ...(tool.outputSchema ? { output_schema: tool.outputSchema } : {}) });
|
|
289
|
+
});
|
|
290
|
+
server.registerTool("call_remote_tool", {
|
|
291
|
+
description: "Call an MCP tool on a remote agent",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
peer_id: z.string().describe("The Peer ID of the target agent"),
|
|
294
|
+
tool_name: z.string().describe("The name of the server to call"),
|
|
295
|
+
arguments: z.record(z.string(), z.unknown()).optional().describe("Server arguments as a JSON object whose keys match the target server's input_schema. Call describe_remote_tool first to learn the schema."),
|
|
296
|
+
required_labels: z.string().optional().describe("Comma-separated key=value pairs (e.g. 'region=us-east-1,team=platform'). Fails closed: the call is rejected unless the peer attests every one of them. Empty means no requirement."),
|
|
297
|
+
},
|
|
298
|
+
}, async (args, extra) => {
|
|
299
|
+
const s = await withSignal(ready, AbortSignal.any([extra.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]));
|
|
300
|
+
const peer = canonicalPeerId(args.peer_id), target = toolTarget(args.tool_name);
|
|
301
|
+
const open = AbortSignal.any([extra.signal, AbortSignal.timeout(OPEN_TIMEOUT_MS)]);
|
|
302
|
+
return call(s, peer, target.service, target.tool, args.arguments ?? {}, { signal: open, requiredLabels: requiredLabels(args.required_labels ?? "") }, { signal: extra.signal, timeout: CALL_TIMEOUT_MS, resetTimeoutOnProgress: true });
|
|
303
|
+
});
|
|
304
|
+
return server;
|
|
305
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -21,7 +21,7 @@ import { decodeAuthFrame, decodeAuthResponse } from "./credential.js";
|
|
|
21
21
|
/** go-msgio's default message cap, which agentmesh-node's StreamTransport uses. */
|
|
22
22
|
export const MAX_MCP_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
23
23
|
/** The MCP client the SDK identifies itself as. */
|
|
24
|
-
export const MCP_CLIENT_INFO = { name: "agent-mesh-sdk", version: "0.
|
|
24
|
+
export const MCP_CLIENT_INFO = { name: "agent-mesh-sdk", version: "0.2.0" };
|
|
25
25
|
/**
|
|
26
26
|
* A Transport for @modelcontextprotocol/sdk over a libp2p stream that has
|
|
27
27
|
* already passed the auth handshake.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentmesh-p2p/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Native SDK for joining an Agent Mesh from a Node.js agent process or a browser page",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"directory": "sdk/js"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"agentmesh": "./dist/cli.js"
|
|
14
|
+
},
|
|
12
15
|
"engines": {
|
|
13
16
|
"node": ">=22.18"
|
|
14
17
|
},
|
|
@@ -72,5 +75,6 @@
|
|
|
72
75
|
"esbuild": "^0.28.2",
|
|
73
76
|
"express": "^5.2.1",
|
|
74
77
|
"typescript": "^7.0.2"
|
|
75
|
-
}
|
|
78
|
+
},
|
|
79
|
+
"mcpName": "io.github.google/agentmesh"
|
|
76
80
|
}
|