@redocly/realm 0.136.0-next.4 → 0.136.0-next.5
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 +26 -0
- package/dist/client/ErrorBoundary.js +1 -1
- package/dist/client/app/l10n/hooks/useTranslate.js +1 -1
- package/dist/client/app/l10n/initL10n.js +1 -1
- package/dist/client/templates/openapi-docs/helpers.d.ts +1 -1
- package/dist/client/templates/openapi-docs/helpers.js +2 -2
- package/dist/constants/common.d.ts +3 -0
- package/dist/constants/common.js +1 -1
- package/dist/server/config/env-config.d.ts +2 -4
- package/dist/server/config/env-schema.d.ts +5 -10
- package/dist/server/config/env-schemas/feature-flags.d.ts +4 -8
- package/dist/server/config/env-schemas/feature-flags.js +1 -1
- package/dist/server/constants/common.d.ts +2 -0
- package/dist/server/constants/common.js +1 -1
- package/dist/server/constants/entitlements.js +1 -1
- package/dist/server/plugins/config-parser/loaders/utils/read-and-validate-config.js +1 -1
- package/dist/server/plugins/markdown/search/create-render-tag-fn.d.ts +2 -1
- package/dist/server/plugins/markdown/search/create-render-tag-fn.js +1 -1
- package/dist/server/plugins/markdown/search/get-ai-search-documents.js +9 -9
- package/dist/server/plugins/mcp/codemode/tools/execute.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/elicitation.d.ts +25 -0
- package/dist/server/plugins/mcp/gateway-mcp/elicitation.js +2 -0
- package/dist/server/plugins/mcp/gateway-mcp/eligibility.d.ts +4 -15
- package/dist/server/plugins/mcp/gateway-mcp/eligibility.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/allowed-hosts.d.ts +8 -8
- package/dist/server/plugins/mcp/gateway-mcp/fetch/allowed-hosts.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/fetch-bridge.d.ts +15 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/fetch-bridge.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-blocklist.d.ts +10 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-blocklist.js +1 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-guard.d.ts +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-guard.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-safe-lookup.d.ts +11 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch/ssrf-safe-lookup.js +1 -0
- package/dist/server/plugins/mcp/gateway-mcp/fetch-capability.d.ts +8 -1
- package/dist/server/plugins/mcp/gateway-mcp/fetch-capability.js +1 -1
- package/dist/server/plugins/mcp/gateway-mcp/secret-store.d.ts +24 -0
- package/dist/server/plugins/mcp/gateway-mcp/secret-store.js +1 -0
- package/dist/server/plugins/mcp/gateway-mcp/upstream-auth-scheme.d.ts +18 -0
- package/dist/server/plugins/mcp/gateway-mcp/upstream-auth-scheme.js +1 -0
- package/dist/server/plugins/mcp/gateway-mcp/upstream-auth.d.ts +10 -0
- package/dist/server/plugins/mcp/gateway-mcp/upstream-auth.js +1 -0
- package/dist/server/plugins/mcp/handlers/docs-mcp-handler.js +1 -1
- package/dist/server/plugins/mcp/handlers/errors.d.ts +3 -3
- package/dist/server/plugins/mcp/handlers/errors.js +1 -1
- package/dist/server/plugins/mcp/handlers/handle-mcp-request.js +1 -1
- package/dist/server/plugins/mcp/handlers/mcp-credentials-handler.d.ts +5 -0
- package/dist/server/plugins/mcp/handlers/mcp-credentials-handler.js +101 -0
- package/dist/server/plugins/mcp/index.d.ts +1 -0
- package/dist/server/plugins/mcp/index.js +1 -1
- package/dist/server/plugins/mcp/servers/docs-server.d.ts +3 -2
- package/dist/server/plugins/mcp/servers/docs-server.js +1 -1
- package/dist/server/plugins/mcp/servers/mcp-server.d.ts +11 -2
- package/dist/server/plugins/mcp/servers/mcp-server.js +1 -1
- package/dist/server/plugins/mcp/servers/session-manager.d.ts +15 -0
- package/dist/server/plugins/mcp/servers/session-manager.js +1 -0
- package/dist/server/plugins/mcp/types.d.ts +6 -12
- package/dist/server/plugins/mcp/utils/jwt.d.ts +0 -9
- package/dist/server/plugins/mcp/utils/jwt.js +1 -1
- package/dist/server/plugins/mcp/utils/mode.d.ts +12 -2
- package/dist/server/plugins/mcp/utils/mode.js +1 -1
- package/dist/server/plugins/mcp/workers/execute-mcp-tool.js +1 -1
- package/dist/server/plugins/openapi-docs/search/get-ai-search-documents.js +16 -16
- package/dist/server/plugins/sso/index.js +1 -1
- package/dist/server/sandbox/types.d.ts +3 -2
- package/dist/server/sandbox/utils/execution-budget.d.ts +4 -3
- package/dist/server/sandbox/utils/execution-budget.js +1 -1
- package/dist/server/types/plugins/markdown.d.ts +2 -2
- package/dist/server/utils/crypto/constants.d.ts +5 -0
- package/dist/server/utils/crypto/constants.js +1 -0
- package/dist/server/utils/crypto/decrypt-aes-gcm.d.ts +6 -0
- package/dist/server/utils/crypto/decrypt-aes-gcm.js +1 -0
- package/dist/server/utils/crypto/decrypt-claim-value.d.ts +6 -0
- package/dist/server/utils/crypto/decrypt-claim-value.js +1 -0
- package/dist/server/utils/crypto/derive-key.d.ts +5 -0
- package/dist/server/utils/crypto/derive-key.js +1 -0
- package/dist/server/utils/crypto/encrypt-aes-gcm.d.ts +6 -0
- package/dist/server/utils/crypto/encrypt-aes-gcm.js +1 -0
- package/dist/server/utils/crypto/encrypt-claim-value.d.ts +6 -0
- package/dist/server/utils/crypto/encrypt-claim-value.js +1 -0
- package/dist/server/utils/l10n/get-build-translate-context.d.ts +7 -0
- package/dist/server/utils/l10n/get-build-translate-context.js +1 -0
- package/dist/server/utils/rbac.js +1 -1
- package/dist/server/web-server/auth.d.ts +33 -3
- package/dist/server/web-server/auth.js +3 -3
- package/dist/server/web-server/handle-api-route-request.js +1 -1
- package/dist/server/web-server/routes/api-routes/api-routes.js +1 -1
- package/dist/server/web-server/routes/auth.js +1 -1
- package/dist/server/web-server/routes/mcp-routes/mcp-oauth.d.ts +5 -2
- package/dist/server/web-server/routes/mcp-routes/mcp-oauth.js +1 -1
- package/dist/server/web-server/routes/mcp-routes/mcp-routes.js +1 -1
- package/dist/types/entitlements.d.ts +3 -1
- package/dist/utils/l10n/build-i18n-config.d.ts +24 -0
- package/dist/utils/l10n/build-i18n-config.js +1 -0
- package/dist/utils/l10n/create-translate.d.ts +19 -0
- package/dist/utils/l10n/create-translate.js +1 -0
- package/package.json +10 -9
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type LookupFunction } from 'node:net';
|
|
2
|
+
/**
|
|
3
|
+
* Returns a drop-in replacement for `dns.lookup` that errors with
|
|
4
|
+
* {@link SsrfBlockedError} if any resolved address is in the blocklist.
|
|
5
|
+
*
|
|
6
|
+
* Install on an http/https agent (`connect.lookup` for undici, or `lookup` for a node:https Agent)
|
|
7
|
+
* so that every socket connection — including those made while following redirects — is re-validated
|
|
8
|
+
* against the blocklist at connect time.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createSsrfSafeLookup(): LookupFunction;
|
|
11
|
+
//# sourceMappingURL=ssrf-safe-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{lookup as $}from"node:dns";import{isIP as v}from"node:net";import{isBlockedIp as l,SsrfBlockedError as n}from"./ssrf-blocklist.js";function S(){return(r,p,o)=>{const f=p,s=t=>{if(f.all){o(null,t);return}const[e]=t;if(!e){o(new n(`Hostname "${r}" did not resolve`),"",0);return}o(null,e.address,e.family)},u=v(r);if(u!==0){if(l(r)){o(new n(`Hostname "${r}" is a blocked address`),"",0);return}s([{address:r,family:u}]);return}$(r,{...f,all:!0},(t,e)=>{if(t){o(t,"",0);return}const d=e.filter(({address:i})=>l(i));if(d.length>0){const i=d.map(({address:m})=>m).join(", ");o(new n(`Hostname "${r}" resolves to a blocked address: ${i}`),"",0);return}if(!e.length){o(new n(`Hostname "${r}" did not resolve`),"",0);return}s(e)})}}export{S as createSsrfSafeLookup};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { ApiDescriptionInfo, McpToolContext } from '../types.js';
|
|
2
2
|
import type { SandboxFetch } from '../codemode/types.js';
|
|
3
|
+
import type { UpstreamAuthRequiredEvent } from './upstream-auth.js';
|
|
3
4
|
export declare function getCallableApis(context: McpToolContext): ApiDescriptionInfo[];
|
|
5
|
+
/**
|
|
6
|
+
* Fetch capability at request/worker time is derived purely from the build-time stamps:
|
|
7
|
+
* `fetchEligible` is stamped per API by the lifecycle plugin under `isGatewayCapabilityEnabled`
|
|
8
|
+
* (entitlement + hide checks), and the map arriving here is already RBAC-filtered.
|
|
9
|
+
* No EntitlementsProvider read — this code runs in graphs where the provider is uninitialized.
|
|
10
|
+
*/
|
|
4
11
|
export declare function isFetchCapable(context: McpToolContext): boolean;
|
|
5
|
-
export declare function buildExecuteHostFetch(context: McpToolContext): SandboxFetch;
|
|
12
|
+
export declare function buildExecuteHostFetch(context: McpToolContext, onAuthRequired?: (event: UpstreamAuthRequiredEvent) => void): SandboxFetch;
|
|
6
13
|
//# sourceMappingURL=fetch-capability.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{buildAllowedHostMatchers as i}from"./fetch/allowed-hosts.js";import{buildFetchBridge as l}from"./fetch/fetch-bridge.js";function a(e){return Object.values(e.apiDescriptionsMap??{}).filter(r=>r.fetchEligible)}function n(e){return a(e).length>0}function c(e,r){const s=a(e).map(t=>({slug:t.relativePath,name:t.name,matchers:i(t.servers??[]),secret:e.upstreamSecrets?.[t.relativePath]}));return l({allowedHosts:i([]),apis:s,authorization:e.upstreamAuthorization,onAuthRequired:r})}export{c as buildExecuteHostFetch,a as getCallableApis,n as isFetchCapable};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UpstreamAuthScheme } from './upstream-auth-scheme.js';
|
|
2
|
+
export declare const ELICITATION_TTL_MS: number;
|
|
3
|
+
export type StoredSecret = {
|
|
4
|
+
header: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
export type ElicitationTarget = {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
api: string;
|
|
10
|
+
name: string;
|
|
11
|
+
auth: UpstreamAuthScheme;
|
|
12
|
+
};
|
|
13
|
+
export declare class SecretStore {
|
|
14
|
+
#private;
|
|
15
|
+
constructor();
|
|
16
|
+
setSecret(sessionId: string, api: string, secret: StoredSecret): void;
|
|
17
|
+
getSecretsForSession(sessionId: string): Record<string, StoredSecret>;
|
|
18
|
+
deleteSession(sessionId: string): void;
|
|
19
|
+
createElicitation(target: ElicitationTarget): string;
|
|
20
|
+
peekElicitation(token: string): ElicitationTarget | undefined;
|
|
21
|
+
consumeElicitation(token: string): ElicitationTarget | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare const secretStore: SecretStore;
|
|
24
|
+
//# sourceMappingURL=secret-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createCipheriv as f,createDecipheriv as u,createHash as d,hkdfSync as l,randomBytes as p,randomUUID as y}from"node:crypto";const A=14400*1e3,g=900*1e3,S=60*1e3,x="mcp-upstream-secrets";class m{#e=new Map;#t=new Map;#n=p(32);constructor(){setInterval(()=>this.#r(),S).unref?.()}setSecret(e,t,i){const s=this.#s(e),r=p(12),o=f("aes-256-gcm",s,r),c=Buffer.concat([o.update(JSON.stringify(i),"utf8"),o.final()]),n=this.#e.get(this.#i(e))??{byApi:new Map};n.byApi.set(t,{iv:r,tag:o.getAuthTag(),data:c,expiresAt:Date.now()+A}),this.#e.set(this.#i(e),n)}getSecretsForSession(e){const t=this.#i(e),i=this.#e.get(t);if(!i)return{};const s=Date.now(),r=this.#s(e),o={};for(const[c,n]of i.byApi){if(n.expiresAt<=s){i.byApi.delete(c);continue}try{const a=u("aes-256-gcm",r,n.iv);a.setAuthTag(n.tag);const h=Buffer.concat([a.update(n.data),a.final()]).toString("utf8");o[c]=JSON.parse(h)}catch{i.byApi.delete(c)}}return i.byApi.size===0&&this.#e.delete(t),o}deleteSession(e){this.#e.delete(this.#i(e))}createElicitation(e){const t=y();return this.#t.set(t,{...e,expiresAt:Date.now()+g}),t}peekElicitation(e){const t=this.#t.get(e);if(!t)return;if(t.expiresAt<=Date.now()){this.#t.delete(e);return}const{expiresAt:i,...s}=t;return s}consumeElicitation(e){const t=this.peekElicitation(e);return t&&this.#t.delete(e),t}#r(){const e=Date.now();for(const[t,i]of this.#e){for(const[s,r]of i.byApi)r.expiresAt<=e&&i.byApi.delete(s);i.byApi.size===0&&this.#e.delete(t)}for(const[t,i]of this.#t)i.expiresAt<=e&&this.#t.delete(t)}#i(e){return d("sha256").update(e).digest("hex")}#s(e){return Buffer.from(l("sha256",e,this.#n,x,32))}}const T=new m;export{g as ELICITATION_TTL_MS,m as SecretStore,T as secretStore};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { McpToolContext } from '../types.js';
|
|
2
|
+
import { getApiDescriptionFromFs } from '../docs-mcp/tools/openapi/utils.js';
|
|
3
|
+
export type UpstreamAuthScheme = {
|
|
4
|
+
kind: 'raw';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'bearer';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'apiKey';
|
|
9
|
+
header: string;
|
|
10
|
+
} | {
|
|
11
|
+
kind: 'oauth2ClientCredentials';
|
|
12
|
+
tokenUrl: string;
|
|
13
|
+
scopes: string[];
|
|
14
|
+
};
|
|
15
|
+
type SecuritySchemeLoader = typeof getApiDescriptionFromFs;
|
|
16
|
+
export declare function resolveUpstreamAuthScheme(context: McpToolContext, apiRelativePath: string, loadDescription?: SecuritySchemeLoader): Promise<UpstreamAuthScheme>;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=upstream-auth-scheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getApiDescriptionFromFs as o}from"../docs-mcp/tools/openapi/utils.js";const c={kind:"raw"};async function u(i,r,n=o){const s=Object.values(i.apiDescriptionsMap??{}).find(t=>t.relativePath===r);if(!s)return c;try{const t=await n({relativePath:s.relativePath,outdir:i.outdir||"",user:i.user,rbac:i.config?.access?.rbac||{},requiresLogin:!!i.config?.access?.requiresLogin});return a(t)}catch{return c}}function a(i){const r=Object.values(i?.components?.securitySchemes??{}).filter(l),n=r.find(e=>e.type==="oauth2"&&e.flows?.clientCredentials);if(n?.flows.clientCredentials){const e=n.flows.clientCredentials;return{kind:"oauth2ClientCredentials",tokenUrl:e.tokenUrl,scopes:Object.keys(e.scopes??{})}}if(r.find(e=>e.type==="http"&&e.scheme==="bearer"||e.type==="oauth2"))return{kind:"bearer"};const t=r.find(e=>e.type==="apiKey"&&e.in==="header");return t?{kind:"apiKey",header:t.name||"X-Api-Key"}:c}function l(i){return typeof i=="object"&&i!==null&&"type"in i}export{u as resolveUpstreamAuthScheme};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CallToolResult } from '@redocly/mcp-typescript-sdk/types.js';
|
|
2
|
+
export declare const UPSTREAM_AUTH_REQUIRED_META = "com.redocly/upstream-auth-required";
|
|
3
|
+
export type UpstreamAuthRequiredEvent = {
|
|
4
|
+
api: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function withAuthRequiredMeta(result: CallToolResult, events: UpstreamAuthRequiredEvent[]): CallToolResult;
|
|
8
|
+
export declare function readAuthRequiredMeta(result: CallToolResult): UpstreamAuthRequiredEvent[];
|
|
9
|
+
export declare function stripAuthRequiredMeta(result: CallToolResult): CallToolResult;
|
|
10
|
+
//# sourceMappingURL=upstream-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const n="com.redocly/upstream-auth-required";function i(e,a){const t=[...new Map(a.map(r=>[r.api,r])).values()];return t.length===0?e:{...e,_meta:{...e._meta,[n]:t}}}function o(e){const a=e._meta?.[n];return Array.isArray(a)?a.filter(t=>typeof t?.api=="string"&&typeof t?.name=="string"):[]}function p(e){if(!e._meta||!(n in e._meta))return e;const{[n]:a,...t}=e._meta,r={...e};return Object.keys(t).length>0?r._meta=t:delete r._meta,r}export{n as UPSTREAM_AUTH_REQUIRED_META,o as readAuthRequiredMeta,p as stripAuthRequiredMeta,i as withAuthRequiredMeta};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{toFetchResponse as
|
|
1
|
+
import{toFetchResponse as j,toReqRes as H}from"fetch-to-node";import{isInitializeRequest as L}from"@redocly/mcp-typescript-sdk/types.js";import{logger as S}from"../../../tools/notifiers/logger.js";import{isObject as B}from"../../../../utils/guards/is-object.js";import{getAccessibleSkills as J}from"../../../utils/skills/get-accessible-skills.js";import{getSkillUrl as P}from"../../../utils/skills/get-skill-url.js";import{createDocsMcpServer as A}from"../servers/docs-server.js";import{mcpSessionManager as g}from"../servers/session-manager.js";import{filterApiDescriptionsByRbac as _,filterGraphqlDescriptionsByRbac as z}from"../utils.js";import{createMethodNotAllowedError as V,withErrorHandling as F}from"./errors.js";import{attachSseKeepalive as G}from"./sse-keepalive.js";import{constructInvalidTokenResponse as $,constructUnauthorizedResponse as K,handleMcpAuth as X,shouldHandleMcpAuth as Q}from"../auth/auth-handlers.js";import{extractTokenFromAuthHeader as W}from"../utils/jwt.js";import{DEFAULT_DOCS_MCP_SERVER_NAME as Y}from"../constants.js";const h="X-Redocly-AI-Metadata";function Z(e){if(e)try{const o=JSON.parse(e);if(B(o))return o;S.warn(`Ignoring ${h} header: not a JSON object.`)}catch{S.warn(`Ignoring ${h} header: invalid JSON.`)}}const q=async(e,o,a)=>{const t=!!o?.config?.access?.requiresLogin,r=o?.config?.access?.rbac||{},l=a?.props?.config?.mcpMode??"tools";let c;const R=e.headers.get("Authorization");if(Q(t,r)){const{isAuthenticated:f,isTokenValid:p,currentUser:n,accessToken:u}=await X(e,o);if(!f)return K(new URL(e.url).origin);if(!p)return $();n&&(o.user=n),c=u}else R&&(c=W(R||"")??void 0);let i;return await F(async()=>{const f=e.headers.get("mcp-session-id")??void 0,p=a?.props?.config?.elicitationEnabled??!1;if(p&&f){const s=g.get(f);if(!s)return new Response(JSON.stringify({jsonrpc:"2.0",error:{code:-32001,message:"Session not found"},id:null}),{status:404,headers:{"Content-Type":"application/json"}});const d=e.method==="POST"?await e.json():void 0;return await w(e,s,d)}if(e.method==="GET")return new Response(JSON.stringify({error:"Method Not Allowed",message:`In order to use this MCP server, you need to register it in your MCP Client (VS Code, Cursor, Claude Code, etc.) using this URL: ${e.url}`}),{status:405,headers:{"Content-Type":"application/json"}});if(e.method!=="POST")return V();const n=a,u=n?.props?.config?.apiDescriptionsMap||{},y=n?.props?.config?.graphqlDescriptions||[],D=n?.props?.config?.mcpDocsServerName||Y,{config:{mcp:T={}}}=o,b=_(u,o.user,r,t),C=z(y,o.user,r,t),E=o.config.products?Object.values(o.config.products).map(s=>s?.name):[],v=T.docs?.name||D,U=Z(e.headers.get(h)),O=n?.props?.tools||[],k=n?.props?.skillsBaseUrl||new URL(e.url).origin,I=J(n?.props?.skills||[],o.user,r,t).map(s=>({slug:s.slug,name:s.name,description:s.description,content:s.content,uri:P(k,s.slug)})),m=await e.json(),N=p&&L(m),M={name:v,tools:O,skills:I,context:{...o,accessToken:c,outdir:o.outdir||"",baseUrl:o.baseUrl||new URL(e.url).origin,apiDescriptionsMap:b,graphqlDescriptions:C,products:E,metadata:U,mcpMode:l,elicitationEnabled:p}};if(N){const s=await A({...M,sessionHooks:{onInitialized:d=>{g.set(d,s),i=void 0},onClosed:d=>{g.close(d)}}});return i=s,await w(e,s,m)}return i=await A(M),await w(e,i,m)},async()=>{i&&(await i.cleanup(),i=void 0)},l)};async function w(e,o,a){const{req:t,res:r}=H(e),l=G(r);try{await o.transport.handleRequest(t,r,a)}catch(c){throw l.stop(),c}return j(r)}var me=q;export{me as default};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { McpErrorCodeTypes } from '../types.js';
|
|
1
|
+
import type { McpErrorCodeTypes, McpMode } from '../types.js';
|
|
2
2
|
export type McpErrorData = {
|
|
3
3
|
readonly code: McpErrorCodeTypes;
|
|
4
4
|
readonly message: string;
|
|
@@ -13,8 +13,8 @@ export declare class McpError extends Error {
|
|
|
13
13
|
createErrorResponse(): Response;
|
|
14
14
|
}
|
|
15
15
|
export declare function createMethodNotAllowedError(requestId?: string | number | null): Response;
|
|
16
|
-
export declare function createInternalServerError(error: Error | string, requestId?: string | number | null): Response;
|
|
16
|
+
export declare function createInternalServerError(error: Error | string, requestId?: string | number | null, mode?: McpMode): Response;
|
|
17
17
|
export declare function createInvalidRequestError(message?: string, requestId?: string | number | null): Response;
|
|
18
18
|
export declare function createInvalidParamsError(message?: string, requestId?: string | number | null): Response;
|
|
19
|
-
export declare function withErrorHandling<T>(operation: () => Promise<T>, cleanup?: () => Promise<void
|
|
19
|
+
export declare function withErrorHandling<T>(operation: () => Promise<T>, cleanup?: () => Promise<void>, mode?: McpMode): Promise<T | Response>;
|
|
20
20
|
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{telemetry as d}from"../../../telemetry/index.js";import{McpErrorCodes as
|
|
1
|
+
import{telemetry as d}from"../../../telemetry/index.js";import{McpErrorCodes as s,McpServerType as n}from"../constants.js";class a extends Error{code;data;requestId;constructor(e,t,o,c){super(t),this.name="McpError",this.code=e,this.data=o,this.requestId=c}createErrorResponse(){const e={jsonrpc:"2.0",error:{code:this.code,message:this.message,...this.data?{data:this.data}:{}},id:this.requestId??null},t=i(this.code);return new Response(JSON.stringify(e),{status:t,headers:{"Content-Type":"application/json"}})}}function i(r){switch(r){case s.InvalidRequest:case s.InvalidParams:return 400;case s.MethodNotFound:return 404;case s.ServerError:return 405;case s.InternalError:default:return 500}}function E(r){return new a(s.ServerError,"Method not allowed",void 0,r).createErrorResponse()}function u(r,e,t="tools"){const o=r instanceof Error?r.message:String(r),c=r instanceof Error?r.stack:void 0;return d.sendMcpErrorMessage([{id:n.Docs,object:"mcpServer",uri:`urn:redocly:realm:mcp:server:${n.Docs}`,server_type:n.Docs,message:o,stack:c||"",mode:t}]),new a(s.InternalError,"Internal server error mcp",o,e).createErrorResponse()}function v(r="Invalid request",e){return new a(s.InvalidRequest,r,void 0,e).createErrorResponse()}function f(r="Invalid parameters",e){return new a(s.InvalidParams,r,void 0,e).createErrorResponse()}async function h(r,e,t="tools"){try{return await r()}catch(o){return d.sendMcpErrorMessage([{id:n.Docs,object:"mcpServer",uri:`urn:redocly:realm:mcp:server:${n.Docs}`,server_type:n.Docs,message:o?.message||"",stack:o?.stack||"",mode:t}]),e&&e(),u(o,void 0,t)}}export{a as McpError,u as createInternalServerError,f as createInvalidParamsError,v as createInvalidRequestError,E as createMethodNotAllowedError,h as withErrorHandling};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{withPathPrefix as f}from"@redocly/theme/core/utils";import{importApiRoutesHandler as
|
|
1
|
+
import{withPathPrefix as f}from"@redocly/theme/core/utils";import{importApiRoutesHandler as D}from"../../../api-routes/import-api-routes-handlers.js";import{enhanceContext as g}from"../../../api-routes/helpers/enhance-context.js";import{telemetry as h}from"../../../telemetry/index.js";import{KvService as A}from"../../../persistence/kv/services/kv-service.js";import{envConfig as o}from"../../../config/env-config.js";import{acceptsHtml as R}from"../../../utils/ai-agent-detection.js";import{handleErrorPageRender as E}from"../../../web-server/utils.js";import{MCP_NOT_ALLOWED_TEMPLATE_ID as T}from"../constants.js";import{MCP_DOCS_SERVER_HANDLER_ID as _}from"../index.js";import{resolvePublicBaseUrl as w}from"../../../web-server/utils/resolve-public-base-url.js";async function P(a,t,e){const s=t.req.raw;if(a.requestHandlerId===_&&s.method==="GET"&&R(s.headers.get("accept"))&&e.globalData.mcpData?.docs?.enabled)return E(t,e,{slug:a.slug},405,T);const r=t.get("auth"),n=await e.resolveRouteStaticData(a)||{},l=f(a.slug),{requestHandlers:m}=await D(e.serverOutDir),c=m[a.requestHandlerId],p=(await c()).default,d=async()=>await A.getInstance({baseDbDir:e.serverOutDir,sqldRemoteDatabaseUrl:o.SQLD_REMOTE_DATABASE_URL,sqldRemoteDatabaseAuthToken:o.SQLD_REMOTE_DATABASE_AUTH_TOKEN}),u=g({honoCtx:t,ctx:{user:{teams:r.teams,email:r.claims.email,claims:r.claims,idpAccessToken:r.idpAccessToken,idpId:r.claims.idpId,isAuthenticated:r.isAuthenticated},config:e.config,outdir:e.outdir,baseUrl:new URL(t.req.url).origin},telemetry:h,getKv:d}),i=await p(t.req.raw,u,{...n,props:{...n.props,routeSlug:l,outdir:e.outdir,skills:e.config.skills?.hide===!0?[]:e.getSkills(),skillsBaseUrl:w(e,t)}});return i instanceof Response?i:typeof i=="string"?new Response(i,{headers:{"Content-Type":"text/plain"}}):new Response(JSON.stringify(i),{headers:{"Content-Type":"application/json"}})}export{P as handleMcpRequest};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ApiRoutesHandler } from '@redocly/config';
|
|
2
|
+
export declare function setCredentialsFetchImpl(impl: typeof fetch): void;
|
|
3
|
+
declare const mcpCredentialsHandler: ApiRoutesHandler;
|
|
4
|
+
export default mcpCredentialsHandler;
|
|
5
|
+
//# sourceMappingURL=mcp-credentials-handler.d.ts.map
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import{secretStore as d}from"../gateway-mcp/secret-store.js";import{buildAllowedHostMatchers as C}from"../gateway-mcp/fetch/allowed-hosts.js";import{assertUrlAllowed as P}from"../gateway-mcp/fetch/ssrf-guard.js";import{readCappedBody as $,ssrfSafeAgent as I}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'"},_=3e4,E=64*1024;let v=fetch;function j(e){v=e}const T=async e=>{if(e.method==="GET"){const t=new URL(e.url).searchParams.get("token")??"",n=d.peekElicitation(t);return n?new Response(B(n,t),{status:200,headers:p}):l()}if(e.method==="POST"){const t=await e.formData(),n=String(t.get("token")??"");return String(t.get("mode")??"token")==="client-credentials"?R(t,n):z(t,n)}return new Response("Method Not Allowed",{status:405,headers:{Allow:"GET, POST"}})};async function z(e,t){const n=String(e.get("secret")??"");if(!n)return l();const o=d.consumeElicitation(t);return o?(d.setSecret(o.sessionId,o.api,F(o.auth,n)),new Response(w(o.name),{status:200,headers:p})):l()}async function R(e,t){const n=String(e.get("client_id")??"").trim(),o=String(e.get("client_secret")??"").trim();if(!n||!o)return l();const y=d.peekElicitation(t);if(!y||y.auth.kind!=="oauth2ClientCredentials")return l();const r=d.consumeElicitation(t);if(!r||r.auth.kind!=="oauth2ClientCredentials")return l();const h=r.auth;try{const x=C([{url:h.tokenUrl}]),A=P(h.tokenUrl,x),g=new AbortController,S=setTimeout(()=>g.abort(),_);let f;try{f=await v(A,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:n,client_secret:o,...h.scopes.length>0?{scope:h.scopes.join(" ")}:{}}),redirect:"manual",signal:g.signal,dispatcher:I})}finally{clearTimeout(S)}if(!f.ok)return m(r.name);let k;try{k=await $(f,E)}catch{return m(r.name)}let u;try{u=JSON.parse(k)}catch{return m(r.name)}const b=typeof u=="object"&&u!==null&&"access_token"in u?u.access_token:void 0;return typeof b!="string"||!b?m(r.name):(d.setSecret(r.sessionId,r.api,{header:"Authorization",value:`Bearer ${b}`}),new Response(w(r.name),{status:200,headers:p}))}catch{return m(r.name)}}function F(e,t){return e.kind==="apiKey"?{header:e.header,value:t}:e.kind==="bearer"||e.kind==="oauth2ClientCredentials"?{header:"Authorization",value:O(t)}:{header:"Authorization",value:t}}function O(e){const t=e.trim();return t.toLowerCase().startsWith("bearer")?t:`Bearer ${t}`}function a(e){return e.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""")}function i(e){return`<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="robots" content="noindex">
|
|
7
|
+
<title>API credentials</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root{color-scheme:light dark}
|
|
10
|
+
*{box-sizing:border-box}
|
|
11
|
+
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#f3f4f6;color:#111827;padding:1.5rem}
|
|
12
|
+
.card{width:100%;max-width:26rem;background:#ffffff;border-radius:.75rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);padding:2rem}
|
|
13
|
+
h1{font-size:1.25rem;margin:0 0 .5rem}
|
|
14
|
+
.section{margin-top:1.5rem;padding-top:1.5rem;border-top:1px solid #e5e7eb}
|
|
15
|
+
.section h2{font-size:1rem;margin:0 0 .5rem}
|
|
16
|
+
p.hint{color:#6b7280;font-size:.875rem;line-height:1.4}
|
|
17
|
+
label{display:block;margin:1rem 0 .25rem;font-weight:600;font-size:.875rem}
|
|
18
|
+
input[type=password],input[type=text]{width:100%;padding:.625rem .75rem;font-size:1rem;border:1px solid #d1d5db;border-radius:.5rem;background:#fff;color:#111827}
|
|
19
|
+
input:focus{outline:none;border-color:#4f46e5;box-shadow:0 0 0 3px rgba(79,70,229,.2)}
|
|
20
|
+
button{margin-top:1.25rem;padding:.625rem 1.5rem;font-size:1rem;font-weight:600;color:#fff;background:#4f46e5;border:none;border-radius:.5rem;cursor:pointer}
|
|
21
|
+
button:hover{background:#4338ca}
|
|
22
|
+
button:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}
|
|
23
|
+
code{background:#f3f4f6;padding:.125rem .375rem;border-radius:.25rem;font-size:.85em}
|
|
24
|
+
footer{margin-top:1.5rem;padding-top:1rem;border-top:1px solid #e5e7eb;font-size:.75rem;color:#9ca3af;text-align:center}
|
|
25
|
+
@media (prefers-color-scheme:dark){
|
|
26
|
+
body{background:#0b0f19;color:#e5e7eb}
|
|
27
|
+
.card{background:#111827;box-shadow:0 10px 25px -5px rgba(0,0,0,.4)}
|
|
28
|
+
.section{border-top-color:#1f2937}
|
|
29
|
+
p.hint{color:#9ca3af}
|
|
30
|
+
input[type=password],input[type=text]{background:#0b0f19;border-color:#374151;color:#e5e7eb}
|
|
31
|
+
code{background:#1f2937}
|
|
32
|
+
footer{border-top-color:#1f2937;color:#6b7280}
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
35
|
+
</head>
|
|
36
|
+
<body><div class="card">${e}</div></body>
|
|
37
|
+
</html>`}function s(e){return`<h1>\u{1F512} ${a(e)}</h1>`}function c(){return"<footer>Secured by Redocly · credentials never enter the AI conversation</footer>"}function B(e,t){switch(e.auth.kind){case"bearer":return M(e.name,t);case"apiKey":return q(e.name,t,e.auth.header);case"oauth2ClientCredentials":return H(e.name,t,e.auth.scopes);default:return N(e.name,t)}}function M(e,t){return i(`
|
|
38
|
+
${s(`Credentials for ${e}`)}
|
|
39
|
+
<p class="hint">Stored encrypted for this MCP session only (up to 4 hours) and sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
|
|
40
|
+
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
41
|
+
<input type="hidden" name="token" value="${a(t)}">
|
|
42
|
+
<input type="hidden" name="mode" value="token">
|
|
43
|
+
<label for="secret">API token</label>
|
|
44
|
+
<input type="password" id="secret" name="secret" placeholder="your API token" required autofocus>
|
|
45
|
+
<p class="hint">Paste the raw token or one already prefixed with <code>Bearer</code> — we add the prefix automatically if it is missing.</p>
|
|
46
|
+
<button type="submit">Save credentials</button>
|
|
47
|
+
</form>
|
|
48
|
+
${c()}`)}function q(e,t,n){return i(`
|
|
49
|
+
${s(`Credentials for ${e}`)}
|
|
50
|
+
<p class="hint">Stored encrypted for this MCP session only (up to 4 hours) and sent to the API as the <code>${a(n)}</code> header. It never passes through the AI conversation.</p>
|
|
51
|
+
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
52
|
+
<input type="hidden" name="token" value="${a(t)}">
|
|
53
|
+
<input type="hidden" name="mode" value="token">
|
|
54
|
+
<label for="secret">${a(n)} value</label>
|
|
55
|
+
<input type="password" id="secret" name="secret" placeholder="value" required autofocus>
|
|
56
|
+
<button type="submit">Save credentials</button>
|
|
57
|
+
</form>
|
|
58
|
+
${c()}`)}function H(e,t,n){const o=n.length>0?n.map(a).join(", "):"default scopes";return i(`
|
|
59
|
+
${s(`Credentials for ${e}`)}
|
|
60
|
+
<p class="hint">Paste a token you already have, or let us exchange client credentials for one via OAuth2 client_credentials.</p>
|
|
61
|
+
<div class="section">
|
|
62
|
+
<h2>I have a token</h2>
|
|
63
|
+
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
64
|
+
<input type="hidden" name="token" value="${a(t)}">
|
|
65
|
+
<input type="hidden" name="mode" value="token">
|
|
66
|
+
<label for="secret">API token</label>
|
|
67
|
+
<input type="password" id="secret" name="secret" placeholder="your API token" required autofocus>
|
|
68
|
+
<p class="hint">We add <code>Bearer</code> automatically if it is missing.</p>
|
|
69
|
+
<button type="submit">Save token</button>
|
|
70
|
+
</form>
|
|
71
|
+
</div>
|
|
72
|
+
<div class="section">
|
|
73
|
+
<h2>Exchange client credentials</h2>
|
|
74
|
+
<p class="hint">We request a token from the API on your behalf. Scopes: ${o}.</p>
|
|
75
|
+
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
76
|
+
<input type="hidden" name="token" value="${a(t)}">
|
|
77
|
+
<input type="hidden" name="mode" value="client-credentials">
|
|
78
|
+
<label for="client_id">Client ID</label>
|
|
79
|
+
<input type="text" id="client_id" name="client_id" required>
|
|
80
|
+
<label for="client_secret">Client secret</label>
|
|
81
|
+
<input type="password" id="client_secret" name="client_secret" required>
|
|
82
|
+
<button type="submit">Exchange & save</button>
|
|
83
|
+
</form>
|
|
84
|
+
</div>
|
|
85
|
+
${c()}`)}function N(e,t){return i(`
|
|
86
|
+
${s(`Credentials for ${e}`)}
|
|
87
|
+
<p class="hint">The value is stored encrypted for this MCP session only (up to 4 hours) and is sent to the API as the <code>Authorization</code> header. It never passes through the AI conversation.</p>
|
|
88
|
+
<form method="post" action="/mcp/credentials" autocomplete="off">
|
|
89
|
+
<input type="hidden" name="token" value="${a(t)}">
|
|
90
|
+
<input type="hidden" name="mode" value="token">
|
|
91
|
+
<label for="secret">Authorization header value</label>
|
|
92
|
+
<input type="password" id="secret" name="secret" placeholder="Bearer <your token>" required autofocus>
|
|
93
|
+
<button type="submit">Save credentials</button>
|
|
94
|
+
</form>
|
|
95
|
+
${c()}`)}function w(e){return i(`
|
|
96
|
+
${s("Credentials saved")}
|
|
97
|
+
<p>Credentials for ${a(e)} are set for this session. Return to your AI client and continue - confirm the elicitation prompt if it is still open.</p>
|
|
98
|
+
${c()}`)}function m(e){return new Response(i(`
|
|
99
|
+
${s("Exchange failed")}
|
|
100
|
+
<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>
|
|
101
|
+
${c()}`),{status:502,headers:p})}function l(){return new Response(i(`${s("Link expired")}<p>This credentials link is invalid or has expired. Retry the request in your AI client to get a fresh link.</p>${c()}`),{status:404,headers:p})}var D=T;export{D as default,j as setCredentialsFetchImpl};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { LifecyclePluginInstance } from '../../types';
|
|
2
2
|
export declare const MCP_DOCS_SERVER_HANDLER_ID = "mcp-docs-server-handler";
|
|
3
|
+
export declare const MCP_CREDENTIALS_HANDLER_ID = "mcp-credentials-handler";
|
|
3
4
|
export declare function mcpServerPlugin(): Promise<LifecyclePluginInstance>;
|
|
4
5
|
export default mcpServerPlugin;
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fileURLToPath as
|
|
1
|
+
import{fileURLToPath as L}from"node:url";import{dirname as q,join as c}from"node:path";import{logger as m}from"../../tools/notifiers/logger.js";import{reporter as w}from"../../tools/notifiers/reporter.js";import{isDefined as N}from"../../../utils/guards/is-defined.js";import{buildGraphqlDescriptions as G,filterIgnoredApiDescriptions as D,getCleanedUpApiDescriptions as H,isMcpInRedirects as I}from"./utils.js";import{getMcpMode as U,isMcpElicitationEnabled as j}from"./utils/mode.js";import{getDefaultToolSchemas as x}from"./docs-mcp/tool-schemas.js";import{EXECUTE_TOOL_SCHEMA as F}from"./codemode/tools/execute-schema.js";import{isGatewayCapabilityEnabled as V,isGatewayEligible as B}from"./gateway-mcp/eligibility.js";import{telemetryTraceStep as J}from"../../../cli/telemetry/helpers/trace-step.js";import{DEFAULT_DOCS_MCP_SERVER_NAME as E,MCP_NOT_ALLOWED_TEMPLATE_ID as W}from"./constants.js";const M="mcp-docs-server-handler",X="/mcp",C="mcp-credentials-handler",k="/mcp/credentials",p=q(L(import.meta.url));async function z(){let o=!1;return{id:"mcp",async processContent(i,r){o||await J("build.plugin.mcp",async a=>{const s=await i.loadOpenApiDefinitions(r),d=await i.loadGraphqlDefinitions(r),n=await r.getConfig(),{mcp:t,redirects:l}=n;if(a?.setAttribute("config",JSON.stringify(t||{})),!(t?.hide||t?.docs?.hide)&&(I(l)&&await w.panicOnBuildContentError('The reserved route "/mcp" cannot be used in redirect configuration. Please remove any redirects involving "/mcp" from your redocly.yaml config.'),i.createTemplate(W,"@redocly/theme/layouts/McpNotAllowed"),!t?.docs?.hide)){m.info("Configuring MCP servers...");const _=c(p,"./handlers/docs-mcp-handler.js"),P=H(D(s,t?.docs?.ignore||[])),v=!!n?.access?.requiresLogin,f=n?.access?.rbac||{},R=V(t),g=new Map;for(const e of P){const b=e.definition.info?.title||"",O=e.definition.info?.version||"";g.set(e.relativePath,{name:b,description:e.definition.info?.description||"",version:O,servers:e.definition.servers||[],relativePath:e.relativePath,...R&&B(e.definition.info,e.relativePath,f)?{fetchEligible:!0}:{}})}const u=G(D(d,t?.docs?.ignore||[])),S=Object.values(n.products||{}).map(e=>e?.name).filter(N),y=n.l10n?.locales?.map(e=>e.code)??r.fs.localeFolders,A=x({products:S,locales:y,requiresLogin:v,rbac:f,hasGraphqlApis:u.length>0});i.addMcpTools(c(p,"./docs-mcp/tools/index.js"),A);const h=U(),T=j();h==="code"&&i.addMcpTools(c(p,"./codemode/tools/execute.js"),[F]),await K({actions:i,handlerPath:_,apiDescriptionsMap:Object.fromEntries(g),graphqlDescriptions:u,mcpConfig:t,mcpMode:h,elicitationEnabled:T}),o=!0}})},async afterRoutesCreated(i,r){const a=await r.getConfig(),{mcp:s}=a;i.setGlobalData({mcpData:{docs:{enabled:!s?.hide&&!s?.docs?.hide,name:s?.docs?.name||E}}})}}}var ce=z;async function K({actions:o,handlerPath:i,apiDescriptionsMap:r,graphqlDescriptions:a,mcpConfig:s,mcpMode:d,elicitationEnabled:n}){if(o.createRequestHandler(M,i),o.addApiRoute({slug:X,requestHandlerId:M,httpMethod:"all",getStaticData:async()=>({props:{config:{apiDescriptionsMap:r,graphqlDescriptions:a,mcpDocsServerName:s?.docs?.name||E,mcpMode:d,elicitationEnabled:n},serverOutDir:o.serverOutDir,tools:o.getMcpTools().map(({importPath:t,...l})=>l)}})}),m.info("Registered Docs MCP Server endpoint at /mcp"),n){const t=c(p,"./handlers/mcp-credentials-handler.js");o.createRequestHandler(C,t),o.addApiRoute({slug:k,requestHandlerId:C,httpMethod:"all",getStaticData:async()=>({props:{}})}),m.info("Registered MCP credentials endpoint at /mcp/credentials")}}export{C as MCP_CREDENTIALS_HANDLER_ID,M as MCP_DOCS_SERVER_HANDLER_ID,ce as default,z as mcpServerPlugin};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { McpToolSchema } from '../../../types';
|
|
2
2
|
import type { McpToolContext, SkillMcpResource } from '../types.js';
|
|
3
|
-
import { type McpServerInstance } from './mcp-server.js';
|
|
4
|
-
export declare function createDocsMcpServer({ name, tools, context, skills, }: {
|
|
3
|
+
import { type McpServerInstance, type McpSessionHooks } from './mcp-server.js';
|
|
4
|
+
export declare function createDocsMcpServer({ name, tools, context, sessionHooks, skills, }: {
|
|
5
5
|
name: string;
|
|
6
6
|
tools: McpToolSchema[];
|
|
7
7
|
context: McpToolContext;
|
|
8
|
+
sessionHooks?: McpSessionHooks;
|
|
8
9
|
skills?: SkillMcpResource[];
|
|
9
10
|
}): Promise<McpServerInstance>;
|
|
10
11
|
//# sourceMappingURL=docs-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{logger as O}from"../../../tools/notifiers/logger.js";import{mcpToolWorkers as _,MCP_TOOL_IS_AVAILABLE_KEY as u}from"../../../workers/mcp-tool-worker-pool.js";import{EXECUTE_TOOL_SCHEMA as
|
|
1
|
+
import{logger as O}from"../../../tools/notifiers/logger.js";import{mcpToolWorkers as _,MCP_TOOL_IS_AVAILABLE_KEY as u}from"../../../workers/mcp-tool-worker-pool.js";import{EXECUTE_TOOL_SCHEMA as b}from"../codemode/tools/execute-schema.js";import{EXECUTE_TOOL_NAME as p,DESCRIBE_TOOLS_TOOL_NAME as M,CODE_MODE_TOOL_NAMES as S,FETCH_TOOL_NAME as A}from"../codemode/constants.js";import{buildCodeModeInstructions as h,buildExecuteDescription as L,DESCRIBE_TOOLS_DESCRIPTION as g}from"../codemode/prompts.js";import{composeExecuteAnnotations as v,getActiveCapabilities as x}from"../codemode/capabilities/index.js";import{describeToolsSchema as D,describeTools as y}from"../codemode/tools/describe-tools.js";import{connectMcpServer as I,createMcpServer as k,createToolDispatch as w,extractSerializableContext as N}from"./mcp-server.js";async function $({name:E,tools:n,context:o,sessionHooks:d,skills:T=[]}){const a=o.mcpMode==="code",r=a?x(o):[],s=r.some(e=>e.id===A),{server:i,transport:f}=k(E,a?h(s):void 0,d),c=N(o),m=w(c,{getClientCapabilities:()=>i.server.getClientCapabilities()}),l=async e=>{try{return await _.exec(u,[{toolName:e,context:c}],{timeout:1e4})}catch(t){return O.error(`Failed to check MCP tool availability for "${e}": ${t instanceof Error?t.message:String(t)}`),!1}};if(a){const e=n.filter(t=>!S.has(t.name));i.tool(p,L(s),b.schema,v(r),m(p)),i.tool(M,g,D(e,r),y(e,l,r,o))}else await Promise.all(n.map(async e=>{const t=e.annotations||{title:e.name},C=i.tool(e.name,e.description,e.schema,t,m(e.name));await l(e.name)||C.disable()}));for(const e of T)i.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 I(i,f)}export{$ as createDocsMcpServer};
|
|
@@ -7,12 +7,21 @@ export type McpServerInstance = {
|
|
|
7
7
|
transport: StreamableHTTPServerTransport;
|
|
8
8
|
cleanup: () => Promise<void>;
|
|
9
9
|
};
|
|
10
|
-
export
|
|
10
|
+
export type McpSessionHooks = {
|
|
11
|
+
onInitialized: (sessionId: string) => void;
|
|
12
|
+
onClosed: (sessionId: string) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function createMcpServer(name: string, instructions?: string, sessionHooks?: McpSessionHooks): {
|
|
11
15
|
server: McpServer;
|
|
12
16
|
transport: StreamableHTTPServerTransport;
|
|
13
17
|
};
|
|
14
18
|
export declare function connectMcpServer(server: McpServer, transport: StreamableHTTPServerTransport): Promise<McpServerInstance>;
|
|
19
|
+
export type ToolDispatchDeps = {
|
|
20
|
+
getClientCapabilities: () => {
|
|
21
|
+
elicitation?: unknown;
|
|
22
|
+
} | undefined;
|
|
23
|
+
};
|
|
15
24
|
/** Build the tool callback that ships a tool call to the worker pool. */
|
|
16
|
-
export declare function createToolDispatch(serializableContext: McpToolContext): (toolName: string) => ToolCallback<Record<string, unknown>>;
|
|
25
|
+
export declare function createToolDispatch(serializableContext: McpToolContext, deps?: ToolDispatchDeps): (toolName: string) => ToolCallback<Record<string, unknown>>;
|
|
17
26
|
export declare function extractSerializableContext(context: McpToolContext): McpToolContext;
|
|
18
27
|
//# sourceMappingURL=mcp-server.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{McpServer as
|
|
1
|
+
import{randomUUID as I}from"node:crypto";import{McpServer as S}from"@redocly/mcp-typescript-sdk/server/mcp.js";import{StreamableHTTPServerTransport as g}from"@redocly/mcp-typescript-sdk/server/streamableHttp.js";import{logger as M}from"../../../tools/notifiers/logger.js";import{mcpToolWorkers as h,MCP_TOOL_WORKER_KEY as q}from"../../../workers/mcp-tool-worker-pool.js";import{secretStore as E}from"../gateway-mcp/secret-store.js";import{readAuthRequiredMeta as T,stripAuthRequiredMeta as b}from"../gateway-mcp/upstream-auth.js";import{runUpstreamCredentialElicitation as w}from"../gateway-mcp/elicitation.js";import{resolveUpstreamAuthScheme as U}from"../gateway-mcp/upstream-auth-scheme.js";const v=6e4,p=new Map;function D(e,t){const i=p.get(e);if(i)return i;const r=t().finally(()=>{p.delete(e)});return p.set(e,r),r}function G(e,t,i){const r=new S({name:e,version:new Date().toISOString().slice(0,10)},{capabilities:{logging:{}},...t?{instructions:t}:{}}),s=new g(i?{sessionIdGenerator:()=>I(),onsessioninitialized:i.onInitialized,onsessionclosed:i.onClosed}:{sessionIdGenerator:void 0});return{server:r,transport:s}}async function K(e,t){return await e.connect(t),{server:e,transport:t,cleanup:async()=>{t.close()}}}function L(e,t){return i=>async(r,s)=>{M.info(`MCP tool called: ${i}`);const l=async()=>{const a=e.elicitationEnabled&&s.sessionId?E.getSecretsForSession(s.sessionId):void 0,c={toolName:i,args:r,context:{...e,upstreamSecrets:a},extra:O(s)};return await h.exec(q,[c],{timeout:v})};let n=await l();const o=T(n);if(o.length>0&&e.elicitationEnabled&&s.sessionId){const a=s.sessionId,c=await U(e,o[0].api),u=await D(`${a}:${o[0].api}`,()=>w({sendRequest:(d,m,f)=>s.sendRequest(d,m,f),clientSupportsElicitation:!!t?.getClientCapabilities()?.elicitation,sessionId:a,baseUrl:e.baseUrl??"",events:o,auth:c}));u.status==="accepted"?n=await l():u.status==="fallback"&&(n={...n,content:[...n.content,{type:"text",text:u.text}]})}return b(n)}}function O(e){return{sessionId:e.sessionId,authInfo:e.authInfo,requestId:e.requestId,requestInfo:e.requestInfo,_meta:e._meta}}function W(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,upstreamSecrets:e.upstreamSecrets}}export{K as connectMcpServer,G as createMcpServer,L as createToolDispatch,W as extractSerializableContext};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { McpServerInstance } from './mcp-server.js';
|
|
2
|
+
type SessionManagerOptions = {
|
|
3
|
+
maxSessions?: number;
|
|
4
|
+
onEvict?: (sessionId: string) => void;
|
|
5
|
+
};
|
|
6
|
+
export declare class McpSessionManager {
|
|
7
|
+
#private;
|
|
8
|
+
constructor(options?: SessionManagerOptions);
|
|
9
|
+
get(sessionId: string): McpServerInstance | undefined;
|
|
10
|
+
set(sessionId: string, instance: McpServerInstance): void;
|
|
11
|
+
close(sessionId: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare const mcpSessionManager: McpSessionManager;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=session-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{logger as i}from"../../../tools/notifiers/logger.js";import{secretStore as a}from"../gateway-mcp/secret-store.js";const c=1800*1e3,o=60*1e3,r=500;class h{#t=new Map;#e;#s;constructor(t={}){this.#e=t.maxSessions??r,this.#s=t.onEvict??(s=>a.deleteSession(s)),setInterval(()=>{this.#i()},o).unref?.()}get(t){const e=this.#t.get(t);if(e)return e.lastAccess=Date.now(),this.#t.delete(t),this.#t.set(t,e),e.instance}set(t,e){const s=this.#t.get(t);if(this.#t.delete(t),this.#t.set(t,{instance:e,lastAccess:Date.now()}),s&&s.instance!==e&&this.#n(t,s.instance),this.#t.size>this.#e){const n=this.#t.keys().next().value;n!==void 0&&this.close(n)}}async close(t){const e=this.#t.get(t);e&&(this.#t.delete(t),this.#s(t),await this.#n(t,e.instance))}async#n(t,e){try{await e.cleanup()}catch(s){i.warn(`Failed to clean up MCP session ${t.slice(0,8)}\u2026: ${s instanceof Error?s.message:String(s)}`)}}async#i(){const t=Date.now()-c;for(const[e,s]of this.#t)s.lastAccess<=t&&await this.close(e)}}const g=new h;export{h as McpSessionManager,g as mcpSessionManager};
|
|
@@ -6,6 +6,7 @@ import type { OpenAPIServer } from '@redocly/openapi-docs/lib/types/open-api.js'
|
|
|
6
6
|
import type { McpErrorCodes, McpServerType } from './constants';
|
|
7
7
|
import type { McpToolSchema } from '../../types';
|
|
8
8
|
import type { Skill } from '../../types/plugins/skills.js';
|
|
9
|
+
import type { StoredSecret } from './gateway-mcp/secret-store.js';
|
|
9
10
|
type ObjectValues<T> = T[keyof T];
|
|
10
11
|
export type McpServerType = ObjectValues<typeof McpServerType>;
|
|
11
12
|
export type McpErrorCodeTypes = ObjectValues<typeof McpErrorCodes>;
|
|
@@ -26,6 +27,8 @@ export type McpDocsStaticDataPropsConfig = {
|
|
|
26
27
|
mcpDocsServerName: string;
|
|
27
28
|
apiDescriptionsMap: Record<string, ApiDescriptionInfo>;
|
|
28
29
|
graphqlDescriptions: GraphqlDescriptionInfo[];
|
|
30
|
+
mcpMode: McpMode;
|
|
31
|
+
elicitationEnabled: boolean;
|
|
29
32
|
};
|
|
30
33
|
export type McpDocsStaticData = PageStaticData & {
|
|
31
34
|
props: {
|
|
@@ -59,18 +62,6 @@ export type McpEndpoint = OpenAPIOperation & {
|
|
|
59
62
|
'x-mcp'?: XMcpConfig;
|
|
60
63
|
};
|
|
61
64
|
export type McpMode = 'tools' | 'code';
|
|
62
|
-
export type McpPluginConfig = {
|
|
63
|
-
readonly hide: boolean;
|
|
64
|
-
readonly docs: {
|
|
65
|
-
readonly hide: boolean;
|
|
66
|
-
readonly name: string;
|
|
67
|
-
readonly ignore: readonly string[];
|
|
68
|
-
};
|
|
69
|
-
readonly gateway?: {
|
|
70
|
-
readonly hide: boolean;
|
|
71
|
-
readonly allowedHosts?: readonly string[];
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
65
|
export type McpUserInfo = {
|
|
75
66
|
email?: string;
|
|
76
67
|
name?: string;
|
|
@@ -102,6 +93,9 @@ export type McpToolContext = Omit<ApiFunctionsContext, 'telemetry' | 'getKv' | k
|
|
|
102
93
|
accessToken?: string;
|
|
103
94
|
metadata?: Record<string, unknown>;
|
|
104
95
|
upstreamAuthorization?: string;
|
|
96
|
+
mcpMode?: McpMode;
|
|
97
|
+
elicitationEnabled?: boolean;
|
|
98
|
+
upstreamSecrets?: Record<string, StoredSecret>;
|
|
105
99
|
};
|
|
106
100
|
export type McpToolHandler<TArgs extends Record<string, unknown> = Record<string, unknown>> = (args: TArgs, context: McpToolContext, extra: McpToolExtra) => Promise<McpToolWorkerResponse>;
|
|
107
101
|
export type McpToolIsAvailable = (context: McpToolContext) => boolean | Promise<boolean>;
|
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import type { McpUserInfo } from '../types';
|
|
2
1
|
/**
|
|
3
2
|
* Extract JWT token from Authorization header
|
|
4
3
|
*/
|
|
5
4
|
export declare function extractTokenFromAuthHeader(authHeader?: string | string[]): string | null;
|
|
6
|
-
/**
|
|
7
|
-
* Decode and validate JWT token to get user information
|
|
8
|
-
*/
|
|
9
|
-
export declare function getUserInfoFromToken(token: string): McpUserInfo | null;
|
|
10
|
-
/**
|
|
11
|
-
* Get user info from MCP request headers
|
|
12
|
-
*/
|
|
13
|
-
export declare function getUserInfoFromHeaders(headers: Record<string, string | string[] | undefined>): McpUserInfo | null;
|
|
14
5
|
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
function n(r){if(!r)return null;const e=Array.isArray(r)?r[0]:r;return!e||!e.startsWith("Bearer ")?null:e.replace(/^Bearer /,"")}export{n as extractTokenFromAuthHeader};
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { McpMode } from '../types.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Codemode - `execute` sandbox and `describeTools`. Gated by the org-level `mcpMode`
|
|
4
|
+
* entitlement (GrowthBook `mcp-mode` flag → PLAN_GATES JWT).
|
|
5
|
+
*
|
|
6
|
+
* Read the VALUE, never `canAccessFeature('mcpMode')` — 'tools' is truthy.
|
|
7
|
+
* Only call these helpers where EntitlementsProvider is initialized: the build lifecycle
|
|
8
|
+
* and web-server routes. Request handlers and workers run in module graphs with an
|
|
9
|
+
* UNINITIALIZED provider copy — they must read the build-stamped `mcpMode` from static
|
|
10
|
+
* data / McpToolContext instead (see docs-mcp-handler / execute-mcp-tool).
|
|
11
|
+
*/
|
|
3
12
|
export declare function isMcpCodeModeEnabled(): boolean;
|
|
4
|
-
/** The gateway capability — a guarded `fetch` in the `execute` sandbox. Requires
|
|
13
|
+
/** The gateway capability — a guarded `fetch` in the `execute` sandbox. Requires code mode. */
|
|
5
14
|
export declare function isMcpGatewayEnabled(): boolean;
|
|
15
|
+
export declare function isMcpElicitationEnabled(): boolean;
|
|
6
16
|
export declare function getMcpMode(): McpMode;
|
|
7
17
|
//# sourceMappingURL=mode.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{EntitlementsProvider as e}from"../../../entitlements/entitlements-provider.js";import{envConfig as n}from"../../../config/env-config.js";function t(){return e.instance().entitlements?.mcpMode==="code"}function o(){return!!e.instance().canAccessFeature("mcpGateway")&&t()}function i(){return(n.REDOCLY_EXP_MCP_ELICITATION_ENABLED??!1)&&o()}function a(){return t()?"code":"tools"}export{a as getMcpMode,t as isMcpCodeModeEnabled,i as isMcpElicitationEnabled,o as isMcpGatewayEnabled};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{telemetry as d}from"../../../telemetry/index.js";import{mcpToolHandlers as w}from"../../../../client/mcp-tool-handlers-entry.js";import{
|
|
1
|
+
import{telemetry as d}from"../../../telemetry/index.js";import{mcpToolHandlers as w}from"../../../../client/mcp-tool-handlers-entry.js";import{McpServerType as f}from"../constants.js";import{reportMcpToolCalled as y,reportMcpToolError as u}from"./mcp-tool-telemetry.js";async function b(r){d.initialize();const{toolName:o,args:c,context:i,extra:t}=r,a=i?.mcpMode??"tools",n=performance.now();try{const e=w[o];if(!e)throw new Error(`Unknown MCP tool: ${o}`);const{default:h}=await e(),l=h[o];if(!l)throw new Error(`MCP tool module does not export a handler for "${o}". Expected \`export default { '${o}': handler }\`.`);const s=await(typeof l=="function"?l:l.execute)(c,i,t);if(!x(s))throw new Error(`MCP tool "${o}" returned an invalid result.`);const m=Math.round(performance.now()-n);if(s.isError)u({tool:o,message:s.content.map(({text:p})=>p).join(" "),mode:a,serverType:f.Docs,durationMs:m});else{const p=s.content.map(({text:M})=>M).join("").length;y({tool:o,mode:a,serverType:f.Docs,durationMs:m,outputLength:p})}return s}catch(e){throw u({tool:o,message:e instanceof Error?e.message:String(e),mode:a,serverType:f.Docs,durationMs:Math.round(performance.now()-n),stack:e instanceof Error&&e.stack||""}),e}}function x(r){return!r||typeof r!="object"?!1:Array.isArray(r.content)}async function k(r){d.initialize();const{toolName:o,context:c}=r,i=performance.now();try{const t=w[o];if(!t)return!1;const{default:a}=await t(),n=a[o];return n?typeof n=="object"&&typeof n.isAvailable=="function"?await n.isAvailable(c):!0:!1}catch(t){throw u({tool:o,message:"Failed to check if MCP tool is available: "+(t instanceof Error?t.message:String(t)),mode:c?.mcpMode??"tools",serverType:f.Docs,durationMs:Math.round(performance.now()-i),stack:t instanceof Error&&t.stack||""}),t}}export{b as executeMcpTool,k as isMcpToolAvailable};
|