akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21
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/README.ko.md +1 -1
- package/README.md +1 -1
- package/base/symbols.ts +4 -0
- package/client/clientRuntime.ts +8 -0
- package/client/cn.ts +7 -0
- package/client/csrTypes.ts +2 -0
- package/client/frameDebug.ts +3 -2
- package/common/Logger.ts +4 -0
- package/common/deepObjectify.ts +21 -7
- package/common/index.ts +14 -0
- package/common/mcpExposure.ts +99 -0
- package/common/pathSet.ts +17 -5
- package/common/routeConvention.ts +30 -0
- package/constant/crystalize.ts +3 -1
- package/constant/fieldInfo.ts +6 -0
- package/constant/getDefault.ts +37 -9
- package/constant/immerify.ts +1 -1
- package/constant/index.ts +2 -0
- package/constant/labelOf.ts +17 -0
- package/constant/mask.ts +60 -0
- package/constant/purify.ts +17 -11
- package/constant/types.ts +14 -12
- package/dictionary/DictionaryLookup.ts +30 -0
- package/dictionary/agent.dictionary.ts +31 -0
- package/dictionary/agentTurn.dictionary.ts +29 -0
- package/dictionary/base.dictionary.ts +8 -0
- package/dictionary/dictInfo.ts +8 -0
- package/dictionary/dictionary.ts +9 -3
- package/dictionary/index.ts +1 -0
- package/document/database.ts +21 -0
- package/document/filterMeta.ts +7 -0
- package/document/index.ts +1 -0
- package/document/into.ts +7 -1
- package/document/noDocumentError.ts +12 -0
- package/fetch/agentTurn.ts +19 -0
- package/fetch/client/fetchClient.ts +71 -13
- package/fetch/client/wsClient.ts +17 -2
- package/fetch/fetchType/endpointFetch.type.ts +10 -4
- package/fetch/index.ts +1 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
- package/package.json +11 -9
- package/server/akanApp.ts +30 -34
- package/server/akanOption.ts +65 -7
- package/server/akanServer.ts +172 -7
- package/server/animatedImage.ts +83 -0
- package/server/artifact/routeClientCache.ts +14 -0
- package/server/assetEncoding.ts +47 -0
- package/server/devtools/signalSerializer.ts +2 -2
- package/server/devtools/types.ts +2 -2
- package/server/di/diLifecycle.ts +31 -3
- package/server/di/predefinedAdaptor.ts +6 -0
- package/server/imageOptimizer.ts +33 -46
- package/server/index.ts +1 -2
- package/server/mcp/McpAuth.ts +177 -0
- package/server/mcp/McpDispatcher.ts +245 -0
- package/server/mcp/McpEventStream.ts +76 -0
- package/server/mcp/McpExecutionContext.ts +105 -0
- package/server/mcp/McpRouter.ts +591 -0
- package/server/mcp/index.ts +5 -0
- package/server/resolver/CascadeRunner.ts +1 -1
- package/server/resolver/database.resolver.ts +42 -5
- package/server/resolver/service.resolver.ts +29 -0
- package/server/resolver/signal.resolver.ts +16 -1
- package/server/routeTreeBuilder.ts +2 -44
- package/server/rscWorker.tsx +6 -2
- package/server/subRouteIndexDocument.tsx +61 -0
- package/server/systemPageDocument.tsx +2 -2
- package/server/systemPages.tsx +49 -16
- package/server/vendor/akanjs-fetch.ts +1 -0
- package/server/webRouter.ts +159 -35
- package/service/agent.service.ts +14 -0
- package/service/base.service.ts +2 -1
- package/service/index.ts +1 -0
- package/service/predefinedAdaptor/database.adaptor.ts +78 -24
- package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
- package/service/predefinedAdaptor/index.ts +3 -0
- package/service/predefinedAdaptor/insightQuery.ts +183 -0
- package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
- package/service/predefinedAdaptor/role.adaptor.ts +2 -0
- package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
- package/service/serviceModule.ts +17 -0
- package/service/types.ts +4 -0
- package/signal/agent/AgentCatalogue.ts +165 -0
- package/signal/agent/index.ts +1 -0
- package/signal/agent.signal.ts +42 -0
- package/signal/agentTurn.ts +8 -0
- package/signal/agentTurnStream.ts +48 -0
- package/signal/base.signal.ts +2 -1
- package/signal/endpointInfo.ts +38 -2
- package/signal/guard.ts +13 -1
- package/signal/guards.ts +52 -1
- package/signal/index.ts +6 -0
- package/signal/mcp/McpDocument.ts +286 -0
- package/signal/mcp/McpProgress.ts +106 -0
- package/signal/mcp/McpUriTemplate.ts +85 -0
- package/signal/mcp/Msg.ts +368 -0
- package/signal/mcp/index.ts +5 -0
- package/signal/mcp/mcpProtocol.ts +120 -0
- package/signal/middleware.ts +7 -3
- package/signal/openapi/openapi.ts +32 -171
- package/signal/schema/JsonSchemaBuilder.ts +211 -0
- package/signal/schema/index.ts +1 -0
- package/signal/signalContext.ts +85 -9
- package/signal/slice.ts +9 -9
- package/signal/types.ts +1 -1
- package/store/action.ts +11 -22
- package/store/actionTag.ts +33 -0
- package/store/agent/AgentBridge.ts +323 -0
- package/store/agent/AgentContext.ts +74 -0
- package/store/agent/AgentPrompts.ts +82 -0
- package/store/agent/AgentVisibility.ts +68 -0
- package/store/agent/ScreenReader.ts +208 -0
- package/store/agent/StoreCatalogue.ts +351 -0
- package/store/agent/StoreSurfaceSource.ts +122 -0
- package/store/agent/index.ts +9 -0
- package/store/agent/storeSurface.ts +28 -0
- package/store/agent/types.ts +50 -0
- package/store/agentic/StToolBuilder.ts +199 -0
- package/store/agentic/attachAgentic.ts +22 -0
- package/store/agentic/index.ts +5 -0
- package/store/agentic/readableValue.ts +17 -0
- package/store/agentic/useStExpose.ts +18 -0
- package/store/agentic/useStState.ts +35 -0
- package/store/baseSt.ts +7 -0
- package/store/databaseStateNames.ts +31 -0
- package/store/formSetterNames.ts +21 -0
- package/store/index.ts +8 -0
- package/store/rootStore.ts +3 -1
- package/store/sliceRole.ts +36 -0
- package/store/state.ts +2 -12
- package/store/store.ts +19 -1
- package/store/storeInstance.ts +165 -20
- package/store/storeRegistry.ts +16 -1
- package/store/types.ts +18 -0
- package/store/withSelector.ts +7 -8
- package/test/registerDom.ts +4 -0
- package/types/base/symbols.d.ts +4 -0
- package/types/client/cn.d.ts +5 -0
- package/types/client/csrTypes.d.ts +2 -0
- package/types/common/Logger.d.ts +2 -0
- package/types/common/deepObjectify.d.ts +4 -2
- package/types/common/index.d.ts +2 -1
- package/types/common/mcpExposure.d.ts +60 -0
- package/types/common/routeConvention.d.ts +8 -0
- package/types/constant/fieldInfo.d.ts +1 -0
- package/types/constant/getDefault.d.ts +5 -0
- package/types/constant/index.d.ts +2 -0
- package/types/constant/labelOf.d.ts +6 -0
- package/types/constant/mask.d.ts +34 -0
- package/types/constant/purify.d.ts +2 -2
- package/types/constant/types.d.ts +2 -2
- package/types/dictionary/DictionaryLookup.d.ts +14 -0
- package/types/dictionary/agent.dictionary.d.ts +1 -0
- package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
- package/types/dictionary/base.dictionary.d.ts +1 -1
- package/types/dictionary/dictInfo.d.ts +6 -0
- package/types/dictionary/dictionary.d.ts +11 -9
- package/types/dictionary/index.d.ts +1 -0
- package/types/document/database.d.ts +20 -0
- package/types/document/filterMeta.d.ts +1 -0
- package/types/document/index.d.ts +1 -0
- package/types/document/into.d.ts +6 -1
- package/types/document/noDocumentError.d.ts +12 -0
- package/types/fetch/agentTurn.d.ts +593 -0
- package/types/fetch/client/fetchClient.d.ts +10 -0
- package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
- package/types/fetch/index.d.ts +1 -0
- package/types/server/akanOption.d.ts +35 -7
- package/types/server/akanServer.d.ts +44 -1
- package/types/server/animatedImage.d.ts +7 -0
- package/types/server/artifact/routeClientCache.d.ts +6 -0
- package/types/server/assetEncoding.d.ts +7 -0
- package/types/server/devtools/types.d.ts +2 -2
- package/types/server/di/predefinedAdaptor.d.ts +4 -1
- package/types/server/index.d.ts +0 -2
- package/types/server/mcp/McpAuth.d.ts +62 -0
- package/types/server/mcp/McpDispatcher.d.ts +54 -0
- package/types/server/mcp/McpEventStream.d.ts +18 -0
- package/types/server/mcp/McpExecutionContext.d.ts +44 -0
- package/types/server/mcp/McpRouter.d.ts +60 -0
- package/types/server/mcp/index.d.ts +5 -0
- package/types/server/mcp.d.ts +1 -0
- package/types/server/subRouteIndexDocument.d.ts +8 -0
- package/types/server/systemPageDocument.d.ts +1 -0
- package/types/server/systemPages.d.ts +5 -0
- package/types/server/vendor/akanjs-fetch.d.ts +1 -0
- package/types/service/agent.service.d.ts +12 -0
- package/types/service/base.service.d.ts +4 -0
- package/types/service/index.d.ts +1 -0
- package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
- package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
- package/types/service/predefinedAdaptor/index.d.ts +3 -0
- package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
- package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
- package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
- package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
- package/types/service/types.d.ts +4 -1
- package/types/signal/agent/AgentCatalogue.d.ts +100 -0
- package/types/signal/agent/index.d.ts +1 -0
- package/types/signal/agent.d.ts +1 -0
- package/types/signal/agent.signal.d.ts +29 -0
- package/types/signal/agentTurn.d.ts +9 -0
- package/types/signal/agentTurnStream.d.ts +16 -0
- package/types/signal/base.signal.d.ts +1 -1
- package/types/signal/endpointInfo.d.ts +21 -3
- package/types/signal/guard.d.ts +11 -0
- package/types/signal/guards.d.ts +22 -1
- package/types/signal/index.d.ts +6 -0
- package/types/signal/mcp/McpDocument.d.ts +65 -0
- package/types/signal/mcp/McpProgress.d.ts +40 -0
- package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
- package/types/signal/mcp/Msg.d.ts +143 -0
- package/types/signal/mcp/index.d.ts +5 -0
- package/types/signal/mcp/mcpProtocol.d.ts +107 -0
- package/types/signal/mcp.d.ts +1 -0
- package/types/signal/openapi/openapi.d.ts +3 -3
- package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
- package/types/signal/schema/index.d.ts +1 -0
- package/types/signal/schema.d.ts +1 -0
- package/types/signal/signalContext.d.ts +16 -1
- package/types/signal/slice.d.ts +8 -8
- package/types/signal/types.d.ts +1 -1
- package/types/store/actionTag.d.ts +21 -0
- package/types/store/agent/AgentBridge.d.ts +77 -0
- package/types/store/agent/AgentContext.d.ts +16 -0
- package/types/store/agent/AgentPrompts.d.ts +31 -0
- package/types/store/agent/AgentVisibility.d.ts +21 -0
- package/types/store/agent/ScreenReader.d.ts +11 -0
- package/types/store/agent/StoreCatalogue.d.ts +26 -0
- package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
- package/types/store/agent/index.d.ts +9 -0
- package/types/store/agent/storeSurface.d.ts +14 -0
- package/types/store/agent/types.d.ts +49 -0
- package/types/store/agent.d.ts +1 -0
- package/types/store/agentic/StToolBuilder.d.ts +41 -0
- package/types/store/agentic/attachAgentic.d.ts +14 -0
- package/types/store/agentic/index.d.ts +5 -0
- package/types/store/agentic/readableValue.d.ts +3 -0
- package/types/store/agentic/useStExpose.d.ts +8 -0
- package/types/store/agentic/useStState.d.ts +12 -0
- package/types/store/agentic.d.ts +1 -0
- package/types/store/baseSt.d.ts +29 -22
- package/types/store/databaseStateNames.d.ts +25 -0
- package/types/store/formSetterNames.d.ts +16 -0
- package/types/store/index.d.ts +7 -0
- package/types/store/rootStore.d.ts +5 -1
- package/types/store/sliceRole.d.ts +25 -0
- package/types/store/store.d.ts +7 -2
- package/types/store/storeInstance.d.ts +33 -1
- package/types/store/storeRegistry.d.ts +5 -0
- package/types/store/types.d.ts +15 -0
- package/types/store/withSelector.d.ts +7 -8
- package/types/test/registerDom.d.ts +1 -0
- package/types/ui/Agent/Approval.d.ts +7 -0
- package/types/ui/Agent/Bubble.d.ts +13 -0
- package/types/ui/Agent/Chat.d.ts +24 -0
- package/types/ui/Agent/Context.d.ts +9 -0
- package/types/ui/Agent/Dock.d.ts +16 -0
- package/types/ui/Agent/Guide.d.ts +9 -0
- package/types/ui/Agent/Section.d.ts +10 -0
- package/types/ui/Agent/StateKey.d.ts +16 -0
- package/types/ui/Agent/Tool.d.ts +15 -0
- package/types/ui/Agent/Transcript.d.ts +13 -0
- package/types/ui/Agent/Zone.d.ts +23 -0
- package/types/ui/Agent/fetchRunner.d.ts +12 -0
- package/types/ui/Agent/index.d.ts +17 -0
- package/types/ui/Agent/index_.d.ts +1 -0
- package/types/ui/Agent/sessionHistory.d.ts +12 -0
- package/types/ui/Agent.d.ts +1 -0
- package/types/ui/Badge.d.ts +1 -1
- package/types/ui/Button.d.ts +1 -1
- package/types/ui/CsrImage.d.ts +1 -1
- package/types/ui/Dropdown.d.ts +2 -0
- package/types/ui/Image.d.ts +3 -3
- package/types/ui/Layout/BottomInset.d.ts +2 -1
- package/types/ui/Layout/index.d.ts +1 -1
- package/types/ui/Signal/style.d.ts +4 -1
- package/types/ui/UiOverride/context.d.ts +14 -2
- package/types/ui/agentAttrs.d.ts +14 -0
- package/types/ui/index.d.ts +4 -1
- package/types/ui/overlayLayer.d.ts +24 -0
- package/types/ui/recipe/badgeRecipe.d.ts +7 -3
- package/types/ui/recipe/buttonRecipe.d.ts +7 -3
- package/types/ui/recipe/inputRecipe.d.ts +5 -1
- package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
- package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
- package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
- package/types/vendor/use-agentic/Agentic.d.ts +21 -0
- package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
- package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
- package/types/vendor/use-agentic/index.d.ts +13 -0
- package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
- package/types/vendor/use-agentic/types.d.ts +133 -0
- package/types/vendor/use-agentic/useAgent.d.ts +4 -0
- package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
- package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
- package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
- package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
- package/types/vendor/use-agentic.d.ts +1 -0
- package/types/webkit/index.d.ts +3 -0
- package/types/webkit/lazy.d.ts +12 -0
- package/types/webkit/useCsrValues.d.ts +3 -3
- package/types/webkit/useEscapeKey.d.ts +5 -0
- package/types/webkit/useFrameRuntime.d.ts +6 -1
- package/types/webkit/useScreenScope.d.ts +18 -0
- package/ui/Agent/Approval.tsx +29 -0
- package/ui/Agent/Bubble.tsx +74 -0
- package/ui/Agent/Chat.tsx +225 -0
- package/ui/Agent/Context.tsx +38 -0
- package/ui/Agent/Dock.tsx +72 -0
- package/ui/Agent/Guide.tsx +16 -0
- package/ui/Agent/Section.tsx +24 -0
- package/ui/Agent/StateKey.tsx +44 -0
- package/ui/Agent/Tool.tsx +66 -0
- package/ui/Agent/Transcript.tsx +33 -0
- package/ui/Agent/Zone.tsx +48 -0
- package/ui/Agent/fetchRunner.ts +30 -0
- package/ui/Agent/index.ts +12 -0
- package/ui/Agent/index_.tsx +4 -0
- package/ui/Agent/sessionHistory.ts +33 -0
- package/ui/Badge.tsx +3 -3
- package/ui/BottomSheet.tsx +5 -0
- package/ui/Button.tsx +6 -2
- package/ui/Constant/Doc.tsx +1 -1
- package/ui/CsrImage.tsx +1 -1
- package/ui/Data/ListContainer.tsx +1 -1
- package/ui/DatePicker.tsx +5 -4
- package/ui/Dialog/Modal.tsx +12 -15
- package/ui/DraggableList.tsx +3 -1
- package/ui/Dropdown.tsx +33 -11
- package/ui/Field.tsx +30 -16
- package/ui/Image.tsx +3 -3
- package/ui/Input.tsx +14 -5
- package/ui/Layout/BottomInset.tsx +6 -1
- package/ui/Load/Units.tsx +11 -2
- package/ui/Load/View.tsx +7 -2
- package/ui/Loading/ProgressBar.tsx +8 -1
- package/ui/Menu.tsx +7 -8
- package/ui/Model/EditModal.tsx +37 -2
- package/ui/Model/SureToRemove.tsx +2 -1
- package/ui/Model/index_.tsx +42 -15
- package/ui/ObjectId.tsx +3 -4
- package/ui/Pagination.tsx +3 -4
- package/ui/Popconfirm.tsx +8 -7
- package/ui/Select.tsx +6 -4
- package/ui/Signal/RestApi.tsx +43 -16
- package/ui/Signal/WebSocket.tsx +1 -1
- package/ui/Signal/style.ts +6 -1
- package/ui/Switch.tsx +2 -0
- package/ui/System/CSR.tsx +6 -2
- package/ui/System/ThemeToggle.tsx +55 -33
- package/ui/ToggleSelect.tsx +4 -3
- package/ui/Tooltip.tsx +2 -1
- package/ui/UiOverride/context.ts +14 -2
- package/ui/agentAttrs.ts +19 -0
- package/ui/index.ts +10 -1
- package/ui/overlayLayer.ts +39 -0
- package/ui/recipe/badgeRecipe.ts +22 -3
- package/ui/recipe/buttonRecipe.ts +51 -2
- package/ui/recipe/factory.ts +2 -2
- package/ui/recipe/inputRecipe.ts +18 -4
- package/vendor/use-agentic/AgentProvider.tsx +39 -0
- package/vendor/use-agentic/AgentScope.tsx +20 -0
- package/vendor/use-agentic/AgentSession.ts +292 -0
- package/vendor/use-agentic/Agentic.tsx +44 -0
- package/vendor/use-agentic/AgenticSurface.ts +294 -0
- package/vendor/use-agentic/WIRE.md +57 -0
- package/vendor/use-agentic/httpRunner.ts +115 -0
- package/vendor/use-agentic/index.ts +14 -0
- package/vendor/use-agentic/surfaceContext.ts +9 -0
- package/vendor/use-agentic/types.ts +144 -0
- package/vendor/use-agentic/useAgent.ts +17 -0
- package/vendor/use-agentic/useAgentGuide.ts +16 -0
- package/vendor/use-agentic/useAgentResource.ts +30 -0
- package/vendor/use-agentic/useAgentState.ts +61 -0
- package/vendor/use-agentic/useAgentTool.ts +55 -0
- package/webkit/bootCsr.tsx +2 -33
- package/webkit/index.ts +3 -0
- package/webkit/lazy.tsx +27 -3
- package/webkit/useCsrValues.ts +121 -4
- package/webkit/useEscapeKey.tsx +42 -0
- package/webkit/useFrameRuntime.ts +65 -32
- package/webkit/useScreenScope.tsx +49 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import type { BaseEnv } from "akanjs/base";
|
|
2
|
+
import { Logger } from "akanjs/common";
|
|
3
|
+
import { DictionaryLookup } from "akanjs/dictionary";
|
|
4
|
+
import type { InjectRegistry, LiveRegistry } from "akanjs/service";
|
|
5
|
+
import {
|
|
6
|
+
MCP_LEGACY_VERSION,
|
|
7
|
+
MCP_META_CLIENT_CAPABILITIES,
|
|
8
|
+
MCP_META_PROTOCOL_VERSION,
|
|
9
|
+
MCP_META_SERVER_INFO,
|
|
10
|
+
MCP_SUPPORTED_VERSIONS,
|
|
11
|
+
McpDocument,
|
|
12
|
+
type McpEra,
|
|
13
|
+
McpErrorCode,
|
|
14
|
+
type McpExposedEndpoint,
|
|
15
|
+
type McpJsonRpcRequest,
|
|
16
|
+
McpProgress,
|
|
17
|
+
type McpToolResult,
|
|
18
|
+
} from "../../signal/mcp";
|
|
19
|
+
import type { MiddlewareCls } from "../../signal/middleware";
|
|
20
|
+
import { FetchSerializer } from "../../signal/serializer";
|
|
21
|
+
import type { HttpRoutes } from "../types";
|
|
22
|
+
import { McpAuth, type McpAuthOption } from "./McpAuth";
|
|
23
|
+
import { McpAuthRequiredError, McpDispatcher, McpPromptError } from "./McpDispatcher";
|
|
24
|
+
import { McpEventStream } from "./McpEventStream";
|
|
25
|
+
|
|
26
|
+
export interface McpRouterProps {
|
|
27
|
+
registry: InjectRegistry;
|
|
28
|
+
env: BaseEnv;
|
|
29
|
+
live: LiveRegistry;
|
|
30
|
+
middleware: Map<string, MiddlewareCls>;
|
|
31
|
+
path?: string;
|
|
32
|
+
version?: string;
|
|
33
|
+
/** Free-text usage guidance handed to the model alongside the tool list. */
|
|
34
|
+
instructions?: string;
|
|
35
|
+
/** Extra origins allowed past the DNS-rebinding check, beyond the server's own host. */
|
|
36
|
+
allowedOrigins?: string[];
|
|
37
|
+
readOnly?: boolean;
|
|
38
|
+
/** Entries per catalogue page. A client that wants the whole list follows `nextCursor` until it stops. */
|
|
39
|
+
pageSize?: number;
|
|
40
|
+
/**
|
|
41
|
+
* The one language every title, description and domain error text is resolved in. The catalogue is built once
|
|
42
|
+
* at boot and cached by clients, so it is a server-wide choice rather than a per-request one: `Accept-Language`
|
|
43
|
+
* would mean a document per language, re-deriving every tool schema, for a surface a model reads and a human
|
|
44
|
+
* rarely sees. Defaults to `en`, falling back to the first registered language when the app has no `en`.
|
|
45
|
+
*/
|
|
46
|
+
language?: string;
|
|
47
|
+
auth?: McpAuthOption;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface McpCall {
|
|
51
|
+
id: string | number | null;
|
|
52
|
+
method: string;
|
|
53
|
+
params: Record<string, unknown>;
|
|
54
|
+
era: McpEra;
|
|
55
|
+
req: Request;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface McpErrorOptions {
|
|
59
|
+
status?: number;
|
|
60
|
+
data?: unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface McpCacheHint {
|
|
64
|
+
ttlMs: number;
|
|
65
|
+
cacheScope: "public" | "private";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const notAllowed = () => new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
|
|
69
|
+
const defaultPageSize = 100;
|
|
70
|
+
/**
|
|
71
|
+
* Named rather than left to `DictionaryLookup`'s own fallback, which is whichever language registered first —
|
|
72
|
+
* true today only because dictionaries are written `[en, ko]`, and nothing keeps it true.
|
|
73
|
+
*/
|
|
74
|
+
const defaultLanguage = "en";
|
|
75
|
+
/**
|
|
76
|
+
* The catalogue is fixed for the life of the process, so the ceiling is how long a client may keep serving a
|
|
77
|
+
* list from a server that has since been redeployed. `private` because the listing is filtered per credential
|
|
78
|
+
* (`filterForAccount`): a shared cache would hand one caller another's view of the shelf.
|
|
79
|
+
*/
|
|
80
|
+
const listCache: McpCacheHint = { ttlMs: 300_000, cacheScope: "private" };
|
|
81
|
+
/** Capabilities name what this build implements — they do not vary by caller and change only with the binary. */
|
|
82
|
+
const discoverCache: McpCacheHint = { ttlMs: 3_600_000, cacheScope: "public" };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Serves the app's signals as one MCP endpoint.
|
|
86
|
+
*
|
|
87
|
+
* Answers both protocol eras from the same stateless handler. `2026-07-28` is stateless by design; the legacy
|
|
88
|
+
* revisions only *offer* sessions — a server may decline to issue `Mcp-Session-Id`, and a client that never
|
|
89
|
+
* receives one neither sends one back nor asks to resume a stream. So the legacy era costs an `initialize` reply
|
|
90
|
+
* and a laxer `_meta` check, not a session store. (Measured against Claude Code 2.1.226, which speaks it.)
|
|
91
|
+
*/
|
|
92
|
+
export class McpRouter {
|
|
93
|
+
static readonly logger = new Logger("McpRouter");
|
|
94
|
+
|
|
95
|
+
readonly #props: McpRouterProps;
|
|
96
|
+
readonly #dispatcher: McpDispatcher;
|
|
97
|
+
readonly #auth: McpAuth;
|
|
98
|
+
#document: McpDocument | null = null;
|
|
99
|
+
|
|
100
|
+
constructor(props: McpRouterProps) {
|
|
101
|
+
this.#props = props;
|
|
102
|
+
|
|
103
|
+
this.#dispatcher = new McpDispatcher({ ...props, language: props.language ?? defaultLanguage });
|
|
104
|
+
this.#auth = new McpAuth({ ...props.auth, path: props.path ?? "/mcp" });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
createRoutes(): HttpRoutes {
|
|
108
|
+
return {
|
|
109
|
+
[this.#props.path ?? "/mcp"]: {
|
|
110
|
+
POST: async (req: Request) => this.#cors(req, await this.#post(req)),
|
|
111
|
+
OPTIONS: (req: Request) => this.#preflight(req),
|
|
112
|
+
|
|
113
|
+
GET: (req: Request) => this.#cors(req, notAllowed()),
|
|
114
|
+
DELETE: (req: Request) => this.#cors(req, notAllowed()),
|
|
115
|
+
},
|
|
116
|
+
...this.#auth.createRoutes(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Says once, at boot, what this build actually published — and names every endpoint that was kept out.
|
|
122
|
+
*
|
|
123
|
+
* The rejections are fail-closed by design: an endpoint MCP cannot carry, or whose guards do not admit it, is
|
|
124
|
+
* simply not in the catalogue. That is the right default and the wrong silence, and it matters more now that
|
|
125
|
+
* exposure follows the guards — nobody wrote an opt-in whose absence would explain a missing tool, so this log
|
|
126
|
+
* is the only place the answer exists. A refusal turns on a resolved return type and a resolved guard list, so
|
|
127
|
+
* it reads only from here.
|
|
128
|
+
*
|
|
129
|
+
* An entry published with no description rides here for the same reason: the text every generated entry borrows
|
|
130
|
+
* is a *model* `.desc()`, which no source rule would read as that entry's description. This holds the resolved
|
|
131
|
+
* catalogue, so it can simply look.
|
|
132
|
+
*
|
|
133
|
+
* Called by whatever mounts the router rather than from `createRoutes`, so building a router to answer one
|
|
134
|
+
* request — which tests and tooling do — does not narrate a catalogue nobody asked about.
|
|
135
|
+
*/
|
|
136
|
+
report() {
|
|
137
|
+
try {
|
|
138
|
+
const { tools, prompts, resourceTemplates, refusals, undescribed } = this.#getDocument();
|
|
139
|
+
const counts = `tools=${tools.length} prompts=${prompts.length} resourceTemplates=${resourceTemplates.length}`;
|
|
140
|
+
McpRouter.logger.info(`MCP catalogue: ${counts}${this.#props.readOnly ? " (read-only deployment)" : ""}`);
|
|
141
|
+
|
|
142
|
+
if (!tools.length && !prompts.length)
|
|
143
|
+
McpRouter.logger.warn(
|
|
144
|
+
"MCP is enabled but published nothing. Every candidate was refused; see the reasons below.",
|
|
145
|
+
);
|
|
146
|
+
for (const { key, reason } of refusals) McpRouter.logger.warn(`MCP did not expose "${key}": ${reason}`);
|
|
147
|
+
for (const { key, reason } of undescribed)
|
|
148
|
+
McpRouter.logger.warn(`MCP exposed "${key}" with no description: ${reason}`);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
|
|
151
|
+
McpRouter.logger.warn(
|
|
152
|
+
`MCP catalogue could not be built at boot: ${error instanceof Error ? error.message : error}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Rebuilding per request would re-derive every tool schema on a list agents poll; the set is fixed at boot. */
|
|
158
|
+
#getDocument() {
|
|
159
|
+
if (this.#document) return this.#document;
|
|
160
|
+
const lookup = new DictionaryLookup(this.#props.language ?? defaultLanguage);
|
|
161
|
+
this.#document = new McpDocument(FetchSerializer.serializeRegistry(this.#props.live).signal, {
|
|
162
|
+
resolveDescription: (key) => lookup.text(key),
|
|
163
|
+
readOnly: this.#props.readOnly,
|
|
164
|
+
});
|
|
165
|
+
return this.#document;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async #post(req: Request) {
|
|
169
|
+
if (!this.#originAllowed(req)) return new Response("Forbidden", { status: 403 });
|
|
170
|
+
const rejected = this.#auth.reject(req);
|
|
171
|
+
if (rejected) return rejected;
|
|
172
|
+
let body: McpJsonRpcRequest;
|
|
173
|
+
try {
|
|
174
|
+
body = (await req.json()) as McpJsonRpcRequest;
|
|
175
|
+
} catch {
|
|
176
|
+
return McpRouter.#error(null, McpErrorCode.parse, "Invalid JSON body.", { status: 400 });
|
|
177
|
+
}
|
|
178
|
+
const id = body.id ?? null;
|
|
179
|
+
const method = body.method;
|
|
180
|
+
if (typeof method !== "string")
|
|
181
|
+
return McpRouter.#error(id, McpErrorCode.invalidRequest, "Missing JSON-RPC method.", { status: 400 });
|
|
182
|
+
|
|
183
|
+
if (method.startsWith("notifications/")) return new Response(null, { status: 202 });
|
|
184
|
+
|
|
185
|
+
const params = (body.params ?? {}) as Record<string, unknown>;
|
|
186
|
+
|
|
187
|
+
const meta = McpRouter.#meta(params);
|
|
188
|
+
const era: McpEra = method !== "initialize" && meta && MCP_META_PROTOCOL_VERSION in meta ? "modern" : "legacy";
|
|
189
|
+
const rejection = era === "modern" ? McpRouter.#validateModern(req, method, params, meta ?? {}, id) : null;
|
|
190
|
+
if (rejection) return rejection;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return await this.#dispatch({ id, method, params, era, req });
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error instanceof McpAuthRequiredError) return this.#auth.unauthorized(req);
|
|
196
|
+
|
|
197
|
+
if (error instanceof McpPromptError) return McpRouter.#error(id, error.code, error.message);
|
|
198
|
+
McpRouter.logger.error(
|
|
199
|
+
`MCP ${method} failed: ${error instanceof Error ? (error.stack ?? error.message) : error}`,
|
|
200
|
+
);
|
|
201
|
+
return McpRouter.#error(id, McpErrorCode.internal, "Internal server error.");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async #dispatch(call: McpCall) {
|
|
206
|
+
const document = this.#getDocument();
|
|
207
|
+
switch (call.method) {
|
|
208
|
+
case "initialize":
|
|
209
|
+
|
|
210
|
+
return this.#result(call, {
|
|
211
|
+
protocolVersion: McpRouter.#negotiate(call.params.protocolVersion),
|
|
212
|
+
capabilities: McpRouter.#capabilities(document),
|
|
213
|
+
serverInfo: this.#serverInfo(),
|
|
214
|
+
...(this.#props.instructions ? { instructions: this.#props.instructions } : {}),
|
|
215
|
+
});
|
|
216
|
+
case "ping":
|
|
217
|
+
|
|
218
|
+
return this.#result(call, {});
|
|
219
|
+
case "server/discover":
|
|
220
|
+
return this.#result(
|
|
221
|
+
call,
|
|
222
|
+
{
|
|
223
|
+
supportedVersions: MCP_SUPPORTED_VERSIONS,
|
|
224
|
+
capabilities: McpRouter.#capabilities(document),
|
|
225
|
+
...(this.#props.instructions ? { instructions: this.#props.instructions } : {}),
|
|
226
|
+
},
|
|
227
|
+
discoverCache,
|
|
228
|
+
);
|
|
229
|
+
case "tools/list":
|
|
230
|
+
return await this.#list(call, "tools", document.tools);
|
|
231
|
+
case "resources/list":
|
|
232
|
+
return await this.#list(call, "resources", document.resources);
|
|
233
|
+
case "resources/templates/list":
|
|
234
|
+
return await this.#list(call, "resourceTemplates", document.resourceTemplates);
|
|
235
|
+
case "prompts/list":
|
|
236
|
+
return await this.#list(call, "prompts", document.prompts);
|
|
237
|
+
case "tools/call":
|
|
238
|
+
return await this.#toolsCall(call, document);
|
|
239
|
+
case "prompts/get":
|
|
240
|
+
return await this.#promptsGet(call, document);
|
|
241
|
+
case "resources/read":
|
|
242
|
+
return await this.#resourcesRead(call, document);
|
|
243
|
+
default:
|
|
244
|
+
|
|
245
|
+
return McpRouter.#error(call.id, McpErrorCode.methodNotFound, McpRouter.#methodNotFound(call.method), {
|
|
246
|
+
status: call.era === "modern" ? 404 : 200,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Filter first, then page: an offset has to address the list the caller can actually see. */
|
|
252
|
+
async #list<T extends { name: string }>(call: McpCall, key: string, items: T[]) {
|
|
253
|
+
const visible = await this.#dispatcher.filterForAccount(items, call.req);
|
|
254
|
+
const page = McpRouter.#page(visible, call.params.cursor, this.#props.pageSize ?? defaultPageSize);
|
|
255
|
+
if (!page) return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Invalid cursor.");
|
|
256
|
+
const result = { [key]: page.items, ...(page.nextCursor ? { nextCursor: page.nextCursor } : {}) };
|
|
257
|
+
return this.#result(call, result, listCache);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async #toolsCall(call: McpCall, document: McpDocument) {
|
|
261
|
+
const name = call.params.name;
|
|
262
|
+
if (typeof name !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing tool name.");
|
|
263
|
+
const exposed = document.findTool(name);
|
|
264
|
+
|
|
265
|
+
if (!exposed) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown tool: ${name}.`);
|
|
266
|
+
const args = McpRouter.#arguments(call.params);
|
|
267
|
+
if (!args) return McpRouter.#error(call.id, McpErrorCode.invalidParams, McpRouter.#badArguments);
|
|
268
|
+
const progressToken = McpRouter.#progressToken(call);
|
|
269
|
+
if (progressToken === undefined) return this.#result(call, await this.#dispatcher.call(exposed, args, call.req));
|
|
270
|
+
return await this.#streamedToolCall(call, exposed, args, progressToken);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Runs the tool and only commits to a stream once it actually reports progress.
|
|
275
|
+
*
|
|
276
|
+
* Deciding late is what keeps the failure modes intact: an HTTP status is fixed the moment the response is
|
|
277
|
+
* returned, so a call that opened a stream up front could no longer answer 401 with a `WWW-Authenticate`
|
|
278
|
+
* challenge. Guards run before an endpoint body can report anything, so by the time this switches to SSE the
|
|
279
|
+
* authorization decision has already been made — which is why the streamed path below need not carry one.
|
|
280
|
+
*/
|
|
281
|
+
async #streamedToolCall(
|
|
282
|
+
call: McpCall,
|
|
283
|
+
exposed: McpExposedEndpoint,
|
|
284
|
+
args: Record<string, unknown>,
|
|
285
|
+
progressToken: string | number,
|
|
286
|
+
) {
|
|
287
|
+
const channel = new McpProgress();
|
|
288
|
+
const settled = McpProgress.run(channel, async () => await this.#dispatcher.call(exposed, args, call.req))
|
|
289
|
+
.then(
|
|
290
|
+
(result) => ({ result }),
|
|
291
|
+
(error: unknown) => ({ error }),
|
|
292
|
+
)
|
|
293
|
+
.finally(() => channel.end());
|
|
294
|
+
const reported = await Promise.race([channel.started.then(() => true), settled.then(() => false)]);
|
|
295
|
+
if (!reported) {
|
|
296
|
+
const outcome = await settled;
|
|
297
|
+
if ("error" in outcome) throw outcome.error;
|
|
298
|
+
return this.#result(call, outcome.result);
|
|
299
|
+
}
|
|
300
|
+
const stream = new McpEventStream(() => channel.abort());
|
|
301
|
+
|
|
302
|
+
void this.#pump(call, channel, settled, stream, progressToken).catch((error: unknown) => {
|
|
303
|
+
McpRouter.logger.error(`MCP stream for ${exposed.key} failed: ${error instanceof Error ? error.stack : error}`);
|
|
304
|
+
});
|
|
305
|
+
return stream.response();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async #pump(
|
|
309
|
+
call: McpCall,
|
|
310
|
+
channel: McpProgress,
|
|
311
|
+
settled: Promise<{ result: McpToolResult } | { error: unknown }>,
|
|
312
|
+
stream: McpEventStream,
|
|
313
|
+
progressToken: string | number,
|
|
314
|
+
) {
|
|
315
|
+
try {
|
|
316
|
+
for await (const report of channel.reports())
|
|
317
|
+
stream.write({ jsonrpc: "2.0", method: "notifications/progress", params: { progressToken, ...report } });
|
|
318
|
+
const outcome = await settled;
|
|
319
|
+
|
|
320
|
+
stream.write(
|
|
321
|
+
"error" in outcome
|
|
322
|
+
? McpRouter.#errorBody(call.id, McpErrorCode.internal, "Internal server error.")
|
|
323
|
+
: this.#envelope(call, outcome.result),
|
|
324
|
+
);
|
|
325
|
+
} finally {
|
|
326
|
+
stream.close();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
static readonly #badArguments = "`arguments` must be an object of named values.";
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* `arguments` is an object or it is not there at all; anything else is the caller's own mistake and is reported
|
|
334
|
+
* as one. Coercing it to `{}` ran the call with every argument missing — a tool that takes none then simply
|
|
335
|
+
* succeeded, and one that takes some answered "Missing required argument", which sends a model looking for a
|
|
336
|
+
* value it did send. An array is refused with the rest: MCP names its arguments, and positional ones would
|
|
337
|
+
* silently be read as the properties `0`, `1`, `2`.
|
|
338
|
+
*/
|
|
339
|
+
static #arguments(params: Record<string, unknown>) {
|
|
340
|
+
const args = params.arguments;
|
|
341
|
+
if (args === undefined || args === null) return {};
|
|
342
|
+
if (typeof args !== "object" || Array.isArray(args)) return null;
|
|
343
|
+
return args as Record<string, unknown>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Derived from the catalogue rather than fixed, so a server with no prompts does not invite a `prompts/list`
|
|
348
|
+
* that can only come back empty. Read from the unfiltered document on purpose: a capability says what this
|
|
349
|
+
* build implements, and one that narrowed per credential would contradict itself across a cached handshake.
|
|
350
|
+
*/
|
|
351
|
+
static #capabilities(document: McpDocument) {
|
|
352
|
+
return {
|
|
353
|
+
...(document.tools.length ? { tools: {} } : {}),
|
|
354
|
+
...(document.resources.length || document.resourceTemplates.length ? { resources: {} } : {}),
|
|
355
|
+
...(document.prompts.length ? { prompts: {} } : {}),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* A client asks for progress by naming a token, and can only receive it over a stream — so both have to be
|
|
361
|
+
* true. Only `tools/call` is offered one: a listing is served from memory, and a prompt is a user-triggered
|
|
362
|
+
* read that a client renders as a slash command rather than something it watches run.
|
|
363
|
+
*/
|
|
364
|
+
static #progressToken(call: McpCall) {
|
|
365
|
+
if (!call.req.headers.get("accept")?.includes("text/event-stream")) return undefined;
|
|
366
|
+
const token = McpRouter.#meta(call.params)?.progressToken;
|
|
367
|
+
return typeof token === "string" || typeof token === "number" ? token : undefined;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async #promptsGet(call: McpCall, document: McpDocument) {
|
|
371
|
+
const name = call.params.name;
|
|
372
|
+
if (typeof name !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing prompt name.");
|
|
373
|
+
const found = document.findPrompt(name);
|
|
374
|
+
if (!found) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown prompt: ${name}.`);
|
|
375
|
+
|
|
376
|
+
const args = McpRouter.#arguments(call.params);
|
|
377
|
+
if (!args) return McpRouter.#error(call.id, McpErrorCode.invalidParams, McpRouter.#badArguments);
|
|
378
|
+
const missing = (found.prompt.arguments ?? [])
|
|
379
|
+
.filter((arg) => arg.required && args[arg.name] === undefined)
|
|
380
|
+
.map((arg) => arg.name);
|
|
381
|
+
if (missing.length)
|
|
382
|
+
return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Missing prompt arguments: ${missing.join(", ")}.`);
|
|
383
|
+
const messages = await this.#dispatcher.prompt(found.exposed, args, call.req);
|
|
384
|
+
return this.#result(call, {
|
|
385
|
+
...(found.prompt.description ? { description: found.prompt.description } : {}),
|
|
386
|
+
messages,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async #resourcesRead(call: McpCall, document: McpDocument) {
|
|
391
|
+
const uri = call.params.uri;
|
|
392
|
+
if (typeof uri !== "string") return McpRouter.#error(call.id, McpErrorCode.invalidParams, "Missing resource uri.");
|
|
393
|
+
const resolved = document.resolveResource(uri);
|
|
394
|
+
if (!resolved) return McpRouter.#error(call.id, McpErrorCode.invalidParams, `Unknown resource: ${uri}.`);
|
|
395
|
+
const result = await this.#dispatcher.call(resolved.exposed, resolved.args, call.req);
|
|
396
|
+
|
|
397
|
+
if (result.isError)
|
|
398
|
+
return McpRouter.#error(call.id, McpErrorCode.invalidParams, result.content[0]?.text ?? "Read failed.");
|
|
399
|
+
return this.#result(call, {
|
|
400
|
+
contents: [{ uri, mimeType: "application/json", text: result.content[0]?.text ?? "null" }],
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Cross-origin gate. MCP clients are not browsers and normally send no `Origin` at all, so absence is allowed;
|
|
406
|
+
* a present one has to name this server or an explicitly configured peer.
|
|
407
|
+
*
|
|
408
|
+
* Matching against our own host stops a page served from somewhere else, and that is all it stops — a rebinding
|
|
409
|
+
* attack points its *own* name at this server, so the `Origin` it sends and the host it arrives on agree and it
|
|
410
|
+
* passes. `allowedOrigins` is the list that actually decides who may drive this server from a browser; leave it
|
|
411
|
+
* unset unless a browser-hosted client needs it.
|
|
412
|
+
*/
|
|
413
|
+
#originAllowed(req: Request) {
|
|
414
|
+
const origin = req.headers.get("origin");
|
|
415
|
+
if (!origin) return true;
|
|
416
|
+
if ((this.#props.allowedOrigins ?? []).includes(origin)) return true;
|
|
417
|
+
try {
|
|
418
|
+
|
|
419
|
+
return new URL(origin).host === new URL(McpAuth.origin(req)).host;
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Answers the preflight a browser-hosted client is forced to make. `content-type: application/json` and the
|
|
427
|
+
* `mcp-*` mirror headers each put the call past the simple-request bar, so without this the request never
|
|
428
|
+
* leaves the browser and `allowedOrigins` grants nothing it can use.
|
|
429
|
+
*
|
|
430
|
+
* The requested header list is echoed rather than fixed: the decision that matters is the origin, and a fixed
|
|
431
|
+
* list only breaks clients that send one more header than we predicted.
|
|
432
|
+
*/
|
|
433
|
+
#preflight(req: Request) {
|
|
434
|
+
if (!req.headers.get("origin") || !this.#originAllowed(req)) return new Response("Forbidden", { status: 403 });
|
|
435
|
+
return this.#cors(
|
|
436
|
+
req,
|
|
437
|
+
new Response(null, {
|
|
438
|
+
status: 204,
|
|
439
|
+
headers: {
|
|
440
|
+
"access-control-allow-methods": "POST, OPTIONS",
|
|
441
|
+
"access-control-allow-headers":
|
|
442
|
+
req.headers.get("access-control-request-headers") ?? "authorization, content-type",
|
|
443
|
+
"access-control-max-age": "600",
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Grants the one origin that already passed `#originAllowed`, so the rebinding defence stays exactly as wide
|
|
451
|
+
* as it was. Never `access-control-allow-credentials`: an MCP client presents its bearer token in a header it
|
|
452
|
+
* sets deliberately, and allowing ambient cookies is what would turn a permitted origin into one that can ride
|
|
453
|
+
* a signed-in user's session.
|
|
454
|
+
*/
|
|
455
|
+
#cors(req: Request, res: Response) {
|
|
456
|
+
const origin = req.headers.get("origin");
|
|
457
|
+
if (!origin || !this.#originAllowed(req)) return res;
|
|
458
|
+
res.headers.set("access-control-allow-origin", origin);
|
|
459
|
+
res.headers.set("vary", "origin");
|
|
460
|
+
return res;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#serverInfo() {
|
|
464
|
+
return { name: `${this.#props.env.appName}-mcp`, version: this.#props.version ?? "0.0.0" };
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#result(call: McpCall, result: object, cache?: McpCacheHint) {
|
|
468
|
+
return Response.json(this.#envelope(call, result, cache));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#envelope(call: McpCall, result: object, cache?: McpCacheHint) {
|
|
472
|
+
|
|
473
|
+
const meta =
|
|
474
|
+
call.era === "modern"
|
|
475
|
+
? { resultType: "complete", _meta: { [MCP_META_SERVER_INFO]: this.#serverInfo() }, ...cache }
|
|
476
|
+
: {};
|
|
477
|
+
return { jsonrpc: "2.0", id: call.id, result: { ...meta, ...result } };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* The cursor is an offset into the filtered list, base64url-wrapped so a client treats it as opaque rather
|
|
482
|
+
* than arithmetic it may do itself. The catalogue is built once at boot, so an offset stays meaningful for the
|
|
483
|
+
* life of the process; one minted by an earlier process may address a position that no longer exists, and that
|
|
484
|
+
* is refused rather than clamped — a silently shortened page reads as "the list ends here".
|
|
485
|
+
*/
|
|
486
|
+
static #page<T>(items: T[], cursor: unknown, size: number) {
|
|
487
|
+
const offset = McpRouter.#offset(cursor);
|
|
488
|
+
if (offset === null || offset > items.length) return null;
|
|
489
|
+
const next = offset + size;
|
|
490
|
+
return {
|
|
491
|
+
items: items.slice(offset, next),
|
|
492
|
+
...(next < items.length ? { nextCursor: Buffer.from(String(next)).toString("base64url") } : {}),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
static #offset(cursor: unknown) {
|
|
497
|
+
if (cursor === undefined || cursor === null) return 0;
|
|
498
|
+
if (typeof cursor !== "string") return null;
|
|
499
|
+
const decoded = Buffer.from(cursor, "base64url").toString("utf8");
|
|
500
|
+
|
|
501
|
+
if (!decoded) return null;
|
|
502
|
+
const offset = Number(decoded);
|
|
503
|
+
return Number.isInteger(offset) && offset >= 0 ? offset : null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
static #meta(params: Record<string, unknown>) {
|
|
507
|
+
const meta = params._meta;
|
|
508
|
+
return meta && typeof meta === "object" ? (meta as Record<string, unknown>) : null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Legacy clients propose a version; answer with theirs when we speak it, and otherwise with whichever end of our
|
|
513
|
+
* list they are likelier to accept.
|
|
514
|
+
*
|
|
515
|
+
* A client proposes the newest revision *it* speaks, so an unknown proposal is either newer than everything here
|
|
516
|
+
* — in which case the newest we have is the closest thing it may still know — or older than everything here, in
|
|
517
|
+
* which case the newest is hopeless and the oldest is the only candidate. The spec's "SHOULD be the latest
|
|
518
|
+
* version supported by the server" covers the first case and would strand the second, which is the one an old
|
|
519
|
+
* client is actually in. Revision names are ISO dates, so they order as strings.
|
|
520
|
+
*/
|
|
521
|
+
static #negotiate(requested: unknown) {
|
|
522
|
+
if (typeof requested !== "string") return MCP_LEGACY_VERSION;
|
|
523
|
+
if (MCP_SUPPORTED_VERSIONS.includes(requested as (typeof MCP_SUPPORTED_VERSIONS)[number])) return requested;
|
|
524
|
+
const [newest] = MCP_SUPPORTED_VERSIONS;
|
|
525
|
+
return requested > newest ? newest : MCP_SUPPORTED_VERSIONS[MCP_SUPPORTED_VERSIONS.length - 1];
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Legacy clients have no way to fall forward, so the one diagnostic they get is this message. */
|
|
529
|
+
static #methodNotFound(method: string) {
|
|
530
|
+
return `Method not found: ${method}. This server speaks MCP ${MCP_SUPPORTED_VERSIONS.join(", ")}.`;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Modern requests mirror parts of the body into headers so a proxy can route and audit without parsing JSON.
|
|
535
|
+
* A mismatch is rejected rather than resolved in the body's favour: whatever a gateway in front of us allowed
|
|
536
|
+
* was decided from the header, so honouring a body that disagrees is how that check gets bypassed. A mirror
|
|
537
|
+
* that is simply absent is refused on the same ground — see `#headerMismatch`.
|
|
538
|
+
*/
|
|
539
|
+
static #validateModern(
|
|
540
|
+
req: Request,
|
|
541
|
+
method: string,
|
|
542
|
+
params: Record<string, unknown>,
|
|
543
|
+
meta: Record<string, unknown>,
|
|
544
|
+
id: string | number | null,
|
|
545
|
+
) {
|
|
546
|
+
const version = meta[MCP_META_PROTOCOL_VERSION];
|
|
547
|
+
if (typeof version !== "string" || !(MCP_META_CLIENT_CAPABILITIES in meta))
|
|
548
|
+
return McpRouter.#error(id, McpErrorCode.invalidParams, "Missing required `_meta` fields.", { status: 400 });
|
|
549
|
+
if (!MCP_SUPPORTED_VERSIONS.includes(version as (typeof MCP_SUPPORTED_VERSIONS)[number]))
|
|
550
|
+
return McpRouter.#error(id, McpErrorCode.unsupportedProtocolVersion, "Unsupported protocol version.", {
|
|
551
|
+
status: 400,
|
|
552
|
+
data: { requested: version, supported: MCP_SUPPORTED_VERSIONS },
|
|
553
|
+
});
|
|
554
|
+
const named = params.name ?? params.uri;
|
|
555
|
+
const mismatch =
|
|
556
|
+
McpRouter.#headerMismatch(req, "mcp-protocol-version", version) ??
|
|
557
|
+
McpRouter.#headerMismatch(req, "mcp-method", method) ??
|
|
558
|
+
(typeof named === "string" ? McpRouter.#headerMismatch(req, "mcp-name", named) : undefined);
|
|
559
|
+
return mismatch ? McpRouter.#error(id, McpErrorCode.headerMismatch, mismatch, { status: 400 }) : null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
static #headerMismatch(req: Request, header: string, expected: string) {
|
|
563
|
+
const raw = req.headers.get(header);
|
|
564
|
+
|
|
565
|
+
if (raw === null) return `Header \`${header}\` is required by this protocol version and was not sent.`;
|
|
566
|
+
return McpRouter.#decodeHeader(raw) === expected ? undefined : `Header \`${header}\` does not match the body.`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Values that are not ASCII-safe travel wrapped in a lowercase base64 sentinel: `=?base64?…?=`. */
|
|
570
|
+
static #decodeHeader(value: string) {
|
|
571
|
+
if (!value.startsWith("=?base64?") || !value.endsWith("?=")) return value;
|
|
572
|
+
try {
|
|
573
|
+
return Buffer.from(value.slice(9, -2), "base64").toString("utf8");
|
|
574
|
+
} catch {
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* A JSON-RPC error body is what tells a client that a 400 or 404 came from an MCP server rather than from a
|
|
581
|
+
* proxy in front of it, so the status never travels alone. Tool-level failures stay at 200 and are carried in
|
|
582
|
+
* the JSON-RPC error itself; only envelope problems escalate to an HTTP status.
|
|
583
|
+
*/
|
|
584
|
+
static #error(id: string | number | null, code: number, message: string, { status, data }: McpErrorOptions = {}) {
|
|
585
|
+
return Response.json(McpRouter.#errorBody(id, code, message, data), { status: status ?? 200 });
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
static #errorBody(id: string | number | null, code: number, message: string, data?: unknown) {
|
|
589
|
+
return { jsonrpc: "2.0", id, error: { code, message, ...(data !== undefined ? { data } : {}) } };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
@@ -186,7 +186,7 @@ export class CascadeRunner {
|
|
|
186
186
|
}
|
|
187
187
|
if (!lines.length) return;
|
|
188
188
|
const bulk = lines.filter((line) => line.endsWith("(bulk)")).length;
|
|
189
|
-
this.#logger.
|
|
189
|
+
this.#logger.verbose(`${lines.length} cascade edge(s), ${bulk} in one query`);
|
|
190
190
|
for (const line of lines) this.#logger.verbose(line);
|
|
191
191
|
}
|
|
192
192
|
|