@redocly/realm 0.136.0-next.13 → 0.136.0-next.15
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 +33 -0
- package/dist/constants/api.d.ts +1 -0
- package/dist/constants/api.js +1 -1
- package/dist/server/constants/common.d.ts +1 -0
- package/dist/server/constants/common.js +1 -1
- package/dist/server/plugins/mcp/audit/audit-log.d.ts +36 -0
- package/dist/server/plugins/mcp/audit/audit-log.js +1 -0
- package/dist/server/plugins/mcp/audit/constants.d.ts +6 -0
- package/dist/server/plugins/mcp/audit/constants.js +1 -0
- package/dist/server/plugins/mcp/audit/secret-patterns.d.ts +2 -0
- package/dist/server/plugins/mcp/audit/secret-patterns.js +1 -0
- package/dist/server/plugins/mcp/codemode/build-tool-list.d.ts +12 -0
- package/dist/server/plugins/mcp/codemode/build-tool-list.js +2 -0
- package/dist/server/plugins/mcp/codemode/capabilities/fetch/index.js +4 -4
- package/dist/server/plugins/mcp/codemode/capabilities/types.d.ts +1 -0
- package/dist/server/plugins/mcp/codemode/capabilities/utils.js +3 -3
- package/dist/server/plugins/mcp/codemode/code-execution-audit.d.ts +17 -0
- package/dist/server/plugins/mcp/codemode/code-execution-audit.js +1 -0
- package/dist/server/plugins/mcp/codemode/prompts.d.ts +6 -3
- package/dist/server/plugins/mcp/codemode/prompts.js +7 -4
- package/dist/server/plugins/mcp/codemode/sandbox/create-sandbox-setup.js +1 -1
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/fetch-shim.d.ts +6 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/fetch-shim.js +54 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/form-data.d.ts +3 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/form-data.js +17 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/index.d.ts +3 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/index.js +2 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/url-search-params.d.ts +3 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/url-search-params.js +64 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/url.d.ts +3 -0
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills/url.js +239 -0
- package/dist/server/plugins/mcp/codemode/tools/describe-tools.js +1 -1
- package/dist/server/plugins/mcp/codemode/tools/execute.js +1 -1
- package/dist/server/plugins/mcp/codemode/tools/tool-errors.d.ts +4 -0
- package/dist/server/plugins/mcp/codemode/tools/tool-errors.js +1 -0
- package/dist/server/plugins/mcp/docs-mcp/tools/openapi/get-endpoint-info.js +1 -1
- package/dist/server/plugins/mcp/docs-mcp/utils.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/elicitation.js +3 -3
- package/dist/server/plugins/mcp/gateway-mcp/fetch/fetch-audit.d.ts +24 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch/fetch-audit.js +1 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch/fetch-bridge.js +1 -1
- package/dist/server/plugins/mcp/handlers/mcp-credentials-handler.js +12 -8
- package/dist/server/plugins/mcp/servers/docs-server.js +1 -1
- package/dist/server/plugins/mcp/servers/mcp-server.d.ts +1 -0
- package/dist/server/plugins/mcp/servers/mcp-server.js +2 -2
- package/dist/server/plugins/mcp/workers/execute-mcp-tool.js +1 -1
- package/dist/server/plugins/mcp/workers/mcp-tool-telemetry.d.ts +11 -0
- package/dist/server/plugins/mcp/workers/mcp-tool-telemetry.js +1 -1
- package/dist/server/tools/notifiers/formatter.d.ts +1 -0
- package/dist/server/tools/notifiers/formatter.js +3 -3
- package/dist/server/tools/notifiers/logger.d.ts +1 -0
- package/dist/server/tools/notifiers/logger.js +2 -2
- package/dist/server/web-server/routes/ask-ai.d.ts +5 -0
- package/dist/server/web-server/routes/ask-ai.js +1 -1
- package/dist/server/web-server/routes/index.js +1 -1
- package/dist/server/web-server/routes/mcp-routes/mcp-server-card.js +1 -1
- package/package.json +12 -12
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills.d.ts +0 -2
- package/dist/server/plugins/mcp/codemode/sandbox/polyfills.js +0 -99
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const t=`
|
|
2
|
+
// Collapses '.' and '..'. A backslash separates path segments, the way the host parser reads
|
|
3
|
+
// one, so it is folded into '/' first \u2014 otherwise the two disagree about where the host name
|
|
4
|
+
// ends in 'https://a.example\\@b.example/', and guest code reads a host the gateway never calls.
|
|
5
|
+
const normalizePath = (rawPath) => {
|
|
6
|
+
const path = rawPath.replace(/\\\\/g, '/');
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const segment of path.split('/')) {
|
|
9
|
+
if (segment === '.') continue;
|
|
10
|
+
if (segment === '..') { if (out.length > 1) out.pop(); continue; }
|
|
11
|
+
out.push(segment);
|
|
12
|
+
}
|
|
13
|
+
let joined = out.join('/');
|
|
14
|
+
if ((path.endsWith('/.') || path.endsWith('/..')) && !joined.endsWith('/')) joined += '/';
|
|
15
|
+
return joined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PORTS = { 'http:': '80', 'https:': '443', 'ws:': '80', 'wss:': '443', 'ftp:': '21' };
|
|
19
|
+
|
|
20
|
+
// Percent-encode what cannot travel literally in a url, so a hand-assembled path or query
|
|
21
|
+
// reaches the host parser as the caller meant it. Non-ASCII goes out as UTF-8, in runs, so a
|
|
22
|
+
// surrogate pair is encoded as the one character it stands for.
|
|
23
|
+
const encodeLiterals = (value) =>
|
|
24
|
+
value.replace(/[\\s"<>\\\`{}\\\\^]|[^\\x00-\\x7F]+/g, (chunk) => {
|
|
25
|
+
try { return encodeURIComponent(chunk); } catch (error) { return chunk; }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// A non-ASCII host name is lower-cased but not punycoded \u2014 that needs an IDNA table this sandbox
|
|
29
|
+
// does not carry. The host re-parses the url before it fetches, so the request still goes to the
|
|
30
|
+
// punycoded name; only what guest code reads back differs.
|
|
31
|
+
const parseHostPort = (hostport, scheme) => {
|
|
32
|
+
let hostname = hostport;
|
|
33
|
+
let port = '';
|
|
34
|
+
if (hostport.startsWith('[')) {
|
|
35
|
+
const close = hostport.indexOf(']');
|
|
36
|
+
hostname = hostport.slice(0, close + 1);
|
|
37
|
+
const after = hostport.slice(close + 1);
|
|
38
|
+
if (after.startsWith(':')) port = after.slice(1);
|
|
39
|
+
} else {
|
|
40
|
+
const colon = hostport.lastIndexOf(':');
|
|
41
|
+
if (colon !== -1) { hostname = hostport.slice(0, colon); port = hostport.slice(colon + 1); }
|
|
42
|
+
}
|
|
43
|
+
if (port !== '' && !/^[0-9]+$/.test(port)) return null;
|
|
44
|
+
if (hostname === '') return null;
|
|
45
|
+
return {
|
|
46
|
+
hostname: hostname.toLowerCase(),
|
|
47
|
+
port: DEFAULT_PORTS[scheme] === port ? '' : port,
|
|
48
|
+
// A host written without a port leaves the port alone, rather than clearing it.
|
|
49
|
+
hasPort: port !== '',
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ABSOLUTE_RE = /^([A-Za-z][A-Za-z0-9+\\-.]*):(\\/\\/)?([\\s\\S]*)$/;
|
|
54
|
+
|
|
55
|
+
const parseAuthorityUrl = (scheme, rest) => {
|
|
56
|
+
const cut = rest.search(/[/\\\\?#]/);
|
|
57
|
+
const authority = cut === -1 ? rest : rest.slice(0, cut);
|
|
58
|
+
let tail = cut === -1 ? '' : rest.slice(cut);
|
|
59
|
+
let username = '';
|
|
60
|
+
let password = '';
|
|
61
|
+
let hostport = authority;
|
|
62
|
+
const at = authority.lastIndexOf('@');
|
|
63
|
+
if (at !== -1) {
|
|
64
|
+
const credentials = authority.slice(0, at);
|
|
65
|
+
hostport = authority.slice(at + 1);
|
|
66
|
+
const colon = credentials.indexOf(':');
|
|
67
|
+
username = colon === -1 ? credentials : credentials.slice(0, colon);
|
|
68
|
+
password = colon === -1 ? '' : credentials.slice(colon + 1);
|
|
69
|
+
}
|
|
70
|
+
const hostPort = parseHostPort(hostport, scheme);
|
|
71
|
+
if (!hostPort) return null;
|
|
72
|
+
let hash = '';
|
|
73
|
+
const hashAt = tail.indexOf('#');
|
|
74
|
+
if (hashAt !== -1) { hash = tail.slice(hashAt); tail = tail.slice(0, hashAt); }
|
|
75
|
+
let search = '';
|
|
76
|
+
const queryAt = tail.indexOf('?');
|
|
77
|
+
if (queryAt !== -1) { search = tail.slice(queryAt); tail = tail.slice(0, queryAt); }
|
|
78
|
+
let pathname = tail === '' ? '/' : tail;
|
|
79
|
+
if (!/^[/\\\\]/.test(pathname)) pathname = '/' + pathname;
|
|
80
|
+
return {
|
|
81
|
+
protocol: scheme,
|
|
82
|
+
username,
|
|
83
|
+
password,
|
|
84
|
+
hostname: hostPort.hostname,
|
|
85
|
+
port: hostPort.port,
|
|
86
|
+
pathname: encodeLiterals(normalizePath(pathname)),
|
|
87
|
+
search: encodeLiterals(search === '?' ? '' : search),
|
|
88
|
+
hash: encodeLiterals(hash === '#' ? '' : hash),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const parseUrl = (input, base) => {
|
|
93
|
+
// A tab or newline inside a url is dropped, not encoded \u2014 the host parser does the same, and
|
|
94
|
+
// a url copied out of a wrapped response body is the usual way one gets in here.
|
|
95
|
+
const str = String(input).trim().replace(/[\\t\\n\\r]/g, '');
|
|
96
|
+
const matched = ABSOLUTE_RE.exec(str);
|
|
97
|
+
// A scheme with no \`//\` (mailto:, data:) is not a fetch target, so it is not resolved.
|
|
98
|
+
if (matched) return matched[2] ? parseAuthorityUrl(matched[1].toLowerCase() + ':', matched[3]) : null;
|
|
99
|
+
if (base === undefined) return null;
|
|
100
|
+
const resolved = parseUrl(base, undefined);
|
|
101
|
+
if (!resolved) return null;
|
|
102
|
+
if (/^[/\\\\]{2}/.test(str)) return parseAuthorityUrl(resolved.protocol, str.slice(2));
|
|
103
|
+
const parts = { ...resolved };
|
|
104
|
+
if (str.startsWith('#')) {
|
|
105
|
+
parts.hash = encodeLiterals(str === '#' ? '' : str);
|
|
106
|
+
return parts;
|
|
107
|
+
}
|
|
108
|
+
let rest = str;
|
|
109
|
+
let hash = '';
|
|
110
|
+
const hashAt = rest.indexOf('#');
|
|
111
|
+
if (hashAt !== -1) { hash = rest.slice(hashAt); rest = rest.slice(0, hashAt); }
|
|
112
|
+
let search = '';
|
|
113
|
+
const queryAt = rest.indexOf('?');
|
|
114
|
+
if (queryAt !== -1) { search = rest.slice(queryAt); rest = rest.slice(0, queryAt); }
|
|
115
|
+
if (rest !== '') {
|
|
116
|
+
const absolute = /^[/\\\\]/.test(rest)
|
|
117
|
+
? rest
|
|
118
|
+
: resolved.pathname.slice(0, resolved.pathname.lastIndexOf('/') + 1) + rest;
|
|
119
|
+
parts.pathname = encodeLiterals(normalizePath(absolute));
|
|
120
|
+
}
|
|
121
|
+
// An empty reference with no query of its own keeps the base query (RFC 3986 section 5.3).
|
|
122
|
+
parts.search = rest === '' && search === '' ? resolved.search : encodeLiterals(search === '?' ? '' : search);
|
|
123
|
+
parts.hash = encodeLiterals(hash === '#' ? '' : hash);
|
|
124
|
+
return parts;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const URL = class URL {
|
|
128
|
+
constructor(input, base) {
|
|
129
|
+
const parsed = parseUrl(input, base === undefined ? undefined : String(base));
|
|
130
|
+
if (!parsed) throw new TypeError('Invalid URL: ' + String(input));
|
|
131
|
+
this._parts = parsed;
|
|
132
|
+
this._params = undefined;
|
|
133
|
+
this._edited = false;
|
|
134
|
+
}
|
|
135
|
+
// The query stays exactly as it arrived until searchParams edits it, so reading a url apart
|
|
136
|
+
// and putting it back together does not re-encode what the server sent.
|
|
137
|
+
_applyParams() {
|
|
138
|
+
if (this._edited && this._params) {
|
|
139
|
+
const query = this._params.toString();
|
|
140
|
+
this._parts.search = query ? '?' + query : '';
|
|
141
|
+
this._edited = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Every part a caller can read, it can also write. Guest code runs sloppy, so a missing
|
|
145
|
+
// setter would swallow \`url.host = \u2026\` and fetch the address the caller meant to replace.
|
|
146
|
+
get protocol() { return this._parts.protocol; }
|
|
147
|
+
set protocol(value) {
|
|
148
|
+
const next = String(value).toLowerCase().replace(/:$/, '') + ':';
|
|
149
|
+
// Only a scheme that carries an authority can replace one, the way the host parser reads it.
|
|
150
|
+
if (DEFAULT_PORTS[next] !== undefined) this._parts.protocol = next;
|
|
151
|
+
}
|
|
152
|
+
get username() { return this._parts.username; }
|
|
153
|
+
set username(value) { this._parts.username = String(value); }
|
|
154
|
+
get password() { return this._parts.password; }
|
|
155
|
+
set password(value) { this._parts.password = String(value); }
|
|
156
|
+
get hostname() { return this._parts.hostname; }
|
|
157
|
+
set hostname(value) { this._parts.hostname = String(value).toLowerCase(); }
|
|
158
|
+
get port() { return this._parts.port; }
|
|
159
|
+
set port(value) {
|
|
160
|
+
const port = String(value);
|
|
161
|
+
if (port !== '' && !/^[0-9]+$/.test(port)) return;
|
|
162
|
+
this._parts.port = DEFAULT_PORTS[this._parts.protocol] === port ? '' : port;
|
|
163
|
+
}
|
|
164
|
+
get host() {
|
|
165
|
+
return this._parts.port ? this._parts.hostname + ':' + this._parts.port : this._parts.hostname;
|
|
166
|
+
}
|
|
167
|
+
set host(value) {
|
|
168
|
+
const parsed = parseHostPort(String(value), this._parts.protocol);
|
|
169
|
+
if (!parsed) return;
|
|
170
|
+
this._parts.hostname = parsed.hostname;
|
|
171
|
+
if (parsed.hasPort) this._parts.port = parsed.port;
|
|
172
|
+
}
|
|
173
|
+
get origin() { return this._parts.protocol + '//' + this.host; }
|
|
174
|
+
get pathname() { return this._parts.pathname; }
|
|
175
|
+
set pathname(value) {
|
|
176
|
+
const path = String(value);
|
|
177
|
+
this._parts.pathname = encodeLiterals(path.startsWith('/') ? path : '/' + path);
|
|
178
|
+
}
|
|
179
|
+
get search() {
|
|
180
|
+
this._applyParams();
|
|
181
|
+
return this._parts.search;
|
|
182
|
+
}
|
|
183
|
+
set search(value) {
|
|
184
|
+
const query = String(value);
|
|
185
|
+
const body = query.startsWith('?') ? query.slice(1) : query;
|
|
186
|
+
this._parts.search = body ? encodeLiterals('?' + body) : '';
|
|
187
|
+
this._params = undefined;
|
|
188
|
+
this._edited = false;
|
|
189
|
+
}
|
|
190
|
+
get searchParams() {
|
|
191
|
+
if (!this._params) {
|
|
192
|
+
const params = new URLSearchParams(this._parts.search);
|
|
193
|
+
const url = this;
|
|
194
|
+
// Every method that can change the parameters has to mark the query for re-serialization.
|
|
195
|
+
for (const method of ['append', 'set', 'delete', 'sort']) {
|
|
196
|
+
const original = params[method];
|
|
197
|
+
params[method] = function (...args) {
|
|
198
|
+
url._edited = true;
|
|
199
|
+
return original.apply(params, args);
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
this._params = params;
|
|
203
|
+
}
|
|
204
|
+
return this._params;
|
|
205
|
+
}
|
|
206
|
+
get hash() { return this._parts.hash; }
|
|
207
|
+
set hash(value) {
|
|
208
|
+
const fragment = String(value);
|
|
209
|
+
this._parts.hash = fragment === ''
|
|
210
|
+
? ''
|
|
211
|
+
: encodeLiterals(fragment.startsWith('#') ? fragment : '#' + fragment);
|
|
212
|
+
}
|
|
213
|
+
get href() {
|
|
214
|
+
this._applyParams();
|
|
215
|
+
const credentials = this._parts.username
|
|
216
|
+
? this._parts.username + (this._parts.password ? ':' + this._parts.password : '') + '@'
|
|
217
|
+
: '';
|
|
218
|
+
return this._parts.protocol + '//' + credentials + this.host +
|
|
219
|
+
this._parts.pathname + this._parts.search + this._parts.hash;
|
|
220
|
+
}
|
|
221
|
+
set href(value) {
|
|
222
|
+
const parsed = parseUrl(value, undefined);
|
|
223
|
+
if (!parsed) throw new TypeError('Invalid URL: ' + String(value));
|
|
224
|
+
this._parts = parsed;
|
|
225
|
+
this._params = undefined;
|
|
226
|
+
this._edited = false;
|
|
227
|
+
}
|
|
228
|
+
toString() { return this.href; }
|
|
229
|
+
toJSON() { return this.href; }
|
|
230
|
+
static canParse(input, base) {
|
|
231
|
+
return parseUrl(input, base === undefined ? undefined : String(base)) !== null;
|
|
232
|
+
}
|
|
233
|
+
static parse(input, base) {
|
|
234
|
+
const parsed = parseUrl(input, base === undefined ? undefined : String(base));
|
|
235
|
+
return parsed ? new URL(input, base) : null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
globalThis.URL = URL;
|
|
239
|
+
`;export{t as URL_POLYFILL};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{renderDescribeToolsOutput as
|
|
1
|
+
import{renderDescribeToolsOutput as k}from"../capabilities/index.js";import{McpServerType as h}from"../../constants.js";import{reportMcpToolCalled as E,reportMcpToolError as S}from"../../workers/mcp-tool-telemetry.js";import{DESCRIBE_TOOLS_TOOL_NAME as w}from"../constants.js";import{sanitizeToolName as s}from"../build-tool-type-declarations.js";import{UnknownToolError as T}from"./tool-errors.js";function x(i,l){const r=[...new Set([...i.map(n=>s(n.name)),...l.map(n=>s(n.id))])];return{type:"object",required:[],additionalProperties:!1,properties:{tools:{type:"array",description:`Tool names to describe: ${r.join(", ")}. Omit to describe everything.`,items:r.length>0?{type:"string",enum:r}:{type:"string"},nullable:!0}}}}function L(i,l,r,n){const c=new Map(i.map(t=>[s(t.name),t])),p=new Map(r.map(t=>[s(t.id),t]));return async t=>{const m=performance.now(),d=n.mcpMode??"code";try{const o=[...new Set((t.tools??[]).map(s))],a=o.length===0,f=o.filter(e=>!c.has(e)&&!p.has(e));if(f.length>0){const e=[...c.keys(),...p.keys()];throw new T(`Unknown tool(s): ${f.join(", ")}. Valid tools: ${e.join(", ")}.`)}const g=a?r:r.filter(e=>o.includes(s(e.id))),u=a?i:o.filter(e=>c.has(e)).map(e=>c.get(e)),M=await Promise.all(u.map(e=>l(e.name))),b=u.filter((e,v)=>M[v]),y=k(b,g,n);return E({tool:w,mode:d,serverType:h.Docs,durationMs:Math.round(performance.now()-m),outputLength:y.length}),{content:[{type:"text",text:y}]}}catch(o){const a=o instanceof T;throw S({tool:w,mode:d,serverType:h.Docs,durationMs:Math.round(performance.now()-m),message:o instanceof Error?o.message:String(o),stack:!a&&o instanceof Error&&o.stack||"",errorClass:a?"tool_error":"server_error"}),o}}}export{L as describeTools,x as describeToolsSchema};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{mcpToolHandlers as l}from"../../../../../client/mcp-tool-handlers-entry.js";import{runInSandbox as p}from"../../../../sandbox/sandbox.js";import{isMcpToolAvailable as h}from"../../workers/execute-mcp-tool.js";import{buildExecuteHostFetch as b,isFetchCapable as E}from"../../gateway-mcp/fetch-capability.js";import{withAuthRequiredMeta as T}from"../../gateway-mcp/upstream-auth.js";import{CODE_MODE_TOOL_NAMES as
|
|
1
|
+
import{mcpToolHandlers as l}from"../../../../../client/mcp-tool-handlers-entry.js";import{runInSandbox as p}from"../../../../sandbox/sandbox.js";import{isMcpToolAvailable as h}from"../../workers/execute-mcp-tool.js";import{buildExecuteHostFetch as b,isFetchCapable as E}from"../../gateway-mcp/fetch-capability.js";import{withAuthRequiredMeta as T}from"../../gateway-mcp/upstream-auth.js";import{CODE_MODE_TOOL_NAMES as C,EXECUTE_TOOL_NAME as O}from"../constants.js";import{sanitizeToolName as A}from"../build-tool-type-declarations.js";import{invokeTool as w}from"../invoke-tool.js";import{createSandboxSetup as x}from"../sandbox/create-sandbox-setup.js";import{auditCode as y}from"../code-audit.js";import{reportCodeExecuted as M}from"../code-execution-telemetry.js";import{logCodeExecutionAudit as N}from"../code-execution-audit.js";const v=async(e,t,r)=>{const o=await F(t,r),n=[],i=E(t)?b(t,c=>n.push(c)):void 0;let a=0;const u=i?c=>(a+=1,i(c)):void 0,d=y(e.code);N({audit:d,code:e.code,description:e.description,sessionHandle:e.sessionHandle,toolNames:Object.keys(o),fetchBudget:t.gatewayFetchBudgetPerExec,fetchCapable:i!==void 0});const m=Date.now(),{result:f,error:s}=await p(e.code,{setupContext:x(o,u)});return M({audit:d,fetchCount:a,outcome:s===void 0?"ok":"error",durationMs:Date.now()-m}),T({content:[{type:"text",text:JSON.stringify({result:f,error:s})}],isError:s!==void 0},n)};async function F(e,t){const r={};for(const o of Object.keys(l))C.has(o)||!await h({toolName:o,context:e}).catch(()=>!1)||(r[A(o)]=i=>w(o,i,e,t));return r}var J={[O]:v};export{J as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
class n extends Error{constructor(r){super(r),this.name="UnknownToolError"}}export{n as UnknownToolError};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveParameters as f,resolveRequestBody as
|
|
1
|
+
import{resolveParameters as f,resolveRequestBody as l,resolveResponses as y}from"../../utils.js";import{isMcpEndpoint as x}from"./utils.js";import{loadApiDescription as E}from"./load-api-description.js";import{checkEndpointAndDeleteXMcp as v}from"../../../utils/xmcp-utils.js";const g=async(s,i)=>{const{path:o,method:r}=s;if(!o||!r)return{content:[{type:"text",text:'Both "path" and "method" are required (e.g. path: "/tickets", method: "GET").'}],isError:!0};let t;try{t=await E(s,i)}catch(h){return{content:[{type:"text",text:h.message}],isError:!0}}const p=o.startsWith("/")?o:`/${o}`,{title:a=""}=t.info||{},n=t.paths?.[p],c=r.toLowerCase();if(!n)return{content:[{type:"text",text:"Endpoint not found"}],isError:!0};const e=n[c];if(!x(e)||!v(e,"docs"))return{content:[{type:"text",text:"Endpoint not found"}],isError:!0};const d=n?.parameters||[],m=e.parameters||[],u={...e,parameters:f({pathParams:d,opParams:m,definition:t}),requestBody:l(e.requestBody,t),responses:y(e.responses,t)};return{content:[{type:"text",text:JSON.stringify({api:a,version:t.info?.version||"",servers:t.servers||[],endpoint:{path:o,method:r.toUpperCase(),...u},globalSecurity:t.security||[],securitySchemes:t.components?.securitySchemes||{}},null,2)}]}};var w={"get-endpoint-info":g};export{w as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
function
|
|
1
|
+
function v(e,o,s){if(!o)return;const r=o.toLowerCase(),t=Object.values(e).filter(({name:f})=>f.toLowerCase()===r);if(t.length===0)return;const n=s?t.filter(f=>f.version===s):t;if(n.length===0)return;if(n.length===1)return n[0];if(new Set(n.map(({version:f})=>f||"")).size>1){const f=n.map(({version:l})=>l||"(no version)").join(", ");throw new Error(`Multiple versions of "${o}" found: ${f}. Specify a version to disambiguate.`)}const i=n.map(({relativePath:f})=>`"${f||"(unknown path)"}"`).join(", "),u=n[0].version?` at version "${n[0].version}"`:"";throw new Error(`Multiple APIs are titled "${o}"${u}. Pass the "filePath" parameter to select one of: ${i}.`)}function h(e,o){return Object.values(e).filter(({name:s})=>s.toLowerCase().includes(o.toLowerCase()))}function d(e,o){if(!o||!e?.startsWith("#/"))return;const s=e.slice(2).split("/");let r=o;for(const t of s)if(typeof r=="object"&&r!==null&&t in r)r=r[t];else return;return r}function p(e,o){const s=Object.fromEntries(Object.entries(e).filter(([r,t])=>r!=="$ref"&&t!==void 0));return{...o,...s}}function a({schema:e,definition:o,visitedRefs:s=new Set,maxDepth:r=50,currentDepth:t=0}){if(!e)return;if(t>=r)return{type:"object",description:`Maximum resolution depth exceeded (${r})`};if("$ref"in e&&e.$ref){if(s.has(e.$ref))return{type:"object",description:`Circular reference detected: ${e.$ref}`,"x-circular-ref":e.$ref};const i=new Set(s);i.add(e.$ref);const u=d(e.$ref,o);if(!u)return{type:"object",description:`Could not resolve reference: ${e.$ref}`,"x-unresolved-ref":e.$ref};const f=a({schema:u,definition:o,visitedRefs:i,maxDepth:r,currentDepth:t+1});return f&&p(e,f)}const n={...e};if(n.properties){const i={};for(const[u,f]of Object.entries(n.properties)){const l=a({schema:f,definition:o,visitedRefs:new Set(s),maxDepth:r,currentDepth:t+1});i[u]=l||f}n.properties=i}if(n.items&&typeof n.items=="object")if(Array.isArray(n.items))n.items=n.items.map(i=>a({schema:i,definition:o,visitedRefs:new Set(s),maxDepth:r,currentDepth:t+1})||i);else{const i=a({schema:n.items,definition:o,visitedRefs:new Set(s),maxDepth:r,currentDepth:t+1});i&&(n.items=i)}return["allOf","anyOf","oneOf"].forEach(i=>{const u=n[i];Array.isArray(u)&&(n[i]=u.reduce((f,l)=>{const m=a({schema:l,definition:o,visitedRefs:new Set(s),maxDepth:r,currentDepth:t+1});return m&&f.push(m),f},[]))}),n}function w({pathParams:e,opParams:o,definition:s}){return[...e,...o].reduce((r,t)=>{if(!t)return r;if("$ref"in t&&t.$ref){const n=d(t.$ref,s);if(!n)return r;const c=p(t,n);return r.push({...c,schema:"schema"in c&&c.schema?a({schema:c.schema,definition:s}):void 0}),r}return r.push({...t,schema:"schema"in t&&t.schema?a({schema:t.schema,definition:s}):void 0}),r},[])}function $(e,o){const s={...e};if(e.example!==void 0&&(s.example=e.example),!e.examples)return s;const r={};for(const[t,n]of Object.entries(e.examples))if("$ref"in n){const c=d(n.$ref,o);c&&(r[t]=c)}else r[t]=n;return s.examples=r,s}function b(e,o){if(!e)return;const s="$ref"in e&&e.$ref?d(e.$ref,o):e;if(!s)return;const r=p(e,s),t={...r};if("content"in r&&r.content){const n={};for(const[c,i]of Object.entries(r.content))n[c]={...$(i,o),schema:i.schema?a({schema:i.schema,definition:o}):void 0};t.content=n}return t}function j(e,o){return Object.entries(e).reduce((s,[r,t])=>{const n="$ref"in t&&t.$ref?d(t.$ref,o):t;if(!n)return s;const c=p(t,n),i={...c};if("content"in c&&c.content){const u={};for(const[f,l]of Object.entries(c.content))u[f]={...$(l,o),schema:l.schema?a({schema:l.schema,definition:o}):void 0};i.content=u}return s[r]=i,s},{})}export{h as filterApiDescriptionsByName,v as findApiDescriptionByNameAndVersion,w as resolveParameters,d as resolveRef,b as resolveRequestBody,j as resolveResponses,a as resolveSchemaRefs};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{inputRequired as
|
|
2
|
-
`)}async function d({kv:e,subject:t,baseUrl:n,target:i,auth:a,hasExistingSecret:o}){const r=await
|
|
3
|
-
`)}export{
|
|
1
|
+
import{inputRequired as c,inputResponse as f}from"@modelcontextprotocol/server";import{MANAGE_API_CREDENTIALS_TOOL_NAME as T}from"../constants.js";import{secretStore as $,ELICITATION_TTL_MS as A}from"./secret-store.js";import{ANONYMOUS_SUBJECT_PREFIX as l}from"./subject.js";const h="credentials";function C(e){const t=f(e,h);return t.kind==="elicit"&&(t.action==="decline"||t.action==="cancel")}function _(e){return{api:e.api,name:e.name}}async function b(e){const{url:t}=await d(e);return c({inputRequests:{[h]:c.elicitUrl({message:P(e.target.name,e.hasExistingSecret),url:t})}})}async function O(e){const{url:t}=await d(e);return y(e,t,I(e.subject))}function R(e){return[e.status===401?`The stored credentials for "${e.name}" were rejected (HTTP 401).`:`The "${e.name}" API rejected the request with HTTP ${e.status}.`,"",`Do not retry the same call. Call \`${T}\` with api: "${e.api}" to get a link the user has to open in a browser to replace or remove the stored credentials, then retry once they confirm they saved them.`].join(`
|
|
2
|
+
`)}async function d({kv:e,subject:t,baseUrl:n,target:i,auth:a,hasExistingSecret:o}){const r=await $.createElicitation(e,{subject:t,api:i.api,name:i.name,auth:a,hasExistingSecret:o});return{token:r,url:g(n,r)}}function I(e){return e.startsWith(l)?e.slice(l.length):void 0}function P(e,t){const n=`The link works for ${u()} minutes.`;return t?`Open this page in a browser to replace or remove the stored credentials for the "${e}" API. They go straight to the API and never enter the chat. ${n}`:`The "${e}" API needs credentials. Open this page in a browser to paste them - they go straight to the API and never enter the chat. ${n}`}function u(){return Math.round(A/6e4)}function g(e,t){if(!e)return`/mcp/credentials?token=${t}`;try{const n=new URL("/mcp/credentials",e);return n.searchParams.set("token",t),n.toString()}catch{return`${e}/mcp/credentials?token=${t}`}}function y({target:e,reason:t,hasExistingSecret:n},i,a){const r=[`**Stop and show the user this link. They have to open it in a browser to ${n?`replace or remove the stored credentials for the "${e.name}" API`:`add credentials for the "${e.name}" API`} - nothing continues until they do.**`,"",i,"",`The link is valid ${u()} minutes and opens a form on this site. Credentials go straight to the API from there: never ask for them in the conversation and never pass them as a tool argument.`,"",t.kind==="authFailure"?`The upstream API "${e.name}" returned ${t.status} and needs credentials.`:n?`Credentials are already stored for the "${e.name}" API; the link lets the user replace or remove them.`:`The "${e.name}" API needs credentials.`,a?`Once the user confirms they saved them, call this tool again with sessionHandle: "${a}" - the credentials are stored against that handle, applied server-side, and never appear in the conversation.`:"Once the user confirms they saved them, call this tool again - the credentials are applied server-side and never appear in the conversation."],s=t.kind==="authFailure"?t.otherApiNames??[]:[];if(s.length>0){const p=s.map(m=>`"${m}"`).join(", ");r.push("",`The remaining APIs (${p}) will prompt for credentials on subsequent calls.`)}return r.join(`
|
|
3
|
+
`)}export{h as CREDENTIALS_INPUT_REQUEST_KEY,R as buildCredentialsToolHint,O as buildUpstreamCredentialFallback,b as buildUpstreamCredentialInputRequired,C as isCredentialElicitationDeclined,_ as targetOf};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GatewayFetchOutcome } from './fetch-telemetry.js';
|
|
2
|
+
type GatewayFetchAuditRecord = {
|
|
3
|
+
rawUrl: string;
|
|
4
|
+
method: string;
|
|
5
|
+
host: string;
|
|
6
|
+
outcome: GatewayFetchOutcome;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
injectedSecretValues: readonly string[];
|
|
9
|
+
path?: string;
|
|
10
|
+
api?: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
requestHeaders?: Record<string, string>;
|
|
13
|
+
requestBody?: string;
|
|
14
|
+
responseHeaders?: Record<string, string>;
|
|
15
|
+
responseBody?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* One outbound gateway fetch. `host`, `method` and `path` repeat the `mcpServer.gatewayFetch`
|
|
19
|
+
* telemetry event verbatim so a Grafana row joins onto this line; `url_sha256` identifies the
|
|
20
|
+
* request itself, since the telemetry event's `path` is deliberately normalized.
|
|
21
|
+
*/
|
|
22
|
+
export declare function logGatewayFetchAudit({ rawUrl, method, host, outcome, durationMs, injectedSecretValues, path, api, status, requestHeaders, requestBody, responseHeaders, responseBody, }: GatewayFetchAuditRecord): void;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=fetch-audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{auditHeaderNames as u,auditInput as g,auditOutputDigest as c,auditSha256 as b,auditUrl as l,logAuditRecord as y}from"../../audit/audit-log.js";function D({rawUrl:e,method:n,host:i,outcome:r,durationMs:o,injectedSecretValues:t,path:_,api:h,status:f,requestHeaders:p,requestBody:d,responseHeaders:m,responseBody:a}){const s=a===void 0?void 0:c(a);y("Record gateway fetch",{url_sha256:b(e),host:i,method:n,path:_,api:h,outcome:r,status:f,duration_ms:o,url:l(e,t),request_header_names:u(p),request_body:d===void 0?void 0:g(d,t),response_header_names:u(m),response_sha256:s?.sha256,response_bytes:s?.bytes})}export{D as logGatewayFetchAudit};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Agent as
|
|
1
|
+
import{Agent as W}from"undici";import{envConfig as I}from"../../../../config/env-config.js";import{logger as R}from"../../../../tools/notifiers/logger.js";import{assertUrlAllowed as B}from"./ssrf-guard.js";import{createSsrfSafeLookup as J}from"./ssrf-safe-lookup.js";import{SsrfBlockedError as C}from"./ssrf-blocklist.js";import{GatewayFetchBlockedError as $,GatewayFetchSystemError as K,GatewayFetchUserError as U}from"./fetch-errors.js";import{redactHeaders as x,redactSecretsFromString as V}from"./redact.js";import{normalizeFetchPath as X}from"./normalize-path.js";import{reportGatewayFetch as G}from"./fetch-telemetry.js";import{logGatewayFetchAudit as O}from"./fetch-audit.js";const N="x-forwarded-for",Y=3e4,z=1024*1024,Q=new Set([401,403,407]);let M;function Z(){if(!M){const t=I.isDevelopMode;t&&R.warn("Gateway MCP SSRF guard: loopback upstreams are ALLOWED (develop mode only, never in runtime)."),M=new W({connect:{lookup:J({allowLoopback:t})}})}return M}function be({allowedHosts:t,apis:e=[],authorization:o,onAuthRequired:s,fetchImpl:i=fetch,dispatcher:u=Z(),timeoutMs:d=Y,maxResponseBytes:h=z,maxFetchesPerExec:f,clientIp:m}){const k=[...t,...e.flatMap(r=>r.matchers)],L=[o,...e.map(r=>r.secret?.value)].filter(r=>!!r),v=e.map(r=>r.secret?.header).filter(r=>!!r);let H=0;return async r=>{const w=r.method??"GET",A=Date.now();let _,g,b,S={},T,D;try{if(f!==void 0){const l=Math.max(0,f);if(H+=1,H>l)throw new U(`Outbound fetch budget exceeded (max ${l} fetches per execute); split the work across calls`)}const n=B(r.url,k);_=n.hostname,g=X(n.pathname);const E=e.filter(l=>l.matchers.some(P=>P.test(n.hostname))),c=E.length===1?E[0]:void 0;b=c?.slug,S=se(r.headers,c?.secret,o,m);const p=new AbortController;D=setTimeout(()=>p.abort(),d);const a=await i(n,{method:w,headers:S,body:r.body,redirect:"manual",signal:p.signal,dispatcher:u});T=a.status,Q.has(a.status)&&c&&s&&s({api:c.slug,name:c.name,status:a.status});const F=await ae(a,h);let y=ce(a.headers);if(re(a.status)){y=x(y,v);const{location:l}=y;l&&!oe(l,n,k)&&delete y.location}return G({host:n.hostname,method:w,outcome:"success",durationMs:Date.now()-A,path:g,api:b,status:a.status,bytes:Buffer.byteLength(F,"utf8")}),O({rawUrl:n.href,method:w,host:n.hostname,outcome:"success",durationMs:Date.now()-A,injectedSecretValues:L,path:g,api:b,status:a.status,requestHeaders:S,requestBody:r.body,responseHeaders:y,responseBody:F}),{status:a.status,statusText:a.statusText,ok:a.ok,headers:y,bodyText:F}}catch(n){const{outcome:E,userSafe:c}=ee(n),p=_??(c instanceof $?c.host:void 0)??te(r.url)??"invalid-url";throw G({host:p,method:w,outcome:E,durationMs:Date.now()-A,path:g,api:b,status:T}),O({rawUrl:r.url,method:w,host:p,outcome:E,durationMs:Date.now()-A,injectedSecretValues:L,path:g,api:b,status:T,requestHeaders:S,requestBody:r.body}),c||(R.error(`Gateway MCP fetch failed [${w} ${p}]: ${V(ne(n),L)} headers=${JSON.stringify(x(S,v))}`),new K("Upstream fetch failed due to an internal error"))}finally{D!==void 0&&clearTimeout(D)}}}function q(t){let e=t;for(let o=0;o<5&&e instanceof Error;o++){if(e instanceof U||e instanceof C)return e;e=e.cause}}function ee(t){const e=q(t);return{outcome:e instanceof $||e instanceof C?"blocked":e?"user_error":"system_error",userSafe:e}}function te(t){try{return new URL(t).hostname||void 0}catch{return}}function re(t){return t>=300&&t<400}function oe(t,e,o){try{return!!B(new URL(t,e).href,o)}catch{return!1}}function ne(t){if(!(t instanceof Error))return String(t);const e=t.cause;return e instanceof Error?`${t.message}: ${e.message}`:t.message}async function ae(t,e){const o=Number(t.headers.get("content-length"));if(Number.isFinite(o)&&o>e)throw j(e,o);if(!t.body)return"";const s=t.body.getReader(),i=new TextDecoder;let u=0,d="";try{for(;;){const{done:h,value:f}=await s.read();if(h)break;if(u+=f.byteLength,u>e)throw j(e,u);d+=i.decode(f,{stream:!0})}return d+i.decode()}finally{await s.cancel().catch(()=>{})}}function j(t,e){return new U(`Response is too large (over ${t} bytes, got ${e}); request less data`)}function se(t,e,o,s){const i={},u=new Set;for(const[h,f]of Object.entries(t??{})){const m=h.toLowerCase();m==="host"||m==="content-length"||m!==N&&(u.add(m),i[h]=f)}s&&(i[N]=s);const d=e??(o?{header:"Authorization",value:o}:void 0);return d&&!u.has(d.header.toLowerCase())&&(i[d.header]=d.value),i}function ce(t){const e={};return t.forEach((o,s)=>{e[s.toLowerCase()]=o}),e}export{be as buildFetchBridge,Z as getDefaultAgent,ae as readCappedBody};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{secretStore as s}from"../gateway-mcp/secret-store.js";import{buildAllowedHostMatchers as
|
|
1
|
+
import{secretStore as s,ELICITATION_TTL_MS as x}from"../gateway-mcp/secret-store.js";import{buildAllowedHostMatchers as P}from"../gateway-mcp/fetch/allowed-hosts.js";import{assertUrlAllowed as _}from"../gateway-mcp/fetch/ssrf-guard.js";import{readCappedBody as E,getDefaultAgent as T}from"../gateway-mcp/fetch/fetch-bridge.js";const p={"Content-Type":"text/html; charset=utf-8","Cache-Control":"no-store","X-Frame-Options":"DENY","Referrer-Policy":"no-referrer","Content-Security-Policy":"default-src 'none'; style-src 'unsafe-inline'; form-action 'self'"},R=3e4,z=64*1024;let $=fetch;function Q(e){$=e}const F=async(e,t)=>{const n=await t.getKv();if(e.method==="GET"){const o=new URL(e.url).searchParams.get("token")??"",r=await s.peekElicitation(n,o);return r?new Response(j(r,o),{status:200,headers:p}):u()}if(e.method==="POST"){const o=await e.formData(),r=String(o.get("token")??""),m=String(o.get("mode")??"token");return m==="client-credentials"?M(n,o,r):m==="clear"?B(n,r):O(n,o,r)}return new Response("Method Not Allowed",{status:405,headers:{Allow:"GET, POST"}})};async function O(e,t,n){const o=String(t.get("secret")??"");if(!o)return u();const r=await s.consumeElicitation(e,n);return r?(await s.setSecret(e,r.subject,r.api,H(r.auth,o)),new Response(A(r.name),{status:200,headers:p})):u()}async function B(e,t){const n=await s.consumeElicitation(e,t);return n?(await s.deleteSecret(e,n.subject,n.api),new Response(D(n.name),{status:200,headers:p})):u()}async function M(e,t,n){const o=String(t.get("client_id")??"").trim(),r=String(t.get("client_secret")??"").trim();if(!o||!r)return u();const m=await s.peekElicitation(e,n);if(!m||m.auth.kind!=="oauth2ClientCredentials")return u();const i=await s.consumeElicitation(e,n);if(!i||i.auth.kind!=="oauth2ClientCredentials")return u();const b=i.auth;try{const S=P([{url:b.tokenUrl}]),C=_(b.tokenUrl,S),k=new AbortController,I=setTimeout(()=>k.abort(),R);let g;try{g=await $(C,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:o,client_secret:r,...b.scopes.length>0?{scope:b.scopes.join(" ")}:{}}),redirect:"manual",signal:k.signal,dispatcher:T()})}finally{clearTimeout(I)}if(!g.ok)return f(i.name);let v;try{v=await E(g,z)}catch{return f(i.name)}let h;try{h=JSON.parse(v)}catch{return f(i.name)}const w=typeof h=="object"&&h!==null&&"access_token"in h?h.access_token:void 0;return typeof w!="string"||!w?f(i.name):(await s.setSecret(e,i.subject,i.api,{header:"Authorization",value:`Bearer ${w}`}),new Response(A(i.name),{status:200,headers:p}))}catch{return f(i.name)}}function H(e,t){return e.kind==="apiKey"?{header:e.header,value:t}:e.kind==="bearer"||e.kind==="oauth2ClientCredentials"?{header:"Authorization",value:N(t)}:{header:"Authorization",value:t}}function N(e){const t=e.trim();return t.toLowerCase().startsWith("bearer")?t:`Bearer ${t}`}function a(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""")}function c(e){return`<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
@@ -37,7 +37,7 @@ footer{border-top-color:#1f2937;color:#6b7280}
|
|
|
37
37
|
</style>
|
|
38
38
|
</head>
|
|
39
39
|
<body><div class="card">${e}</div></body>
|
|
40
|
-
</html>`}function d(e){return`<h1>\u{1F512} ${a(e)}</h1>`}function l(){return"<footer>Secured by Redocly · credentials never enter the AI conversation</footer>"}function
|
|
40
|
+
</html>`}function d(e){return`<h1>\u{1F512} ${a(e)}</h1>`}function l(){return"<footer>Secured by Redocly · credentials never enter the AI conversation</footer>"}function y(){return`<p class="hint">This link expires ${Math.round(x/6e4)} minutes after it was issued. If it stops working, ask your AI client for a new one.</p>`}function j(e,t){const n=e.hasExistingSecret?q(t):"";switch(e.auth.kind){case"bearer":return L(e.name,t,n);case"apiKey":return U(e.name,t,e.auth.header,n);case"oauth2ClientCredentials":return G(e.name,t,e.auth.scopes,n);default:return K(e.name,t,n)}}function q(e){return`
|
|
41
41
|
<div class="section">
|
|
42
42
|
<h2>Remove stored credentials</h2>
|
|
43
43
|
<p class="hint">This API already has credentials stored. Save above to replace them, or remove them to make calls anonymous again.</p>
|
|
@@ -46,7 +46,7 @@ footer{border-top-color:#1f2937;color:#6b7280}
|
|
|
46
46
|
<input type="hidden" name="mode" value="clear">
|
|
47
47
|
<button class="danger" type="submit">Remove credentials</button>
|
|
48
48
|
</form>
|
|
49
|
-
</div>`}function
|
|
49
|
+
</div>`}function L(e,t,n){return c(`
|
|
50
50
|
${d(`Credentials for ${e}`)}
|
|
51
51
|
<p class="hint">Stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
|
|
52
52
|
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
@@ -58,6 +58,7 @@ ${d(`Credentials for ${e}`)}
|
|
|
58
58
|
<button type="submit">Save credentials</button>
|
|
59
59
|
</form>
|
|
60
60
|
${n}
|
|
61
|
+
${y()}
|
|
61
62
|
${l()}`)}function U(e,t,n,o){return c(`
|
|
62
63
|
${d(`Credentials for ${e}`)}
|
|
63
64
|
<p class="hint">Stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and sent to the API as the <code>${a(n)}</code> header. It never passes through the AI conversation.</p>
|
|
@@ -69,7 +70,8 @@ ${d(`Credentials for ${e}`)}
|
|
|
69
70
|
<button type="submit">Save credentials</button>
|
|
70
71
|
</form>
|
|
71
72
|
${o}
|
|
72
|
-
${
|
|
73
|
+
${y()}
|
|
74
|
+
${l()}`)}function G(e,t,n,o){const r=n.length>0?n.map(a).join(", "):"default scopes";return c(`
|
|
73
75
|
${d(`Credentials for ${e}`)}
|
|
74
76
|
<p class="hint">Paste a token you already have, or let us exchange client credentials for one via OAuth2 client_credentials.</p>
|
|
75
77
|
<div class="section">
|
|
@@ -97,7 +99,8 @@ ${d(`Credentials for ${e}`)}
|
|
|
97
99
|
</form>
|
|
98
100
|
</div>
|
|
99
101
|
${o}
|
|
100
|
-
${
|
|
102
|
+
${y()}
|
|
103
|
+
${l()}`)}function K(e,t,n){return c(`
|
|
101
104
|
${d(`Credentials for ${e}`)}
|
|
102
105
|
<p class="hint">The value is stored encrypted for you only (24 hours for anonymous callers, 30 days when signed in) and is sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
|
|
103
106
|
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
@@ -108,13 +111,14 @@ ${d(`Credentials for ${e}`)}
|
|
|
108
111
|
<button type="submit">Save credentials</button>
|
|
109
112
|
</form>
|
|
110
113
|
${n}
|
|
114
|
+
${y()}
|
|
111
115
|
${l()}`)}function D(e){return c(`
|
|
112
116
|
${d("Credentials removed")}
|
|
113
117
|
<p>The stored credentials for ${a(e)} are gone. Calls to that API are anonymous again until you add new ones.</p>
|
|
114
|
-
${l()}`)}function
|
|
118
|
+
${l()}`)}function A(e){return c(`
|
|
115
119
|
${d("Credentials saved")}
|
|
116
|
-
<p>Credentials for ${a(e)} are
|
|
120
|
+
<p>Credentials for ${a(e)} are saved. Go back to your AI client and ask it to retry - or confirm the prompt it left open, if there is one. You can close this tab.</p>
|
|
117
121
|
${l()}`)}function f(e){return new Response(c(`
|
|
118
122
|
${d("Exchange failed")}
|
|
119
123
|
<p>We could not exchange your client credentials for a token for ${a(e)}. This link has been used up - go back to your AI client and request a new link, then try again.</p>
|
|
120
|
-
${l()}`),{status:502,headers:p})}function u(){return new Response(c(`${d("Link expired")}<p>This credentials link is invalid or
|
|
124
|
+
${l()}`),{status:502,headers:p})}function u(){return new Response(c(`${d("Link expired")}<p>This credentials link is invalid, already used, or older than ${Math.round(x/6e4)} minutes. Retry the request in your AI client to get a fresh link.</p>${l()}`),{status:404,headers:p})}var V=F;export{V as default,Q as setCredentialsFetchImpl};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fromJsonSchema as
|
|
1
|
+
import{fromJsonSchema as A}from"@modelcontextprotocol/server";import{logger as b}from"../../../tools/notifiers/logger.js";import{mcpToolWorkers as g,MCP_TOOL_IS_AVAILABLE_KEY as M}from"../../../workers/mcp-tool-worker-pool.js";import{CREATE_ANONYMOUS_SESSION_TOOL_NAME as L}from"../constants.js";import{isAnonymousSessionToolAvailable as T}from"../docs-mcp/tools/core/create-anonymous-session.js";import{isManageApiCredentialsToolAvailable as v,manageApiCredentials as D,manageApiCredentialsSchema as y}from"../codemode/tools/manage-credentials.js";import{EXECUTE_TOOL_SCHEMA as I}from"../codemode/tools/execute-schema.js";import{EXECUTE_TOOL_NAME as O,DESCRIBE_TOOLS_TOOL_NAME as N,CODE_MODE_TOOL_NAMES as H,FETCH_TOOL_NAME as k}from"../codemode/constants.js";import{buildCodeModeInstructions as w,buildExecuteDescription as x,DESCRIBE_TOOLS_DESCRIPTION as F}from"../codemode/prompts.js";import{toToolListEntries as R}from"../codemode/build-tool-list.js";import{composeExecuteAnnotations as P,getActiveCapabilities as B}from"../codemode/capabilities/index.js";import{describeToolsSchema as U,describeTools as z}from"../codemode/tools/describe-tools.js";import{mcpJsonSchemaValidator as J}from"../utils/json-schema-validator.js";import{connectMcpServer as V,createMcpServer as X,createToolDispatch as Y,extractSerializableContext as $}from"./mcp-server.js";function K(r){const t=I.schema;if(T(r))return t;const{sessionHandle:i,...m}=t.properties;return{...t,properties:m}}function W(r,t){const i=t.schema;if(T(r))return i;const{sessionHandle:m,...p}=i.properties;return{...i,properties:p}}async function me({name:r,tools:t,context:i,skills:m=[],getKv:p}){const s=i.mcpMode==="code",a=s?B(i):[],E=a.some(e=>e.id===k),l=s?t.filter(e=>!H.has(e.name)):[],u=R(l,a),_=s&&v(i),{server:n,transport:C}=X(r,s?w(E,u):void 0),c=e=>A(e,J),d=$(i),S={getClientCapabilities:()=>n.server.getClientCapabilities(),getKv:p},f=Y(d,S),h=async e=>{try{return await g.exec(M,[{toolName:e,context:d}],{timeout:1e4})}catch(o){return b.error(`Failed to check MCP tool availability for "${e}": ${o instanceof Error?o.message:String(o)}`),!1}};if(s){n.registerTool(O,{description:x(E,u),inputSchema:c(K(i)),annotations:P(a)},f(O)),n.registerTool(N,{description:F,inputSchema:c(U(l,a))},z(l,h,a,i));const e=t.find(o=>o.name===L);if(e&&T(i)&&n.registerTool(e.name,{description:e.description,inputSchema:c(e.schema),annotations:e.annotations??{title:e.name}},f(e.name)),_){const o=y(i);n.registerTool(o.name,{description:o.description,inputSchema:c(W(i,o)),annotations:o.annotations??{title:o.name}},D(d,S))}}else await Promise.all(t.map(async e=>{const o=n.registerTool(e.name,{description:e.description,inputSchema:c(e.schema),annotations:e.annotations||{title:e.name}},f(e.name));await h(e.name)||o.disable()}));for(const e of m)n.registerResource(e.slug,e.uri,{title:e.name,description:e.description,mimeType:"text/markdown"},async()=>({contents:[{uri:e.uri,mimeType:"text/markdown",text:e.content}]}));return V(n,C)}export{me as createDocsMcpServer};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{McpServer as T,WebStandardStreamableHTTPServerTransport as q}from"@modelcontextprotocol/server";import{logger as
|
|
1
|
+
import{McpServer as T,WebStandardStreamableHTTPServerTransport as q}from"@modelcontextprotocol/server";import{logger as g}from"../../../tools/notifiers/logger.js";import{mcpToolWorkers as v,MCP_TOOL_WORKER_KEY as I}from"../../../workers/mcp-tool-worker-pool.js";import{SSE_KEEPALIVE_INTERVAL_MS as R}from"../constants.js";import{secretStore as _}from"../gateway-mcp/secret-store.js";import{readAuthRequiredMeta as A,stripAuthRequiredMeta as h}from"../gateway-mcp/upstream-auth.js";import{buildCredentialsToolHint as O,buildUpstreamCredentialFallback as w,buildUpstreamCredentialInputRequired as U,isCredentialElicitationDeclined as C,targetOf as D}from"../gateway-mcp/elicitation.js";import{resolveUpstreamAuthScheme as j}from"../gateway-mcp/upstream-auth-scheme.js";import{MISSING_SESSION_HANDLE_MESSAGE as K,resolveSecretSubject as P}from"../gateway-mcp/subject.js";const L=6e4,S=new Map;function $(e,t){const r=S.get(e);if(r)return r;const n=t().finally(()=>{S.delete(e)});return S.set(e,n),n}function ee(e,t){const r=new T({name:e,version:new Date().toISOString().slice(0,10)},{capabilities:{logging:{}},...t?{instructions:t}:{}}),n=new q({keepAliveMs:R,sessionIdGenerator:void 0});return{server:r,transport:n}}async function te(e,t){return await e.connect(t),{server:e,transport:t,cleanup:async()=>{t.close()}}}function re(e,t){return r=>async(n,a)=>{g.info(`MCP tool called: ${r}`);const i=e.elicitationEnabled??!1,s=a.mcpReq.envelope!==void 0,c=i?P(e.user,n.sessionHandle):{kind:"anonymous"},o=c.kind==="resolved"?c.subject:void 0;if(c.kind==="invalid")return{content:[{type:"text",text:c.message}],isError:!0};const d=async()=>{if(!(!i||!o||!t?.getKv))try{const u=await t.getKv();return await _.getSecretsForSubject(u,o)}catch(u){g.warn(`gateway mcp secret store error, continuing unauthenticated: ${String(u)}`);return}};let m;const E=async()=>{m=await d();const u={toolName:r,args:n,context:{...e,upstreamSecrets:m},extra:N(a)};return await v.exec(I,[u],{timeout:L})};let p=await E();const l=A(p);if(l.length>0&&i&&!o)return h({...p,content:[{type:"text",text:K}],isError:!0});if(l.length>0&&i&&o&&t?.getKv){g.info(`gateway mcp credentials prompt: era=${s?"2026-07-28":"2025"} clientElicitation=${F(t)}`);const u=await t.getKv(),b=new Set(Object.keys(m??{})),f=s?await H(e,a,l,o,u,b,t):await B(e,l,o,u,b);switch(f.kind){case"inputRequired":return f.result;case"retry":p=await E();break;case"prependText":p={...p,content:[{type:"text",text:f.text},...p.content],isError:!0};break;case"none":break}}return h(p)}}function x(e){return!!e?.getClientCapabilities()?.elicitation?.url}function F(e){const t=e?.getClientCapabilities()?.elicitation;if(!t)return"none";const r=[t.form?"form":void 0,t.url?"url":void 0].filter(Boolean);return r.length>0?r.join("+"):"none"}function k(e,t){return e.status===401&&!t.has(e.api)}function y(e){return e.map(O).join(`
|
|
2
2
|
|
|
3
|
-
`)}async function M(e,t,r,n){const[a,...i]=t;return{subject:r,baseUrl:e.baseUrl??"",target:
|
|
3
|
+
`)}async function M(e,t,r,n){const[a,...i]=t;return{subject:r,baseUrl:e.baseUrl??"",target:D(a),auth:await j(e,a.api),kv:n,hasExistingSecret:!1,reason:{kind:"authFailure",status:a.status,otherApiNames:i.map(s=>s.name)}}}async function H(e,t,r,n,a,i,s){if(C(t.mcpReq.inputResponses))return{kind:"none"};const c=r.filter(d=>k(d,i));if(c.length===0)return{kind:"prependText",text:y(r)};const o=await M(e,c,n,a);return x(s)?{kind:"inputRequired",result:await U(o)}:{kind:"prependText",text:await w(o)}}async function B(e,t,r,n,a){const i=t.filter(o=>k(o,a));if(i.length===0)return{kind:"prependText",text:y(t)};const s=await M(e,i,r,n);return{kind:"prependText",text:await $(`${r}:${i[0].api}`,()=>w(s))}}function N(e){return{sessionId:e.sessionId,authInfo:e.http?.authInfo,requestId:e.mcpReq.id,_meta:e.mcpReq._meta}}function ne(e){return{user:e.user,config:e.config,outdir:e.outdir,baseUrl:e.baseUrl,params:e.params,query:e.query,cookies:e.cookies,apiDescriptionsMap:e.apiDescriptionsMap,graphqlDescriptions:e.graphqlDescriptions,products:e.products,accessToken:e.accessToken,metadata:e.metadata,upstreamAuthorization:e.upstreamAuthorization,mcpMode:e.mcpMode,elicitationEnabled:e.elicitationEnabled,gatewayFetchBudgetPerExec:e.gatewayFetchBudgetPerExec,clientIp:e.clientIp,upstreamSecrets:e.upstreamSecrets}}export{te as connectMcpServer,ee as createMcpServer,re as createToolDispatch,ne as extractSerializableContext};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{telemetry as
|
|
1
|
+
import{telemetry as h}from"../../../telemetry/index.js";import{mcpToolHandlers as y}from"../../../../client/mcp-tool-handlers-entry.js";import{McpServerType as c}from"../constants.js";import{EXECUTE_TOOL_NAME as T}from"../codemode/constants.js";import{logMcpToolCallAudit as M,reportMcpToolCalled as C,reportMcpToolError as u}from"./mcp-tool-telemetry.js";function E(e){return e.content.map(o=>o.type==="text"?o.text:"")}async function j(e){h.initialize();const{toolName:o,args:a,context:l,extra:r}=e,n=l?.mcpMode??"tools",s=performance.now();try{const t=y[o];if(!t)throw new Error(`Unknown MCP tool: ${o}`);const{default:x}=await t(),i=x[o];if(!i)throw new Error(`MCP tool module does not export a handler for "${o}". Expected \`export default { '${o}': handler }\`.`);const f=await(typeof i=="function"?i:i.execute)(a,l,r);if(!v(f))throw new Error(`MCP tool "${o}" returned an invalid result.`);const m=Math.round(performance.now()-s),p=f.isError?o===T?"sandbox_error":"tool_error":void 0,d=E(f),w=d.join("");return M({tool:o,mode:n,serverType:c.Docs,args:a,result:w,errorClass:p}),p?u({tool:o,message:d.join(" "),mode:n,serverType:c.Docs,durationMs:m,errorClass:p}):C({tool:o,mode:n,serverType:c.Docs,durationMs:m,outputLength:w.length}),f}catch(t){throw M({tool:o,mode:n,serverType:c.Docs,args:a,errorClass:"server_error"}),u({tool:o,message:t instanceof Error?t.message:String(t),mode:n,serverType:c.Docs,durationMs:Math.round(performance.now()-s),stack:t instanceof Error&&t.stack||"",errorClass:"server_error"}),t}}function v(e){return!e||typeof e!="object"?!1:Array.isArray(e.content)}async function P(e){h.initialize();const{toolName:o,context:a}=e,l=performance.now();try{const r=y[o];if(!r)return!1;const{default:n}=await r(),s=n[o];return s?typeof s=="object"&&typeof s.isAvailable=="function"?await s.isAvailable(a):!0:!1}catch(r){throw u({tool:o,message:"Failed to check if MCP tool is available: "+(r instanceof Error?r.message:String(r)),mode:a?.mcpMode??"tools",serverType:c.Docs,durationMs:Math.round(performance.now()-l),stack:r instanceof Error&&r.stack||"",errorClass:"server_error"}),r}}export{j as executeMcpTool,P as isMcpToolAvailable};
|
|
@@ -15,6 +15,17 @@ export type McpErrorClass = 'tool_error' | 'sandbox_error' | 'server_error';
|
|
|
15
15
|
export declare function reportMcpToolCalled({ tool, mode, serverType, durationMs, outputLength, }: McpToolEvent & {
|
|
16
16
|
outputLength: number;
|
|
17
17
|
}): void;
|
|
18
|
+
/**
|
|
19
|
+
* Arguments and result identity for one MCP tool call. `tool` and `mode` repeat the
|
|
20
|
+
* `mcpServer.toolCalled` telemetry event so a Grafana row joins onto this line; `args_sha256`
|
|
21
|
+
* identifies the invocation. The result body itself is never recorded — a tool can return
|
|
22
|
+
* credentials no pattern will recognise — so only its digest lands here.
|
|
23
|
+
*/
|
|
24
|
+
export declare function logMcpToolCallAudit({ tool, mode, serverType, args, result, errorClass, }: Omit<McpToolEvent, 'durationMs'> & {
|
|
25
|
+
args: unknown;
|
|
26
|
+
result?: string;
|
|
27
|
+
errorClass?: McpErrorClass;
|
|
28
|
+
}): void;
|
|
18
29
|
export declare function reportMcpToolError({ tool, mode, serverType, durationMs, message, stack, errorClass, }: McpToolEvent & {
|
|
19
30
|
message: string;
|
|
20
31
|
stack?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{telemetry as
|
|
1
|
+
import{telemetry as u}from"../../../telemetry/index.js";import{serverAttributes as l}from"../utils/telemetry-attributes.js";import{EXECUTE_TOOL_NAME as c}from"../codemode/constants.js";import{auditInput as d,auditOutputDigest as p,auditSha256 as f,logAuditRecord as g}from"../audit/audit-log.js";function E({tool:r,mode:e,serverType:o,durationMs:s,outputLength:t}){u.sendMcpToolCalledMessage([{...l(o),tool:r,mode:e,output_length:t,duration_ms:s}])}function T({tool:r,mode:e,serverType:o,args:s,result:t,errorClass:n}){if(r===c)return;const i=m(s),a=t===void 0?void 0:p(t);g("Record MCP tool call",{tool:r,mode:e,server_type:o,error_class:n,args_sha256:f(i),args:d(i),result_sha256:a?.sha256,result_bytes:a?.bytes})}function m(r){try{return JSON.stringify(r)??String(r)}catch{return"\xAB[unserializable]\xBB"}}function A({tool:r,mode:e,serverType:o,durationMs:s,message:t,stack:n="",errorClass:i}){u.sendMcpErrorMessage([{...l(o),tool:r,message:t,stack:n,error_class:i,mode:e,duration_ms:s}])}export{T as logMcpToolCallAudit,E as reportMcpToolCalled,A as reportMcpToolError};
|