@bevel-software/platform-mcp-core 0.8.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/LICENSE +202 -0
- package/THIRD-PARTY-NOTICES.md +3065 -0
- package/dist/code-mode-names.d.ts +40 -0
- package/dist/code-mode-names.d.ts.map +1 -0
- package/dist/code-mode-names.js +93 -0
- package/dist/code-mode-names.js.map +1 -0
- package/dist/dispatch.d.ts +44 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +72 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/meta-tools.d.ts +27 -0
- package/dist/meta-tools.d.ts.map +1 -0
- package/dist/meta-tools.js +161 -0
- package/dist/meta-tools.js.map +1 -0
- package/dist/proxied-tool.d.ts +49 -0
- package/dist/proxied-tool.d.ts.map +1 -0
- package/dist/proxied-tool.js +183 -0
- package/dist/proxied-tool.js.map +1 -0
- package/dist/results.d.ts +19 -0
- package/dist/results.d.ts.map +1 -0
- package/dist/results.js +130 -0
- package/dist/results.js.map +1 -0
- package/dist/skills.d.ts +22 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +20 -0
- package/dist/skills.js.map +1 -0
- package/dist/utcp-namespace.d.ts +30 -0
- package/dist/utcp-namespace.d.ts.map +1 -0
- package/dist/utcp-namespace.js +66 -0
- package/dist/utcp-namespace.js.map +1 -0
- package/package.json +50 -0
- package/src/code-mode-names.ts +107 -0
- package/src/dispatch.ts +88 -0
- package/src/index.ts +71 -0
- package/src/meta-tools.ts +186 -0
- package/src/proxied-tool.ts +200 -0
- package/src/results.ts +131 -0
- package/src/skills.ts +34 -0
- package/src/utcp-namespace.ts +70 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool-name grammar (also the Anthropic API's), and a length bound. A
|
|
3
|
+
* remote MCP server can expose a tool whose flattened name breaks this — too
|
|
4
|
+
* long, or an illegal char the `<manual>_<name>` flattening didn't remove — and
|
|
5
|
+
* an MCP client (or the model API behind it) rejects the ENTIRE `tools/list`
|
|
6
|
+
* response when a single entry is non-conforming. That makes EVERY tool vanish
|
|
7
|
+
* the moment one bad tool from a newly-added server enters the catalog, with no
|
|
8
|
+
* server-side error (the rejection is the client's). `toListedTool` isolates it
|
|
9
|
+
* per tool: drop the offender (logged), normalize an odd schema, keep the rest.
|
|
10
|
+
*/
|
|
11
|
+
const MCP_TOOL_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
12
|
+
// The Anthropic API caps a tool name at 128 chars — but the MCP CLIENT (Claude
|
|
13
|
+
// Code, claude.ai) prepends `mcp__<server>__` (≈20+ chars) before sending it,
|
|
14
|
+
// and that FULL name is what the 128 applies to. So budget for the prefix here,
|
|
15
|
+
// or a long `googlecalendar_…` name we pass gets the whole request 400'd. This
|
|
16
|
+
// is deliberately conservative; a dropped tool is logged so it's diagnosable.
|
|
17
|
+
const MCP_TOOL_NAME_MAX = 100;
|
|
18
|
+
/** A discovered tool as an MCP listing entry, or null if its name can't be listed. */
|
|
19
|
+
export function toListedTool(tool) {
|
|
20
|
+
if (!MCP_TOOL_NAME_RE.test(tool.mcpName) || tool.mcpName.length > MCP_TOOL_NAME_MAX) {
|
|
21
|
+
console.warn(`[mcp] dropping tool "${tool.mcpName}" from the listing — not a valid MCP tool name ` +
|
|
22
|
+
`(must match ${MCP_TOOL_NAME_RE} and be ≤${MCP_TOOL_NAME_MAX} chars). ` +
|
|
23
|
+
'One non-conforming tool would otherwise make the whole toolset disappear on the client.');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// MCP requires an object inputSchema. A remote server's schema that isn't a
|
|
27
|
+
// plain object (or omits `type: 'object'`) can invalidate the whole response,
|
|
28
|
+
// so normalize it — keeping any declared properties — rather than pass it
|
|
29
|
+
// through verbatim.
|
|
30
|
+
const raw = tool.inputSchema;
|
|
31
|
+
let inputSchema = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
32
|
+
? { type: 'object', ...raw }
|
|
33
|
+
: { type: 'object', properties: {} };
|
|
34
|
+
// Sanitize the schema into what the Anthropic tool validator accepts. A
|
|
35
|
+
// remote server that emits a construct the validator rejects — Google's
|
|
36
|
+
// gmail/calendar use `$ref`/`$defs` AND OpenAPI `format` values like
|
|
37
|
+
// `int32`/`byte` — makes the CLIENT reject the ENTIRE tools/list response,
|
|
38
|
+
// so all tools vanish and nothing registers. Sanitizing per-tool means one
|
|
39
|
+
// odd server can't blank the whole toolset.
|
|
40
|
+
inputSchema = sanitizeInputSchema(inputSchema);
|
|
41
|
+
// The MCP/Anthropic validator requires the top-level `type` to be exactly
|
|
42
|
+
// "object" and (for the Anthropic API) `properties` to be present. Force both
|
|
43
|
+
// so a remote schema that declared something else — or a union like
|
|
44
|
+
// `["object","null"]` — can't reject the whole tools/list.
|
|
45
|
+
inputSchema.type = 'object';
|
|
46
|
+
// An array passes `typeof === 'object'` but is not a property map, so it
|
|
47
|
+
// must coerce like any other non-object or it poisons the whole listing.
|
|
48
|
+
if (typeof inputSchema.properties !== 'object' ||
|
|
49
|
+
inputSchema.properties === null ||
|
|
50
|
+
Array.isArray(inputSchema.properties)) {
|
|
51
|
+
inputSchema.properties = {};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name: tool.mcpName,
|
|
55
|
+
description: tool.description,
|
|
56
|
+
inputSchema: inputSchema,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** JSON-Schema string `format` values the Anthropic tool validator accepts. */
|
|
60
|
+
const SUPPORTED_SCHEMA_FORMATS = new Set([
|
|
61
|
+
'date-time',
|
|
62
|
+
'time',
|
|
63
|
+
'date',
|
|
64
|
+
'duration',
|
|
65
|
+
'email',
|
|
66
|
+
'hostname',
|
|
67
|
+
'uri',
|
|
68
|
+
'ipv4',
|
|
69
|
+
'ipv6',
|
|
70
|
+
'uuid',
|
|
71
|
+
]);
|
|
72
|
+
/**
|
|
73
|
+
* Make a remote server's JSON Schema safe for the Anthropic tool validator:
|
|
74
|
+
* - inline local `$ref` pointers (`#/$defs/...`, `#/definitions/...`) and drop
|
|
75
|
+
* the now-unreferenced `$defs`/`definitions` blocks (the API restricts `$ref`
|
|
76
|
+
* and MCP clients converting our schemas reject it outright);
|
|
77
|
+
* - drop non-standard `format` values (OpenAPI's `int32`/`byte`/… — only the
|
|
78
|
+
* JSON-Schema-standard formats above are accepted; `format` is advisory, so
|
|
79
|
+
* dropping it doesn't change tool behavior).
|
|
80
|
+
* Depth-bounded so a recursive schema degrades to a permissive `{}` node instead
|
|
81
|
+
* of hanging or emitting the unsupported recursion; non-local/external refs
|
|
82
|
+
* degrade the same way. Exported for direct testing.
|
|
83
|
+
*/
|
|
84
|
+
export function sanitizeInputSchema(schema) {
|
|
85
|
+
const root = schema;
|
|
86
|
+
const resolvePointer = (pointer) => {
|
|
87
|
+
if (!pointer.startsWith('#/'))
|
|
88
|
+
return undefined;
|
|
89
|
+
let node = root;
|
|
90
|
+
for (const partRaw of pointer.slice(2).split('/')) {
|
|
91
|
+
const part = partRaw.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
92
|
+
if (!node || typeof node !== 'object')
|
|
93
|
+
return undefined;
|
|
94
|
+
node = node[part];
|
|
95
|
+
}
|
|
96
|
+
return node;
|
|
97
|
+
};
|
|
98
|
+
// `isPropertyMap` marks the value of `properties`/`patternProperties`: its
|
|
99
|
+
// keys are the tool's OWN field names, not schema keywords, so a field
|
|
100
|
+
// literally named `format`, `$ref` or `definitions` must survive untouched
|
|
101
|
+
// (its VALUE is still a schema and is walked as one).
|
|
102
|
+
const walk = (node, depth, isPropertyMap = false) => {
|
|
103
|
+
if (depth > 20)
|
|
104
|
+
return {}; // recursion/cycle guard — permissive fallback
|
|
105
|
+
if (Array.isArray(node))
|
|
106
|
+
return node.map((item) => walk(item, depth + 1));
|
|
107
|
+
if (!node || typeof node !== 'object')
|
|
108
|
+
return node;
|
|
109
|
+
const obj = node;
|
|
110
|
+
if (!isPropertyMap && typeof obj.$ref === 'string') {
|
|
111
|
+
const target = resolvePointer(obj.$ref);
|
|
112
|
+
// JSON Schema allows siblings next to $ref; keep them, target wins ties.
|
|
113
|
+
const siblings = { ...obj };
|
|
114
|
+
delete siblings.$ref;
|
|
115
|
+
const resolved = walk(target ?? {}, depth + 1);
|
|
116
|
+
return resolved && typeof resolved === 'object' && !Array.isArray(resolved)
|
|
117
|
+
? { ...siblings, ...resolved }
|
|
118
|
+
: Object.keys(siblings).length
|
|
119
|
+
? siblings
|
|
120
|
+
: resolved ?? {};
|
|
121
|
+
}
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
124
|
+
if (isPropertyMap) {
|
|
125
|
+
out[key] = walk(value, depth + 1);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (key === '$defs' || key === 'definitions')
|
|
129
|
+
continue; // inlined above
|
|
130
|
+
// Drop a non-standard `format` (OpenAPI `int32`/`byte`/…) — the validator
|
|
131
|
+
// only allows the JSON-Schema-standard set; the annotation is non-load-bearing.
|
|
132
|
+
if (key === 'format' && (typeof value !== 'string' || !SUPPORTED_SCHEMA_FORMATS.has(value))) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
out[key] = walk(value, depth + 1, key === 'properties' || key === 'patternProperties');
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
};
|
|
139
|
+
return walk(schema, 0);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Flatten a discovered UTCP tool (`<manual>.<tool>`) into the advertised shape
|
|
143
|
+
* when MULTIPLE manuals are registered. Tools from `kbManualName` keep their
|
|
144
|
+
* bare name (so existing agents still call `read_file`); every other manual's
|
|
145
|
+
* tool is namespaced as `<manual>_<tool>` to guarantee a unique, dot-free MCP
|
|
146
|
+
* name.
|
|
147
|
+
*
|
|
148
|
+
* `kbManualName` is a parameter rather than a constant because the two surfaces
|
|
149
|
+
* that call this reach the KB through different manuals: the hosted proxy
|
|
150
|
+
* registers it over loopback, the local server registers the deployment's MCP
|
|
151
|
+
* endpoint. Whichever manual carries the core toolset is the one whose names
|
|
152
|
+
* must stay bare.
|
|
153
|
+
*/
|
|
154
|
+
export function flattenManualTool(tool, kbManualName) {
|
|
155
|
+
const dot = tool.name.indexOf('.');
|
|
156
|
+
const manual = dot >= 0 ? tool.name.slice(0, dot) : '';
|
|
157
|
+
const bare = dot >= 0 ? tool.name.slice(dot + 1) : tool.name;
|
|
158
|
+
const mcpName = manual === kbManualName ? bare : tool.name.replace(/\./g, '_');
|
|
159
|
+
return {
|
|
160
|
+
utcpName: tool.name,
|
|
161
|
+
mcpName,
|
|
162
|
+
description: tool.description,
|
|
163
|
+
inputSchema: tool.inputs,
|
|
164
|
+
manualName: manual,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Flatten one discovered UTCP tool into the advertised shape: strip the
|
|
169
|
+
* `<manual>.` namespace prefix for the MCP name and keep the UTCP input schema
|
|
170
|
+
* verbatim (Bevel-hosted HTTP tools show their `{body}` envelope, exactly as in
|
|
171
|
+
* `call_tool_chain`).
|
|
172
|
+
*/
|
|
173
|
+
export function flattenDiscoveredTool(prefix, tool) {
|
|
174
|
+
return {
|
|
175
|
+
utcpName: tool.name,
|
|
176
|
+
mcpName: tool.name.startsWith(prefix) ? tool.name.slice(prefix.length) : tool.name,
|
|
177
|
+
description: tool.description,
|
|
178
|
+
inputSchema: tool.inputs,
|
|
179
|
+
// `prefix` is `<manual>.`; the manual is that without the trailing dot.
|
|
180
|
+
manualName: prefix.endsWith('.') ? prefix.slice(0, -1) : prefix,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
//# sourceMappingURL=proxied-tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxied-tool.js","sourceRoot":"","sources":["../src/proxied-tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extract a human-meaningful failure message from a tool-call error. UTCP's
|
|
4
|
+
* HTTP protocol surfaces a non-2xx as an axios-style error whose `.response.data`
|
|
5
|
+
* is the REST endpoint's JSON body (`{ error: "..." }`). Pull that out so the
|
|
6
|
+
* MCP caller sees the tool's real message instead of a bare "status code 500".
|
|
7
|
+
*/
|
|
8
|
+
export declare function describeToolFailure(err: unknown): string;
|
|
9
|
+
export declare function toCallToolResult(value: unknown): CallToolResult;
|
|
10
|
+
export declare function renderProgress(chunk: unknown): string;
|
|
11
|
+
export declare function toolError(message: string): CallToolResult;
|
|
12
|
+
/**
|
|
13
|
+
* The result returned when the caller is missing personal credentials a tool
|
|
14
|
+
* needs. Marked `isError` so the external agent surfaces it to the person rather
|
|
15
|
+
* than treating it as tool output. Names the tool, lists the missing items, and
|
|
16
|
+
* gives the absolute setup-page URL so the person can provide them and retry.
|
|
17
|
+
*/
|
|
18
|
+
export declare function needsAuthorizationResult(toolName: string, missing: string[], connectUrl: string): CallToolResult;
|
|
19
|
+
//# sourceMappingURL=results.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAgBxD;AAsED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,cAAc,CAS/D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGrD;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CAEzD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EAAE,EACjB,UAAU,EAAE,MAAM,GACjB,cAAc,CAMhB"}
|
package/dist/results.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a human-meaningful failure message from a tool-call error. UTCP's
|
|
3
|
+
* HTTP protocol surfaces a non-2xx as an axios-style error whose `.response.data`
|
|
4
|
+
* is the REST endpoint's JSON body (`{ error: "..." }`). Pull that out so the
|
|
5
|
+
* MCP caller sees the tool's real message instead of a bare "status code 500".
|
|
6
|
+
*/
|
|
7
|
+
export function describeToolFailure(err) {
|
|
8
|
+
const data = err?.response?.data;
|
|
9
|
+
if (data && typeof data === 'object') {
|
|
10
|
+
const inner = data.error;
|
|
11
|
+
if (typeof inner === 'string' && inner.length > 0)
|
|
12
|
+
return inner;
|
|
13
|
+
}
|
|
14
|
+
if (typeof data === 'string' && data.length > 0)
|
|
15
|
+
return data;
|
|
16
|
+
// Total, like `safeJsonText`: a thrown value whose own `toString` throws
|
|
17
|
+
// (e.g. a null-prototype object) must still come back as a description —
|
|
18
|
+
// this function runs inside catch paths, where a second throw would turn
|
|
19
|
+
// a tool failure into a handler failure.
|
|
20
|
+
try {
|
|
21
|
+
return err instanceof Error ? err.message : String(err);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return '(indescribable tool failure)';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Turn a tool's final value into an MCP result:
|
|
29
|
+
* - a tool that already returns the MCP agentic shape (`{ content: [...] }`,
|
|
30
|
+
* each entry a real content block) is passed through unchanged;
|
|
31
|
+
* - a bare string becomes the text content;
|
|
32
|
+
* - anything else is JSON-stringified into one text block.
|
|
33
|
+
*
|
|
34
|
+
* Note we do NOT collapse a structured object down to its `text` field: doing
|
|
35
|
+
* so silently dropped the other fields (e.g. `ask`'s `status` / `sessionId`,
|
|
36
|
+
* the id a caller must echo back to poll or continue a conversation).
|
|
37
|
+
* Stringifying the whole object keeps every field, so the caller always sees
|
|
38
|
+
* the id it is responsible for echoing.
|
|
39
|
+
*
|
|
40
|
+
* The passthrough guard checks the entries, not just that `content` is an array:
|
|
41
|
+
* a domain value that merely happens to carry a `content` array of non-blocks
|
|
42
|
+
* (e.g. `{ content: ['a', 'b'] }`) is data, not an MCP result, so it falls
|
|
43
|
+
* through to JSON-stringify and survives intact instead of being emitted as a
|
|
44
|
+
* malformed result the client can't parse.
|
|
45
|
+
*/
|
|
46
|
+
function isMcpContentBlock(entry) {
|
|
47
|
+
return typeof entry === 'object' && entry !== null && typeof entry.type === 'string';
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `JSON.stringify`, made total. A tool can legally hand back a value JSON
|
|
51
|
+
* refuses verbatim — a BigInt or circular object (throws), a bare
|
|
52
|
+
* function/symbol (stringifies to `undefined`) — and a COMPLETED call must not
|
|
53
|
+
* turn into a crash at the serialization step. The plain stringify runs first
|
|
54
|
+
* so well-formed values (shared non-circular references included) come out
|
|
55
|
+
* exactly as before; only a value it rejects degrades to the replacer pass
|
|
56
|
+
* (BigInt → decimal string, its own ancestor → '[Circular]'), and a value even
|
|
57
|
+
* that can't take (e.g. a throwing `toJSON`) falls back to `String(value)`.
|
|
58
|
+
*/
|
|
59
|
+
function safeJsonText(value) {
|
|
60
|
+
try {
|
|
61
|
+
const text = JSON.stringify(value);
|
|
62
|
+
if (text !== undefined)
|
|
63
|
+
return text;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// BigInt / circular / throwing toJSON — degrade below.
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
// Circularity is being one's own ANCESTOR, not being visited twice: a
|
|
70
|
+
// shared (diamond) reference is legal JSON and stringify visits it once
|
|
71
|
+
// per parent, so a grown-only "seen" set would mislabel every sibling
|
|
72
|
+
// share as circular. The stack tracks only the active descent — the
|
|
73
|
+
// holder (`this`) of the current key is necessarily the innermost live
|
|
74
|
+
// ancestor, so popping down to it discards branches already unwound.
|
|
75
|
+
const ancestors = [];
|
|
76
|
+
const text = JSON.stringify(value, function (_key, v) {
|
|
77
|
+
if (typeof v === 'bigint')
|
|
78
|
+
return v.toString();
|
|
79
|
+
if (v && typeof v === 'object') {
|
|
80
|
+
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this)
|
|
81
|
+
ancestors.pop();
|
|
82
|
+
if (ancestors.includes(v))
|
|
83
|
+
return '[Circular]';
|
|
84
|
+
ancestors.push(v);
|
|
85
|
+
}
|
|
86
|
+
return v;
|
|
87
|
+
});
|
|
88
|
+
if (text !== undefined)
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// fall through to the value's own toString
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return String(value);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return '(unserializable tool output)';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
export function toCallToolResult(value) {
|
|
102
|
+
// Already in MCP agentic format — pass through untouched, but only when every
|
|
103
|
+
// `content` entry is a real content block (has a string `type`).
|
|
104
|
+
const content = value?.content;
|
|
105
|
+
if (value && typeof value === 'object' && Array.isArray(content) && content.every(isMcpContentBlock)) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
const text = typeof value === 'string' ? value : safeJsonText(value ?? null);
|
|
109
|
+
return { content: [{ type: 'text', text: text || '(tool produced no output)' }] };
|
|
110
|
+
}
|
|
111
|
+
export function renderProgress(chunk) {
|
|
112
|
+
const s = typeof chunk === 'string' ? chunk : safeJsonText(chunk);
|
|
113
|
+
return s.length > 500 ? s.slice(0, 497) + '...' : s;
|
|
114
|
+
}
|
|
115
|
+
export function toolError(message) {
|
|
116
|
+
return { isError: true, content: [{ type: 'text', text: message }] };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The result returned when the caller is missing personal credentials a tool
|
|
120
|
+
* needs. Marked `isError` so the external agent surfaces it to the person rather
|
|
121
|
+
* than treating it as tool output. Names the tool, lists the missing items, and
|
|
122
|
+
* gives the absolute setup-page URL so the person can provide them and retry.
|
|
123
|
+
*/
|
|
124
|
+
export function needsAuthorizationResult(toolName, missing, connectUrl) {
|
|
125
|
+
const items = missing.join(', ');
|
|
126
|
+
const text = `The "${toolName}" tool needs credentials you haven't set up yet: ${items}. ` +
|
|
127
|
+
`Open ${connectUrl} to connect your accounts and enter your keys, then run the tool again.`;
|
|
128
|
+
return { isError: true, content: [{ type: 'text', text }] };
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=results.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"results.js","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"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"}
|
package/dist/skills.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills exposed as MCP prompts (slash commands in an MCP client).
|
|
3
|
+
*
|
|
4
|
+
* Both surfaces do this the same way and must keep doing it the same way: a
|
|
5
|
+
* skill's instructions are the prompt body, and the bundled files are named at
|
|
6
|
+
* the end with the exact call that fetches them. A person running the same
|
|
7
|
+
* skill through the hosted endpoint and through the local server should get
|
|
8
|
+
* identical text, so the formatting lives here rather than in either server.
|
|
9
|
+
*/
|
|
10
|
+
/** Minimal skill shapes the prompt bridge needs (no dependency on `modules/skills`). */
|
|
11
|
+
export interface SkillSummary {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
export interface LoadedSkill extends SkillSummary {
|
|
16
|
+
body: string;
|
|
17
|
+
path: string;
|
|
18
|
+
files: string[];
|
|
19
|
+
}
|
|
20
|
+
/** The prompt message text for a loaded skill: its instructions body + a pointer to bundled files. */
|
|
21
|
+
export declare function skillPromptText(skill: LoadedSkill): string;
|
|
22
|
+
//# sourceMappingURL=skills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,YAAY;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,sGAAsG;AACtG,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,GAAG,MAAM,CAU1D"}
|
package/dist/skills.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills exposed as MCP prompts (slash commands in an MCP client).
|
|
3
|
+
*
|
|
4
|
+
* Both surfaces do this the same way and must keep doing it the same way: a
|
|
5
|
+
* skill's instructions are the prompt body, and the bundled files are named at
|
|
6
|
+
* the end with the exact call that fetches them. A person running the same
|
|
7
|
+
* skill through the hosted endpoint and through the local server should get
|
|
8
|
+
* identical text, so the formatting lives here rather than in either server.
|
|
9
|
+
*/
|
|
10
|
+
/** The prompt message text for a loaded skill: its instructions body + a pointer to bundled files. */
|
|
11
|
+
export function skillPromptText(skill) {
|
|
12
|
+
const rel = skill.files.map((f) => f.startsWith(`${skill.path}/`) ? f.slice(skill.path.length + 1) : f);
|
|
13
|
+
// Each name is quoted: a comma inside a file name must not read as a list
|
|
14
|
+
// separator, and the quoted form is exactly the `file` value get_skill takes.
|
|
15
|
+
const footer = rel.length
|
|
16
|
+
? `\n\n---\nSkill folder: ${skill.path}\nBundled files (fetch each with the get_skill tool: { name: ${JSON.stringify(skill.name)}, file }): ${rel.map((f) => JSON.stringify(f)).join(', ')}`
|
|
17
|
+
: '';
|
|
18
|
+
return `${skill.body}${footer}`;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=skills.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skills.js","sourceRoot":"","sources":["../src/skills.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAcH,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,KAAkB;IAChD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAChC,CAAC,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACpE,CAAC;IACF,0EAA0E;IAC1E,8EAA8E;IAC9E,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM;QACvB,CAAC,CAAC,0BAA0B,KAAK,CAAC,IAAI,gEAAgE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,cAAc,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;QAC5L,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,GAAG,KAAK,CAAC,IAAI,GAAG,MAAM,EAAE,CAAC;AAClC,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key UTCP uses to look up a manual's `${VAR}`. `@utcp/sdk` first SANITIZES
|
|
3
|
+
* the manual name — every non-word char (`-`, `.`, spaces, …) becomes `_`
|
|
4
|
+
* (`manualCallTemplate.name.replace(/[^\w]/g, "_")` at registration) — and then
|
|
5
|
+
* namespaces variables under `<namespace>_<VAR>` where every `_` in the
|
|
6
|
+
* sanitized namespace is DOUBLED (the variable substitutor), so `my_tool` + `KEY`
|
|
7
|
+
* → `my__tool_KEY` and `my-tool` + `KEY` → the very same `my__tool_KEY`. Our
|
|
8
|
+
* secret storage AND scope resolution must derive the exact same key. For an
|
|
9
|
+
* alphanumeric name (no `_`, no punctuation) this reduces to `<name>_<VAR>`,
|
|
10
|
+
* so plain tool names are unaffected.
|
|
11
|
+
*/
|
|
12
|
+
export declare function utcpNamespacePrefix(manualName: string): string;
|
|
13
|
+
export declare function utcpNamespacedKey(manualName: string, varName: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Seed the loopback discovery vars (`<ns>_API_URL` + `<ns>_CONNECTION_KEY`) for
|
|
16
|
+
* every Bevel-HOSTED manual in `manuals` — one whose discovery `url` targets our
|
|
17
|
+
* own backend with `${API_URL}` as its ORIGIN (the KB manual and inline `.tool`
|
|
18
|
+
* sub-manuals). A third-party http/mcp `.tool` points at an arbitrary URL, so it
|
|
19
|
+
* is deliberately NOT seeded: handing it `connectionKey` would leak the bearer to
|
|
20
|
+
* that endpoint. Keying off the origin (not a bare `includes('${API_URL}')`) is
|
|
21
|
+
* deliberate — an `http` `.tool`'s URL is author-controlled, so a substring match
|
|
22
|
+
* would let `http://attacker/?x=${API_URL}` be seeded the bearer and exfiltrate
|
|
23
|
+
* it. This is the exfiltration-safe seeding rule shared by the external MCP proxy
|
|
24
|
+
* and the in-process agent factory, so the bearer-leak decision is written ONCE.
|
|
25
|
+
*/
|
|
26
|
+
export declare function seedBevelHostedManualVars(manuals: readonly {
|
|
27
|
+
name?: unknown;
|
|
28
|
+
url?: unknown;
|
|
29
|
+
}[], loopbackBaseUrl: string, connectionKey: string): Record<string, string>;
|
|
30
|
+
//# sourceMappingURL=utcp-namespace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utcp-namespace.d.ts","sourceRoot":"","sources":["../src/utcp-namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,EACrD,eAAe,EAAE,MAAM,EACvB,aAAa,EAAE,MAAM,GACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The key UTCP uses to look up a manual's `${VAR}`. `@utcp/sdk` first SANITIZES
|
|
3
|
+
* the manual name — every non-word char (`-`, `.`, spaces, …) becomes `_`
|
|
4
|
+
* (`manualCallTemplate.name.replace(/[^\w]/g, "_")` at registration) — and then
|
|
5
|
+
* namespaces variables under `<namespace>_<VAR>` where every `_` in the
|
|
6
|
+
* sanitized namespace is DOUBLED (the variable substitutor), so `my_tool` + `KEY`
|
|
7
|
+
* → `my__tool_KEY` and `my-tool` + `KEY` → the very same `my__tool_KEY`. Our
|
|
8
|
+
* secret storage AND scope resolution must derive the exact same key. For an
|
|
9
|
+
* alphanumeric name (no `_`, no punctuation) this reduces to `<name>_<VAR>`,
|
|
10
|
+
* so plain tool names are unaffected.
|
|
11
|
+
*/
|
|
12
|
+
export function utcpNamespacePrefix(manualName) {
|
|
13
|
+
return `${manualName.replace(/[^\w]/g, '_').replace(/_/g, '__')}_`;
|
|
14
|
+
}
|
|
15
|
+
export function utcpNamespacedKey(manualName, varName) {
|
|
16
|
+
return `${utcpNamespacePrefix(manualName)}${varName}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The origin template WE mint for every Bevel-hosted manual's discovery URL
|
|
20
|
+
* (`${API_URL}/api/…`). `${API_URL}` expands to the loopback origin, so a
|
|
21
|
+
* Bevel-hosted URL always has this exact string as its scheme+authority.
|
|
22
|
+
*/
|
|
23
|
+
const LOOPBACK_ORIGIN_TEMPLATE = '${API_URL}';
|
|
24
|
+
/**
|
|
25
|
+
* True only when `${API_URL}` is the URL's ORIGIN (scheme+authority) — the shape
|
|
26
|
+
* we ourselves produce for a Bevel-hosted manual. A third-party `.tool` author
|
|
27
|
+
* can embed the literal `${API_URL}` anywhere in a path or query
|
|
28
|
+
* (`http://evil.example/?x=${API_URL}`) but can NOT make it the authority without
|
|
29
|
+
* pointing the request back at our own loopback, so anchoring the trust decision
|
|
30
|
+
* to the origin is what a user `.tool`'s URL text cannot forge.
|
|
31
|
+
*/
|
|
32
|
+
function isBevelHostedUrl(url) {
|
|
33
|
+
if (!url.startsWith(LOOPBACK_ORIGIN_TEMPLATE))
|
|
34
|
+
return false;
|
|
35
|
+
// The char right after the origin must delimit the authority; anything else
|
|
36
|
+
// (`${API_URL}.evil.com`, `${API_URL}evil`) is a different, untrusted host.
|
|
37
|
+
const next = url.charAt(LOOPBACK_ORIGIN_TEMPLATE.length);
|
|
38
|
+
return next === '' || next === '/' || next === '?' || next === '#';
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Seed the loopback discovery vars (`<ns>_API_URL` + `<ns>_CONNECTION_KEY`) for
|
|
42
|
+
* every Bevel-HOSTED manual in `manuals` — one whose discovery `url` targets our
|
|
43
|
+
* own backend with `${API_URL}` as its ORIGIN (the KB manual and inline `.tool`
|
|
44
|
+
* sub-manuals). A third-party http/mcp `.tool` points at an arbitrary URL, so it
|
|
45
|
+
* is deliberately NOT seeded: handing it `connectionKey` would leak the bearer to
|
|
46
|
+
* that endpoint. Keying off the origin (not a bare `includes('${API_URL}')`) is
|
|
47
|
+
* deliberate — an `http` `.tool`'s URL is author-controlled, so a substring match
|
|
48
|
+
* would let `http://attacker/?x=${API_URL}` be seeded the bearer and exfiltrate
|
|
49
|
+
* it. This is the exfiltration-safe seeding rule shared by the external MCP proxy
|
|
50
|
+
* and the in-process agent factory, so the bearer-leak decision is written ONCE.
|
|
51
|
+
*/
|
|
52
|
+
export function seedBevelHostedManualVars(manuals, loopbackBaseUrl, connectionKey) {
|
|
53
|
+
const variables = {};
|
|
54
|
+
for (const m of manuals) {
|
|
55
|
+
const name = typeof m.name === 'string' ? m.name : '';
|
|
56
|
+
if (!name)
|
|
57
|
+
continue;
|
|
58
|
+
const url = m.url;
|
|
59
|
+
if (typeof url !== 'string' || !isBevelHostedUrl(url))
|
|
60
|
+
continue;
|
|
61
|
+
variables[utcpNamespacedKey(name, 'API_URL')] = loopbackBaseUrl;
|
|
62
|
+
variables[utcpNamespacedKey(name, 'CONNECTION_KEY')] = connectionKey;
|
|
63
|
+
}
|
|
64
|
+
return variables;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=utcp-namespace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utcp-namespace.js","sourceRoot":"","sources":["../src/utcp-namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAkB;IACpD,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC;AACrE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,UAAkB,EAAE,OAAe;IACnE,OAAO,GAAG,mBAAmB,CAAC,UAAU,CAAC,GAAG,OAAO,EAAE,CAAC;AACxD,CAAC;AAED;;;;GAIG;AACH,MAAM,wBAAwB,GAAG,YAAY,CAAC;AAE9C;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,GAAW;IACnC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,wBAAwB,CAAC;QAAE,OAAO,KAAK,CAAC;IAC5D,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACzD,OAAO,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,CAAC;AACrE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,yBAAyB,CACvC,OAAqD,EACrD,eAAuB,EACvB,aAAqB;IAErB,MAAM,SAAS,GAA2B,EAAE,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAI,CAAuB,CAAC,GAAG,CAAC;QACzC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC;YAAE,SAAS;QAChE,SAAS,CAAC,iBAAiB,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,GAAG,eAAe,CAAC;QAChE,SAAS,CAAC,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAAC,GAAG,aAAa,CAAC;IACvE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bevel-software/platform-mcp-core",
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "The transport-agnostic half of Bevel's MCP surface: UTCP manual registration, tool discovery, MCP listing/dispatch and the code-mode meta-tools. Shared by the hosted proxy and the local stdio server.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"src",
|
|
18
|
+
"!src/__tests__",
|
|
19
|
+
"!dist/__tests__",
|
|
20
|
+
"THIRD-PARTY-NOTICES.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22 <23"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"@utcp/code-mode": "^1.2.12",
|
|
28
|
+
"@utcp/http": "^1.1.11",
|
|
29
|
+
"@utcp/mcp": "^1.1.3",
|
|
30
|
+
"@utcp/sdk": "^1.1.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"typescript": "^5.8.0",
|
|
34
|
+
"vitest": "^3.2.6"
|
|
35
|
+
},
|
|
36
|
+
"license": "Apache-2.0",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/Bevel-Software/Hexis.git",
|
|
40
|
+
"directory": "packages/mcp-core"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
50
|
+
}
|