@nebutra/mcp 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +21 -676
- package/README.md +81 -91
- package/dist/{chunk-73DKWF2B.js → chunk-25TNKUCR.js} +190 -26
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +1 -1
- package/package.json +22 -12
- package/.turbo/turbo-build.log +0 -22
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -62
- package/bin/nebutra-mcp.js +0 -5
- package/examples/doctor.ts +0 -14
- package/examples/local-consent.ts +0 -30
- package/examples/read-debug.ts +0 -5
- package/src/__tests__/registry-and-context.test.ts +0 -234
- package/src/cli.ts +0 -47
- package/src/client/index.ts +0 -1
- package/src/client/mcpClient.ts +0 -193
- package/src/consent.ts +0 -46
- package/src/debug.ts +0 -32
- package/src/host.ts +0 -314
- package/src/index.ts +0 -30
- package/src/middleware/index.ts +0 -10
- package/src/middleware/mcpMiddleware.ts +0 -150
- package/src/registry/index.ts +0 -1
- package/src/registry/serverRegistry.ts +0 -165
- package/src/server/contextServer.ts +0 -211
- package/src/server/index.ts +0 -5
- package/src/server/internalServers.ts +0 -70
- package/src/types.ts +0 -135
- package/tsconfig.json +0 -9
- package/tsup.config.ts +0 -17
package/AGENTS.md
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — packages/mcp
|
|
2
|
-
|
|
3
|
-
Execution contract for Nebutra's Model Context Protocol package.
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
Applies to everything under `packages/ai/mcp/`.
|
|
8
|
-
|
|
9
|
-
This package owns checked-in MCP request and server types, the client runtime,
|
|
10
|
-
middleware composition, in-process server registry, and the internal-server
|
|
11
|
-
catalog exposed through MCP. It is an integration boundary and is still `wip`,
|
|
12
|
-
not a production-complete tool platform.
|
|
13
|
-
|
|
14
|
-
## Source Of Truth
|
|
15
|
-
|
|
16
|
-
- Public package surface and subpath exports: `package.json`, `src/index.ts`
|
|
17
|
-
- Canonical MCP types, tool definitions, request/response shapes, and server
|
|
18
|
-
config contract: `src/types.ts`
|
|
19
|
-
- Client runtime and shared singleton: `src/client/mcpClient.ts`,
|
|
20
|
-
`src/client/index.ts`
|
|
21
|
-
- Middleware composition, access control, audit, and rate-limit behavior:
|
|
22
|
-
`src/middleware/mcpMiddleware.ts`, `src/middleware/index.ts`
|
|
23
|
-
- Server registry and registration semantics:
|
|
24
|
-
`src/registry/serverRegistry.ts`, `src/registry/index.ts`
|
|
25
|
-
- Checked-in internal server catalog and registration entrypoint:
|
|
26
|
-
`src/server/internalServers.ts`, `src/server/index.ts`
|
|
27
|
-
- Context server runtime entrypoint: `src/server/contextServer.ts`
|
|
28
|
-
|
|
29
|
-
Treat `README.md` as descriptive only. If package behavior and docs disagree,
|
|
30
|
-
update the source files above instead of preserving stale examples.
|
|
31
|
-
|
|
32
|
-
## Contract Boundaries
|
|
33
|
-
|
|
34
|
-
- Keep `package.json` exports, `src/index.ts`, and the `bin` contract aligned.
|
|
35
|
-
Public changes here affect client, middleware, and server consumers at once.
|
|
36
|
-
- Treat `src/types.ts` as the canonical contract for tool definitions, request
|
|
37
|
-
envelopes, execution results, and server registration. Tightening those
|
|
38
|
-
shapes is a compatibility change.
|
|
39
|
-
- Preserve the split between outbound client runtime, middleware pipeline, and
|
|
40
|
-
registry/server concerns. Client transport logic belongs in `src/client/`,
|
|
41
|
-
middleware semantics in `src/middleware/`, and checked-in server catalogs in
|
|
42
|
-
`src/server/` plus `src/registry/`.
|
|
43
|
-
- Keep internal server declarations centralized in `src/server/internalServers.ts`.
|
|
44
|
-
Do not scatter internal MCP server metadata through apps or docs.
|
|
45
|
-
- Respect the package's current `wip` status. The context server binary and
|
|
46
|
-
plan-based access control are not complete; do not document or code against
|
|
47
|
-
stronger production guarantees than the package actually provides.
|
|
48
|
-
|
|
49
|
-
## Generated And Derived Files
|
|
50
|
-
|
|
51
|
-
- `dist/` is derived build output from `tsup`.
|
|
52
|
-
- The prepared `dist/server/contextServer.js` binary artifact is generated and
|
|
53
|
-
may be a placeholder stub before a full build.
|
|
54
|
-
- Do not hand-edit emitted build output or generated CLI artifacts. Update the
|
|
55
|
-
source files above and rebuild instead.
|
|
56
|
-
|
|
57
|
-
## Validation
|
|
58
|
-
|
|
59
|
-
- Type or middleware/registry/client contract changes:
|
|
60
|
-
`pnpm --filter @nebutra/mcp typecheck`
|
|
61
|
-
- Server entrypoint, subpath export, or bin changes:
|
|
62
|
-
`pnpm --filter @nebutra/mcp build`
|
package/bin/nebutra-mcp.js
DELETED
package/examples/doctor.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { McpHost } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const host = new McpHost();
|
|
4
|
-
|
|
5
|
-
host.connectLocal({
|
|
6
|
-
id: "read-only",
|
|
7
|
-
name: "Read Only",
|
|
8
|
-
description: "Read-only example tools",
|
|
9
|
-
manifest: { name: "read-only", version: "1.0.0", scopes: ["project:read"] },
|
|
10
|
-
tools: [{ name: "ping", description: "Return pong" }],
|
|
11
|
-
handlers: { ping: async () => "pong" },
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
process.stdout.write(`${JSON.stringify(await host.doctor(), null, 2)}\n`);
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { InMemoryToolConsentStore, McpHost } from "../src/index";
|
|
2
|
-
|
|
3
|
-
const consent = new InMemoryToolConsentStore();
|
|
4
|
-
const host = new McpHost({ consent });
|
|
5
|
-
|
|
6
|
-
host.connectLocal({
|
|
7
|
-
id: "notes",
|
|
8
|
-
name: "Notes",
|
|
9
|
-
description: "Tenant-scoped note tools",
|
|
10
|
-
manifest: { name: "notes", version: "1.0.0", scopes: ["notes:write"] },
|
|
11
|
-
tools: [{ name: "create_note", description: "Create a note" }],
|
|
12
|
-
handlers: {
|
|
13
|
-
create_note: async (args) => ({ id: "note_1", title: args.title }),
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
await consent.grant({
|
|
18
|
-
tenantId: "demo",
|
|
19
|
-
serverId: "notes",
|
|
20
|
-
toolName: "create_note",
|
|
21
|
-
scopes: ["notes:write"],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const result = await host.callTool(
|
|
25
|
-
"notes:create_note",
|
|
26
|
-
{ title: "Layer 1" },
|
|
27
|
-
{ requestId: "example_1", tenantId: "demo" },
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
package/examples/read-debug.ts
DELETED
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { MCPClient } from "../client/mcpClient";
|
|
6
|
-
import { InMemoryToolConsentStore } from "../consent";
|
|
7
|
-
import { readToolDebug } from "../debug";
|
|
8
|
-
import { McpHost } from "../host";
|
|
9
|
-
import { MCPServerRegistry } from "../registry/serverRegistry";
|
|
10
|
-
import { createContextServerHandlers } from "../server/contextServer";
|
|
11
|
-
|
|
12
|
-
describe("tool protocol host", () => {
|
|
13
|
-
it("requires manifest and per-tool consent before executing a connected server", async () => {
|
|
14
|
-
const consent = new InMemoryToolConsentStore();
|
|
15
|
-
const host = new McpHost({ consent });
|
|
16
|
-
const traces: string[] = [];
|
|
17
|
-
|
|
18
|
-
expect(() =>
|
|
19
|
-
host.connectLocal({
|
|
20
|
-
id: "unsafe",
|
|
21
|
-
name: "Unsafe",
|
|
22
|
-
description: "Missing manifest",
|
|
23
|
-
tools: [],
|
|
24
|
-
handlers: {},
|
|
25
|
-
}),
|
|
26
|
-
).toThrow(/manifest/i);
|
|
27
|
-
|
|
28
|
-
host.connectLocal({
|
|
29
|
-
id: "notes",
|
|
30
|
-
name: "Notes",
|
|
31
|
-
description: "Tenant-scoped notes tools",
|
|
32
|
-
manifest: {
|
|
33
|
-
name: "notes",
|
|
34
|
-
version: "1.0.0",
|
|
35
|
-
scopes: ["notes:write"],
|
|
36
|
-
},
|
|
37
|
-
tools: [
|
|
38
|
-
{
|
|
39
|
-
name: "create_note",
|
|
40
|
-
description: "Create a note",
|
|
41
|
-
parameters: {},
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
handlers: {
|
|
45
|
-
create_note: async (args, context) => {
|
|
46
|
-
traces.push(`${context.tenantId}:${args.title}`);
|
|
47
|
-
return { id: "note_1" };
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
await expect(
|
|
53
|
-
host.callTool("notes:create_note", { title: "Layer 1" }, { requestId: "r1" }),
|
|
54
|
-
).resolves.toMatchObject({
|
|
55
|
-
success: false,
|
|
56
|
-
error: expect.stringContaining("tenant"),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const context = { requestId: "r2", tenantId: "tenant_a", userId: "user_1" };
|
|
60
|
-
await expect(
|
|
61
|
-
host.callTool("notes:create_note", { title: "Layer 1" }, context),
|
|
62
|
-
).resolves.toMatchObject({
|
|
63
|
-
success: false,
|
|
64
|
-
error: expect.stringContaining("consent"),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
await consent.grant({
|
|
68
|
-
tenantId: "tenant_a",
|
|
69
|
-
userId: "user_1",
|
|
70
|
-
serverId: "notes",
|
|
71
|
-
toolName: "create_note",
|
|
72
|
-
scopes: ["notes:write"],
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await expect(
|
|
76
|
-
host.callTool("notes:create_note", { title: "Layer 1" }, context),
|
|
77
|
-
).resolves.toMatchObject({
|
|
78
|
-
success: true,
|
|
79
|
-
result: { id: "note_1" },
|
|
80
|
-
});
|
|
81
|
-
expect(traces).toEqual(["tenant_a:Layer 1"]);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("writes protocol debug entries for inspected calls", async () => {
|
|
85
|
-
const consent = new InMemoryToolConsentStore();
|
|
86
|
-
const host = new McpHost({ consent });
|
|
87
|
-
host.connectLocal({
|
|
88
|
-
id: "echo",
|
|
89
|
-
name: "Echo",
|
|
90
|
-
description: "Echo tools",
|
|
91
|
-
manifest: { name: "echo", version: "1.0.0", scopes: ["echo:run"] },
|
|
92
|
-
tools: [{ name: "say", description: "Say a value" }],
|
|
93
|
-
handlers: {
|
|
94
|
-
say: async (args) => args,
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
await consent.grant({
|
|
98
|
-
tenantId: "tenant_b",
|
|
99
|
-
serverId: "echo",
|
|
100
|
-
toolName: "say",
|
|
101
|
-
scopes: ["echo:run"],
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
await host.callTool("echo:say", { text: "ok" }, { requestId: "r3", tenantId: "tenant_b" });
|
|
105
|
-
|
|
106
|
-
await expect(readToolDebug(1)).resolves.toEqual([
|
|
107
|
-
expect.objectContaining({ type: "tool_call", serverId: "echo", toolName: "say", ok: true }),
|
|
108
|
-
]);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe("MCP tool registry access control", () => {
|
|
113
|
-
it("executes registered local tools only when tenant and plan policies pass", async () => {
|
|
114
|
-
const registry = new MCPServerRegistry();
|
|
115
|
-
registry.register({
|
|
116
|
-
id: "tenant-tools",
|
|
117
|
-
name: "Tenant tools",
|
|
118
|
-
description: "Tenant-scoped local tools",
|
|
119
|
-
endpoint: "local://tenant-tools",
|
|
120
|
-
transport: "local",
|
|
121
|
-
allowedTenants: ["tenant_allowed"],
|
|
122
|
-
tools: [
|
|
123
|
-
{
|
|
124
|
-
name: "summarize_context",
|
|
125
|
-
description: "Summarize tenant context",
|
|
126
|
-
parameters: {},
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "sync_plan_data",
|
|
130
|
-
description: "Sync plan-gated data",
|
|
131
|
-
parameters: {},
|
|
132
|
-
allowedPlans: ["PRO", "ENTERPRISE"],
|
|
133
|
-
},
|
|
134
|
-
],
|
|
135
|
-
handlers: {
|
|
136
|
-
summarize_context: async (args, context) => ({
|
|
137
|
-
args,
|
|
138
|
-
tenantId: context.tenantId,
|
|
139
|
-
plan: context.plan,
|
|
140
|
-
}),
|
|
141
|
-
sync_plan_data: async () => ({ synced: true }),
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const client = new MCPClient(registry);
|
|
146
|
-
const freeContext = {
|
|
147
|
-
requestId: "req_1",
|
|
148
|
-
tenantId: "tenant_allowed",
|
|
149
|
-
plan: "FREE",
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
await expect(
|
|
153
|
-
client.executeTool("summarize_context", { topic: "billing" }, freeContext),
|
|
154
|
-
).resolves.toMatchObject({
|
|
155
|
-
success: true,
|
|
156
|
-
result: { args: { topic: "billing" }, tenantId: "tenant_allowed", plan: "FREE" },
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
expect(client.listTools(freeContext).map((tool) => tool.name)).toEqual(["summarize_context"]);
|
|
160
|
-
|
|
161
|
-
await expect(client.executeTool("sync_plan_data", {}, freeContext)).resolves.toMatchObject({
|
|
162
|
-
success: false,
|
|
163
|
-
error: "Access denied to tool: sync_plan_data",
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
await expect(
|
|
167
|
-
client.executeTool(
|
|
168
|
-
"sync_plan_data",
|
|
169
|
-
{},
|
|
170
|
-
{ requestId: "req_2", tenantId: "tenant_allowed", plan: "PRO" },
|
|
171
|
-
),
|
|
172
|
-
).resolves.toMatchObject({
|
|
173
|
-
success: true,
|
|
174
|
-
result: { synced: true },
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
await expect(
|
|
178
|
-
client.executeTool(
|
|
179
|
-
"summarize_context",
|
|
180
|
-
{},
|
|
181
|
-
{ requestId: "req_3", tenantId: "tenant_blocked", plan: "ENTERPRISE" },
|
|
182
|
-
),
|
|
183
|
-
).resolves.toMatchObject({
|
|
184
|
-
success: false,
|
|
185
|
-
error: "Access denied to tool: summarize_context",
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe("context server handlers", () => {
|
|
191
|
-
const tempRoots: string[] = [];
|
|
192
|
-
|
|
193
|
-
afterEach(() => {
|
|
194
|
-
for (const root of tempRoots.splice(0)) {
|
|
195
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
196
|
-
}
|
|
197
|
-
fs.rmSync(path.join(process.cwd(), ".nebutra"), { recursive: true, force: true });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("lists project resources and returns non-placeholder project structure", async () => {
|
|
201
|
-
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "nebutra-mcp-"));
|
|
202
|
-
tempRoots.push(projectRoot);
|
|
203
|
-
fs.mkdirSync(path.join(projectRoot, "apps", "web", "src", "app", "dashboard"), {
|
|
204
|
-
recursive: true,
|
|
205
|
-
});
|
|
206
|
-
fs.mkdirSync(path.join(projectRoot, "packages", "platform", "db", "prisma"), {
|
|
207
|
-
recursive: true,
|
|
208
|
-
});
|
|
209
|
-
fs.writeFileSync(path.join(projectRoot, "nebutra.config.json"), '{"features":["mcp"]}');
|
|
210
|
-
fs.writeFileSync(
|
|
211
|
-
path.join(projectRoot, "packages", "platform", "db", "prisma", "schema.prisma"),
|
|
212
|
-
"model User { id String @id }",
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const handlers = createContextServerHandlers(projectRoot);
|
|
216
|
-
const resources = await handlers.listResources();
|
|
217
|
-
expect(resources.resources.map((resource) => resource.uri)).toContain(
|
|
218
|
-
"file:///nebutra.config.json",
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
const structure = await handlers.callTool({
|
|
222
|
-
name: "get_project_structure",
|
|
223
|
-
arguments: {},
|
|
224
|
-
});
|
|
225
|
-
expect(structure.content[0]?.text).toContain("apps/web/src/app/dashboard");
|
|
226
|
-
expect(structure.content[0]?.text).not.toContain("Apps dir not found");
|
|
227
|
-
|
|
228
|
-
await expect(
|
|
229
|
-
handlers.readResource({ uri: "file:///prisma/schema.prisma" }),
|
|
230
|
-
).resolves.toMatchObject({
|
|
231
|
-
contents: [{ text: "model User { id String @id }" }],
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
package/src/cli.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { McpHost, readToolDebug } from "./index";
|
|
2
|
-
import { createContextServerHandlers } from "./server/contextServer";
|
|
3
|
-
|
|
4
|
-
const command = process.argv[2] ?? "doctor";
|
|
5
|
-
const host = new McpHost();
|
|
6
|
-
const contextHandlers = createContextServerHandlers(process.cwd());
|
|
7
|
-
|
|
8
|
-
host.connectLocal({
|
|
9
|
-
id: "nebutra-context",
|
|
10
|
-
name: "Nebutra Context",
|
|
11
|
-
description: "Read-only project context tools",
|
|
12
|
-
manifest: {
|
|
13
|
-
name: "nebutra-context",
|
|
14
|
-
version: "1.0.0",
|
|
15
|
-
scopes: ["project:read"],
|
|
16
|
-
},
|
|
17
|
-
tools: [
|
|
18
|
-
{
|
|
19
|
-
name: "get_project_structure",
|
|
20
|
-
description: "Returns a bounded app/package routing tree for the current project.",
|
|
21
|
-
parameters: {},
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
handlers: {
|
|
25
|
-
get_project_structure: async () => contextHandlers.callTool({ name: "get_project_structure" }),
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (command === "doctor") {
|
|
30
|
-
process.stdout.write(
|
|
31
|
-
`${JSON.stringify({ capability: "tool-protocol", ...(await host.doctor()) }, null, 2)}\n`,
|
|
32
|
-
);
|
|
33
|
-
} else if (command === "debug") {
|
|
34
|
-
const id = process.argv[3];
|
|
35
|
-
const entries = await readToolDebug(id ? 50 : 10);
|
|
36
|
-
process.stdout.write(
|
|
37
|
-
`${JSON.stringify({ capability: "tool-protocol", id, entries }, null, 2)}\n`,
|
|
38
|
-
);
|
|
39
|
-
} else if (command === "inspect") {
|
|
40
|
-
const serverId = process.argv[3] ?? "nebutra-context";
|
|
41
|
-
process.stdout.write(
|
|
42
|
-
`${JSON.stringify({ capability: "tool-protocol", server: host.inspect(serverId) }, null, 2)}\n`,
|
|
43
|
-
);
|
|
44
|
-
} else {
|
|
45
|
-
process.stderr.write(`Unknown tool-protocol command: ${command}\n`);
|
|
46
|
-
process.exitCode = 1;
|
|
47
|
-
}
|
package/src/client/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { MCPClient, mcpClient } from "./mcpClient";
|
package/src/client/mcpClient.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { MCPServerRegistry } from "../registry/serverRegistry";
|
|
2
|
-
import { serverRegistry } from "../registry/serverRegistry";
|
|
3
|
-
import type { MCPContext, MCPServerConfig, ToolExecutionResult } from "../types";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* MCP Client for executing tools across registered servers
|
|
7
|
-
*/
|
|
8
|
-
export class MCPClient {
|
|
9
|
-
private requestCounter = 0;
|
|
10
|
-
|
|
11
|
-
constructor(private readonly registry: MCPServerRegistry = serverRegistry) {}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Execute a tool by name
|
|
15
|
-
*/
|
|
16
|
-
async executeTool(
|
|
17
|
-
toolName: string,
|
|
18
|
-
args: Record<string, unknown>,
|
|
19
|
-
context: MCPContext,
|
|
20
|
-
): Promise<ToolExecutionResult> {
|
|
21
|
-
const startTime = Date.now();
|
|
22
|
-
|
|
23
|
-
// Find server for this tool
|
|
24
|
-
const server = this.registry.findServerByTool(toolName);
|
|
25
|
-
if (!server) {
|
|
26
|
-
return {
|
|
27
|
-
success: false,
|
|
28
|
-
error: `Tool not found: ${toolName}`,
|
|
29
|
-
duration: Date.now() - startTime,
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Check access
|
|
34
|
-
if (!this.registry.canAccessTool(toolName, context)) {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
error: `Access denied to tool: ${toolName}`,
|
|
38
|
-
duration: Date.now() - startTime,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Execute based on transport
|
|
43
|
-
try {
|
|
44
|
-
const result = await this.executeOnServer(server, toolName, args, context);
|
|
45
|
-
return {
|
|
46
|
-
success: true,
|
|
47
|
-
result,
|
|
48
|
-
duration: Date.now() - startTime,
|
|
49
|
-
};
|
|
50
|
-
} catch (error) {
|
|
51
|
-
return {
|
|
52
|
-
success: false,
|
|
53
|
-
error: error instanceof Error ? error.message : String(error),
|
|
54
|
-
duration: Date.now() - startTime,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Execute tool on a specific server
|
|
61
|
-
*/
|
|
62
|
-
private async executeOnServer(
|
|
63
|
-
server: MCPServerConfig,
|
|
64
|
-
toolName: string,
|
|
65
|
-
args: Record<string, unknown>,
|
|
66
|
-
context: MCPContext,
|
|
67
|
-
): Promise<unknown> {
|
|
68
|
-
const requestId = `${context.requestId}-${++this.requestCounter}`;
|
|
69
|
-
|
|
70
|
-
switch (server.transport) {
|
|
71
|
-
case "http":
|
|
72
|
-
return this.executeHttp(server, toolName, args, requestId);
|
|
73
|
-
case "websocket":
|
|
74
|
-
return this.executeWebSocket(server, toolName, args, requestId);
|
|
75
|
-
case "local":
|
|
76
|
-
return this.executeLocal(server, toolName, args, context);
|
|
77
|
-
case "stdio":
|
|
78
|
-
throw new Error("stdio transport not supported in browser/serverless");
|
|
79
|
-
default:
|
|
80
|
-
throw new Error(`Unknown transport: ${server.transport}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Execute via HTTP transport
|
|
86
|
-
*/
|
|
87
|
-
private async executeHttp(
|
|
88
|
-
server: MCPServerConfig,
|
|
89
|
-
toolName: string,
|
|
90
|
-
args: Record<string, unknown>,
|
|
91
|
-
requestId: string,
|
|
92
|
-
): Promise<unknown> {
|
|
93
|
-
const headers: Record<string, string> = {
|
|
94
|
-
"Content-Type": "application/json",
|
|
95
|
-
"X-Request-ID": requestId,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
// Add authentication if configured
|
|
99
|
-
if (server.authentication?.type === "bearer") {
|
|
100
|
-
// In production, get token from secure storage
|
|
101
|
-
const token = process.env[`MCP_${server.id.toUpperCase()}_TOKEN`];
|
|
102
|
-
if (token) {
|
|
103
|
-
headers.Authorization = `Bearer ${token}`;
|
|
104
|
-
}
|
|
105
|
-
} else if (server.authentication?.type === "api-key") {
|
|
106
|
-
const apiKey = process.env[`MCP_${server.id.toUpperCase()}_KEY`];
|
|
107
|
-
const headerName = server.authentication.headerName || "X-API-Key";
|
|
108
|
-
if (apiKey) {
|
|
109
|
-
headers[headerName] = apiKey;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const response = await fetch(server.endpoint, {
|
|
114
|
-
method: "POST",
|
|
115
|
-
headers,
|
|
116
|
-
body: JSON.stringify({
|
|
117
|
-
jsonrpc: "2.0",
|
|
118
|
-
id: requestId,
|
|
119
|
-
method: "tools/call",
|
|
120
|
-
params: {
|
|
121
|
-
name: toolName,
|
|
122
|
-
arguments: args,
|
|
123
|
-
},
|
|
124
|
-
}),
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const data = await response.json();
|
|
132
|
-
|
|
133
|
-
if (data.error) {
|
|
134
|
-
throw new Error(data.error.message || "Unknown MCP error");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return data.result;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Execute via WebSocket transport
|
|
142
|
-
*/
|
|
143
|
-
private async executeWebSocket(
|
|
144
|
-
_server: MCPServerConfig,
|
|
145
|
-
_toolName: string,
|
|
146
|
-
_args: Record<string, unknown>,
|
|
147
|
-
_requestId: string,
|
|
148
|
-
): Promise<unknown> {
|
|
149
|
-
// WebSocket implementation would maintain persistent connections
|
|
150
|
-
// For now, throw not implemented
|
|
151
|
-
throw new Error("WebSocket transport not yet implemented");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
private async executeLocal(
|
|
155
|
-
server: MCPServerConfig,
|
|
156
|
-
toolName: string,
|
|
157
|
-
args: Record<string, unknown>,
|
|
158
|
-
context: MCPContext,
|
|
159
|
-
): Promise<unknown> {
|
|
160
|
-
const shortName = toolName.startsWith(`${server.id}:`)
|
|
161
|
-
? toolName.slice(server.id.length + 1)
|
|
162
|
-
: toolName;
|
|
163
|
-
const handler = server.handlers?.[shortName] ?? server.handlers?.[toolName];
|
|
164
|
-
if (!handler) {
|
|
165
|
-
throw new Error(`No local handler registered for tool: ${toolName}`);
|
|
166
|
-
}
|
|
167
|
-
return handler(args, context);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* List available tools for a context
|
|
172
|
-
*/
|
|
173
|
-
listTools(context: MCPContext) {
|
|
174
|
-
return this.registry.getAccessibleTools(context);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Get tool definition
|
|
179
|
-
*/
|
|
180
|
-
getTool(toolName: string, context: MCPContext) {
|
|
181
|
-
const server = this.registry.findServerByTool(toolName);
|
|
182
|
-
if (!server) return undefined;
|
|
183
|
-
|
|
184
|
-
if (!this.registry.canAccessTool(toolName, context)) {
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return server.tools.find((t) => t.name === toolName || `${server.id}:${t.name}` === toolName);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Global client instance
|
|
193
|
-
export const mcpClient = new MCPClient();
|
package/src/consent.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export interface ToolConsentGrant {
|
|
2
|
-
readonly tenantId: string;
|
|
3
|
-
readonly userId?: string;
|
|
4
|
-
readonly serverId: string;
|
|
5
|
-
readonly toolName: string;
|
|
6
|
-
readonly scopes: readonly string[];
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ToolConsentStore {
|
|
10
|
-
grant(grant: ToolConsentGrant): Promise<void>;
|
|
11
|
-
hasConsent(grant: ToolConsentGrant): Promise<boolean>;
|
|
12
|
-
list(tenantId?: string): Promise<ToolConsentGrant[]>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function consentKey(grant: Omit<ToolConsentGrant, "scopes">): string {
|
|
16
|
-
return [grant.tenantId, grant.userId ?? "*", grant.serverId, grant.toolName].join(":");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class InMemoryToolConsentStore implements ToolConsentStore {
|
|
20
|
-
readonly #grants = new Map<string, ToolConsentGrant>();
|
|
21
|
-
|
|
22
|
-
async grant(grant: ToolConsentGrant): Promise<void> {
|
|
23
|
-
this.#grants.set(consentKey(grant), { ...grant, scopes: [...grant.scopes] });
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async hasConsent(grant: ToolConsentGrant): Promise<boolean> {
|
|
27
|
-
const exact = this.#grants.get(consentKey(grant));
|
|
28
|
-
const wildcard = this.#grants.get(
|
|
29
|
-
consentKey({
|
|
30
|
-
tenantId: grant.tenantId,
|
|
31
|
-
serverId: grant.serverId,
|
|
32
|
-
toolName: grant.toolName,
|
|
33
|
-
}),
|
|
34
|
-
);
|
|
35
|
-
const existing = exact ?? wildcard;
|
|
36
|
-
if (!existing) return false;
|
|
37
|
-
const scopes = new Set(existing.scopes);
|
|
38
|
-
return grant.scopes.every((scope) => scopes.has(scope));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async list(tenantId?: string): Promise<ToolConsentGrant[]> {
|
|
42
|
-
return Array.from(this.#grants.values()).filter(
|
|
43
|
-
(grant) => tenantId === undefined || grant.tenantId === tenantId,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
package/src/debug.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendCapabilityDebug,
|
|
3
|
-
capabilityDebugPath,
|
|
4
|
-
readCapabilityDebug,
|
|
5
|
-
} from "@nebutra/capability-kit/debug";
|
|
6
|
-
|
|
7
|
-
export interface ToolDebugEntry {
|
|
8
|
-
readonly at: string;
|
|
9
|
-
readonly type: "tool_call" | "connect" | "inspect";
|
|
10
|
-
readonly requestId?: string;
|
|
11
|
-
readonly tenantId?: string;
|
|
12
|
-
readonly serverId?: string;
|
|
13
|
-
readonly toolName?: string;
|
|
14
|
-
readonly ok: boolean;
|
|
15
|
-
readonly durationMs?: number;
|
|
16
|
-
readonly error?: string;
|
|
17
|
-
readonly suggestion?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function toolDebugPath(): string {
|
|
21
|
-
return capabilityDebugPath("tool-protocol");
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function appendToolDebug(
|
|
25
|
-
entry: Omit<ToolDebugEntry, "at"> & { at?: string },
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
await appendCapabilityDebug("tool-protocol", entry);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export async function readToolDebug(limit = 10): Promise<ToolDebugEntry[]> {
|
|
31
|
-
return readCapabilityDebug("tool-protocol", { limit }) as Promise<ToolDebugEntry[]>;
|
|
32
|
-
}
|