@pikku/inspector 0.12.83 → 0.12.85
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 +37 -0
- package/dist/add/add-channel.js +7 -1
- package/dist/add/add-wire-addon.js +11 -0
- package/dist/inspector.js +1 -0
- package/dist/types.d.ts +13 -0
- package/dist/utils/load-addon-functions-meta.js +12 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/serialize-mcp-json.d.ts +9 -1
- package/dist/utils/serialize-mcp-json.js +21 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
## 0.12.85
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- b312867: A project can now serve several MCP endpoints, one per connector.
|
|
6
|
+
|
|
7
|
+
Until now every MCP tool in a project was pooled onto a single `/mcp`, so a hub offering three connectors offered one endpoint listing all three connectors' tools at once. A client pointed at it saw tools it had no business calling, and the only way to give a connector an endpoint of its own was to give it a deployment of its own — three deploys, three bills, three service graphs.
|
|
8
|
+
|
|
9
|
+
`wireAddon` gains `mcpEndpoint`. `true` serves that instance's tools at `/mcp/<name>`; a string is the path, used as given. Leaving it unset keeps the tools on the shared endpoint, which is where they have always been, so nothing existing moves.
|
|
10
|
+
|
|
11
|
+
A surfaced instance now gets its own manifest (`.pikku/mcp/mcp.<name>.gen.json`, carrying the path it answers on), its own deploy unit (`mcp-<name>`, routed on that path), and its own MCP server — with its own tool list, so a client pointed at one endpoint never sees another's tools. The plumbing for the per-surface manifest and unit already existed in `deploy apply`; nothing had ever produced one.
|
|
12
|
+
|
|
13
|
+
`pikku dev` mounts every endpoint the generated tree describes, not just the default one. Without that a project that moved its tools onto their own endpoints would have served nothing locally at all — the default manifest it reads is empty precisely because they moved — and the only way to try a connector would have been to deploy it.
|
|
14
|
+
|
|
15
|
+
The node and bun transports take `mcpSurfaces` alongside `mcpJson` and mount each at its own path, longest path first. `/mcp` claims everything beneath `/mcp/`, so without that ordering the default endpoint answers `/mcp/weather` and the surface's tools are unreachable.
|
|
16
|
+
|
|
17
|
+
OAuth discovery is split between the endpoints rather than duplicated across them. RFC 9728 folds a resource's path into its well-known route, so each endpoint's own document is already distinct, but the path-less `/.well-known/oauth-protected-resource` predates that and describes whichever resource answers it. Only the default endpoint claims it — otherwise every unit registers the same route and the provider's router decides which resource a client is told about, and in dev a client probing it is described whichever surface sorted first.
|
|
18
|
+
|
|
19
|
+
- Updated dependencies [87971bd]
|
|
20
|
+
- Updated dependencies [b312867]
|
|
21
|
+
- Updated dependencies [51bd35a]
|
|
22
|
+
- @pikku/core@0.12.116
|
|
23
|
+
|
|
24
|
+
## 0.12.84
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 238902c: A channel's `auth` flag now reaches its meta. The inspector already read it to mark the connect and disconnect functions sessionless, then dropped it, so nothing downstream could tell a public channel from a private one without reading the generated source.
|
|
29
|
+
|
|
30
|
+
That matters in front of the channel rather than inside it. A `wireCLI({ auth: false })` program is reachable by anyone holding its address; a router or deploy that cannot see the flag either guesses or demands a token in front of a surface it was never protecting, locking out the clients the program was opened for.
|
|
31
|
+
|
|
32
|
+
The flag is only recorded when the channel actually said. A channel that never mentioned `auth` leaves it absent rather than claiming a default it did not declare, which the runtime continues to read as requiring a session.
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [238902c]
|
|
35
|
+
- Updated dependencies [2fd2b22]
|
|
36
|
+
- @pikku/core@0.12.115
|
|
37
|
+
|
|
1
38
|
## 0.12.83
|
|
2
39
|
|
|
3
40
|
### Patch Changes
|
package/dist/add/add-channel.js
CHANGED
|
@@ -482,8 +482,9 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
482
482
|
// with pikkuChannelConnectionFunc/pikkuChannelDisconnectionFunc (not pikkuFunc/
|
|
483
483
|
// pikkuSessionlessFunc), so the runtime has correct sessionless info without
|
|
484
484
|
// needing to inject it at runtime.
|
|
485
|
+
const channelAuth = getPropertyValue(obj, 'auth');
|
|
485
486
|
{
|
|
486
|
-
const routeAuth =
|
|
487
|
+
const routeAuth = channelAuth;
|
|
487
488
|
const sessionless = routeAuth === false ? true : undefined;
|
|
488
489
|
for (const funcId of [connectFuncId, disconnectFuncId]) {
|
|
489
490
|
if (funcId && !state.functions.meta[funcId]) {
|
|
@@ -538,6 +539,11 @@ export const addChannel = (logger, node, checker, state, options) => {
|
|
|
538
539
|
message,
|
|
539
540
|
messageWirings,
|
|
540
541
|
binary: binary === undefined ? undefined : binary,
|
|
542
|
+
// Recorded so a deploy can tell a public surface from a private one without
|
|
543
|
+
// reading the generated source. Only stored when the channel actually said,
|
|
544
|
+
// so a channel that never mentioned auth stays absent rather than claiming
|
|
545
|
+
// a default it did not declare.
|
|
546
|
+
auth: typeof channelAuth === 'boolean' ? channelAuth : undefined,
|
|
541
547
|
summary,
|
|
542
548
|
description,
|
|
543
549
|
errors,
|
|
@@ -95,6 +95,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
95
95
|
let pkg;
|
|
96
96
|
let rpcEndpoint;
|
|
97
97
|
let mcp;
|
|
98
|
+
let mcpEndpoint;
|
|
98
99
|
let auth;
|
|
99
100
|
let tags;
|
|
100
101
|
let scopes;
|
|
@@ -125,6 +126,15 @@ export function addWireAddon(node, state, logger) {
|
|
|
125
126
|
? prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
126
127
|
: parseStringArray(prop.initializer);
|
|
127
128
|
}
|
|
129
|
+
else if (key === 'mcpEndpoint') {
|
|
130
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
131
|
+
mcpEndpoint = prop.initializer.text;
|
|
132
|
+
}
|
|
133
|
+
else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
134
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
135
|
+
mcpEndpoint = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
128
138
|
else if (key === 'auth' &&
|
|
129
139
|
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
130
140
|
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
@@ -175,6 +185,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
175
185
|
file: node.getSourceFile().fileName,
|
|
176
186
|
rpcEndpoint,
|
|
177
187
|
mcp,
|
|
188
|
+
mcpEndpoint,
|
|
178
189
|
auth,
|
|
179
190
|
tags,
|
|
180
191
|
scopes,
|
package/dist/inspector.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -589,6 +589,13 @@ export interface InspectorState {
|
|
|
589
589
|
* when the declared value was not a statically-knowable literal.
|
|
590
590
|
*/
|
|
591
591
|
mcp?: boolean | string[];
|
|
592
|
+
/**
|
|
593
|
+
* Serves this instance's MCP tools on an endpoint of their own rather
|
|
594
|
+
* than the project's default one. `true` means `/mcp/<name>`; a string
|
|
595
|
+
* is the path as given. Absent leaves the tools on the default
|
|
596
|
+
* endpoint, which is where they have always been.
|
|
597
|
+
*/
|
|
598
|
+
mcpEndpoint?: boolean | string;
|
|
592
599
|
/**
|
|
593
600
|
* The addon's own gates. `runPikkuFunc` applies these to every function
|
|
594
601
|
* in the package, on every wiring path — not just `namespace:function`.
|
|
@@ -631,6 +638,12 @@ export interface InspectorState {
|
|
|
631
638
|
toolsMeta: MCPToolMeta;
|
|
632
639
|
promptsMeta: MCPPromptMeta;
|
|
633
640
|
files: Set<string>;
|
|
641
|
+
/**
|
|
642
|
+
* Every MCP endpoint the project serves beyond the default one, as
|
|
643
|
+
* surface name -> the path it is served on. A meta entry carrying a
|
|
644
|
+
* `surface` is served by the endpoint named here and by no other.
|
|
645
|
+
*/
|
|
646
|
+
surfaces: Record<string, string>;
|
|
634
647
|
};
|
|
635
648
|
agents: {
|
|
636
649
|
agentsMeta: AgentsMeta;
|
|
@@ -153,6 +153,17 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
153
153
|
// decides which of the addon's functions a model gets to see.
|
|
154
154
|
if (decl.mcp) {
|
|
155
155
|
const selected = Array.isArray(decl.mcp) ? new Set(decl.mcp) : null;
|
|
156
|
+
// `mcpEndpoint` gives this instance an endpoint to itself, which is
|
|
157
|
+
// what lets one project expose several connectors: a client pointed at
|
|
158
|
+
// one surface lists that surface's tools and no others. Unset keeps
|
|
159
|
+
// the tools on the project's single endpoint.
|
|
160
|
+
const surface = decl.mcpEndpoint ? namespace : undefined;
|
|
161
|
+
if (surface) {
|
|
162
|
+
state.mcpEndpoints.surfaces[surface] =
|
|
163
|
+
typeof decl.mcpEndpoint === 'string'
|
|
164
|
+
? decl.mcpEndpoint
|
|
165
|
+
: `/mcp/${namespace}`;
|
|
166
|
+
}
|
|
156
167
|
for (const [funcName, funcMeta] of Object.entries(meta)) {
|
|
157
168
|
if (selected ? !selected.has(funcName) : !funcMeta.mcp) {
|
|
158
169
|
continue;
|
|
@@ -165,6 +176,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
165
176
|
inputSchema: funcMeta.inputSchemaName ?? null,
|
|
166
177
|
outputSchema: funcMeta.outputSchemaName ?? null,
|
|
167
178
|
tags: funcMeta.tags,
|
|
179
|
+
...(surface ? { surface } : {}),
|
|
168
180
|
};
|
|
169
181
|
}
|
|
170
182
|
// A name the addon does not publish is a tool the app believes it
|
|
@@ -212,6 +212,7 @@ export interface SerializableInspectorState {
|
|
|
212
212
|
toolsMeta: InspectorState['mcpEndpoints']['toolsMeta'];
|
|
213
213
|
promptsMeta: InspectorState['mcpEndpoints']['promptsMeta'];
|
|
214
214
|
files: string[];
|
|
215
|
+
surfaces?: InspectorState['mcpEndpoints']['surfaces'];
|
|
215
216
|
};
|
|
216
217
|
agents: {
|
|
217
218
|
invokedAgentsByFile?: Array<[string, string[]]>;
|
|
@@ -92,6 +92,7 @@ export function serializeInspectorState(state) {
|
|
|
92
92
|
toolsMeta: state.mcpEndpoints.toolsMeta,
|
|
93
93
|
promptsMeta: state.mcpEndpoints.promptsMeta,
|
|
94
94
|
files: Array.from(state.mcpEndpoints.files),
|
|
95
|
+
surfaces: state.mcpEndpoints.surfaces,
|
|
95
96
|
},
|
|
96
97
|
agents: {
|
|
97
98
|
agentsMeta: state.agents?.agentsMeta ?? {},
|
|
@@ -278,6 +279,7 @@ export function deserializeInspectorState(data) {
|
|
|
278
279
|
toolsMeta: data.mcpEndpoints.toolsMeta,
|
|
279
280
|
promptsMeta: data.mcpEndpoints.promptsMeta,
|
|
280
281
|
files: new Set(data.mcpEndpoints.files),
|
|
282
|
+
surfaces: data.mcpEndpoints.surfaces ?? {},
|
|
281
283
|
},
|
|
282
284
|
agents: {
|
|
283
285
|
agentsMeta: data.agents?.agentsMeta || {},
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Serializes one MCP endpoint's manifest.
|
|
4
|
+
*
|
|
5
|
+
* A project can serve several endpoints, and a wiring belongs to exactly one of
|
|
6
|
+
* them: the one named by its `surface`. Passing no surface serializes the
|
|
7
|
+
* default endpoint, which is every wiring that named none — so a project with
|
|
8
|
+
* no surfaces at all gets the single manifest it always got.
|
|
9
|
+
*/
|
|
10
|
+
export declare const serializeMCPJson: (logger: InspectorLogger, state: InspectorState, surface?: string) => string;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { resolveFunctionMeta } from './resolve-function-meta.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Serializes one MCP endpoint's manifest.
|
|
4
|
+
*
|
|
5
|
+
* A project can serve several endpoints, and a wiring belongs to exactly one of
|
|
6
|
+
* them: the one named by its `surface`. Passing no surface serializes the
|
|
7
|
+
* default endpoint, which is every wiring that named none — so a project with
|
|
8
|
+
* no surfaces at all gets the single manifest it always got.
|
|
9
|
+
*/
|
|
10
|
+
export const serializeMCPJson = (logger, state, surface) => {
|
|
3
11
|
const { mcpEndpoints, functions, schemas } = state;
|
|
4
12
|
const { typesMap } = functions;
|
|
5
13
|
const { resourcesMeta, toolsMeta, promptsMeta } = mcpEndpoints;
|
|
14
|
+
const onThisSurface = (meta) => (meta.surface ?? undefined) === surface;
|
|
6
15
|
const tools = [];
|
|
7
16
|
const resources = [];
|
|
8
17
|
const prompts = [];
|
|
@@ -37,6 +46,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
37
46
|
return schema;
|
|
38
47
|
};
|
|
39
48
|
for (const [name, endpointMeta] of Object.entries(resourcesMeta)) {
|
|
49
|
+
if (!onThisSurface(endpointMeta))
|
|
50
|
+
continue;
|
|
40
51
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
41
52
|
if (!functionMeta) {
|
|
42
53
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping resource ${name}.`);
|
|
@@ -56,6 +67,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
56
67
|
});
|
|
57
68
|
}
|
|
58
69
|
for (const [name, endpointMeta] of Object.entries(toolsMeta)) {
|
|
70
|
+
if (!onThisSurface(endpointMeta))
|
|
71
|
+
continue;
|
|
59
72
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
60
73
|
if (!functionMeta) {
|
|
61
74
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping tool ${name}.`);
|
|
@@ -74,6 +87,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
74
87
|
});
|
|
75
88
|
}
|
|
76
89
|
for (const [name, endpointMeta] of Object.entries(promptsMeta)) {
|
|
90
|
+
if (!onThisSurface(endpointMeta))
|
|
91
|
+
continue;
|
|
77
92
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
78
93
|
if (!functionMeta) {
|
|
79
94
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping prompt ${name}.`);
|
|
@@ -101,5 +116,9 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
101
116
|
arguments: argumentsArray,
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
|
-
|
|
119
|
+
// The path only travels in a surface's own manifest: the default endpoint
|
|
120
|
+
// stays at whatever the runtime mounts, which is what every existing project
|
|
121
|
+
// already relies on.
|
|
122
|
+
const mcpPath = surface ? mcpEndpoints.surfaces?.[surface] : undefined;
|
|
123
|
+
return JSON.stringify({ ...(mcpPath ? { mcpPath } : {}), tools, resources, prompts }, null, 2);
|
|
105
124
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.85",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"zod-to-ts": "^2.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
58
|
+
"@pikku/core": "^0.12.116"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
61
|
+
"@pikku/core": "^0.12.116",
|
|
62
62
|
"@types/node": "^24.13.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|