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,286 @@
|
|
|
1
|
+
import { capitalize, isMcpDescribableArg, mcpHintsOf, mcpRefusalOf } from "akanjs/common";
|
|
2
|
+
import { FetchClient } from "akanjs/fetch";
|
|
3
|
+
import { type AgentCandidate, AgentCatalogue, type AgentRefusal, type AgentUndescribed } from "../agent";
|
|
4
|
+
import { type JsonSchema, JsonSchemaBuilder } from "../schema";
|
|
5
|
+
import type { SerializedArg, SerializedEndpoint, SerializedSignal } from "../types";
|
|
6
|
+
import { McpUriTemplate } from "./McpUriTemplate";
|
|
7
|
+
import type { McpPrompt, McpResource, McpResourceTemplate, McpTool, McpToolAnnotations } from "./mcpProtocol";
|
|
8
|
+
|
|
9
|
+
export interface McpDocumentOptions {
|
|
10
|
+
resolveDescription?: (key: string) => string | undefined;
|
|
11
|
+
excludeSignals?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Drops every mutation from the catalogue regardless of what it opted into. Off by default: the endpoint's own
|
|
14
|
+
* an endpoint's guards are the decision, and a second switch that silently unlists a published endpoint gives
|
|
15
|
+
* its author no way to see why. This is the read-only-deployment valve.
|
|
16
|
+
*/
|
|
17
|
+
readOnly?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface McpExposedEndpoint {
|
|
21
|
+
refName: string;
|
|
22
|
+
key: string;
|
|
23
|
+
endpoint: SerializedEndpoint;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A candidate this catalogue did not publish. The bookkeeping is audience-independent and lives on
|
|
28
|
+
* `AgentCatalogue`; these are the MCP names for it.
|
|
29
|
+
*/
|
|
30
|
+
export type McpRefusal = AgentRefusal;
|
|
31
|
+
export type McpUndescribed = AgentUndescribed;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Turns the serialized signal registry into the three MCP catalogues and answers the lookups `tools/call` and
|
|
35
|
+
* `resources/read` need. Pure: no IO and no DI — the sibling of `createOpenApiDocument`.
|
|
36
|
+
*/
|
|
37
|
+
export class McpDocument {
|
|
38
|
+
/**
|
|
39
|
+
* An array cannot be `structuredContent` — the spec types it as an object — so a list result is wrapped under
|
|
40
|
+
* this key and `outputSchema` is shaped to match. Both halves must agree, which is why they live together.
|
|
41
|
+
*/
|
|
42
|
+
static readonly listKey = "items";
|
|
43
|
+
|
|
44
|
+
readonly tools: McpTool[];
|
|
45
|
+
readonly prompts: McpPrompt[];
|
|
46
|
+
readonly resourceTemplates: McpResourceTemplate[];
|
|
47
|
+
/** Every readable thing is addressed by a template, so there are no fixed resources to enumerate. */
|
|
48
|
+
readonly resources: McpResource[] = [];
|
|
49
|
+
/**
|
|
50
|
+
* Every candidate that was not published, with the sentence saying why — the rejections are fail-closed by
|
|
51
|
+
* design, and an author whose endpoint is missing otherwise has nowhere to look but the framework source.
|
|
52
|
+
*/
|
|
53
|
+
readonly refusals: McpRefusal[];
|
|
54
|
+
/** What is published with no description of its own, which is the field a model picks a tool by. */
|
|
55
|
+
readonly undescribed: McpUndescribed[];
|
|
56
|
+
readonly #schema = new JsonSchemaBuilder({ refPrefix: "#/$defs/" });
|
|
57
|
+
#allSchemas: Record<string, JsonSchema> | null = null;
|
|
58
|
+
#readSchemas: Record<string, JsonSchema> | null = null;
|
|
59
|
+
readonly #options: McpDocumentOptions;
|
|
60
|
+
readonly #catalogue: AgentCatalogue;
|
|
61
|
+
readonly #byToolName = new Map<string, McpExposedEndpoint>();
|
|
62
|
+
readonly #byPromptName = new Map<string, { exposed: McpExposedEndpoint; prompt: McpPrompt }>();
|
|
63
|
+
/** Keyed by endpoint key: what is addressable, and by exactly which uri. */
|
|
64
|
+
readonly #templates = new Map<string, string>();
|
|
65
|
+
|
|
66
|
+
constructor(serializedSignal: Record<string, SerializedSignal>, options: McpDocumentOptions = {}) {
|
|
67
|
+
this.#options = options;
|
|
68
|
+
this.#catalogue = new AgentCatalogue(options);
|
|
69
|
+
const { tools, prompts } = this.#collect(serializedSignal);
|
|
70
|
+
this.refusals = this.#catalogue.refusals;
|
|
71
|
+
this.tools = tools.map((item) => this.#tool(item));
|
|
72
|
+
this.prompts = prompts.map((item) => {
|
|
73
|
+
const prompt = this.#prompt(item);
|
|
74
|
+
this.#byPromptName.set(item.key, { exposed: item, prompt });
|
|
75
|
+
return prompt;
|
|
76
|
+
});
|
|
77
|
+
this.resourceTemplates = tools.flatMap((item) => {
|
|
78
|
+
const uriTemplate = this.#templates.get(item.key);
|
|
79
|
+
return uriTemplate ? [this.#template(item, uriTemplate)] : [];
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.undescribed = this.#catalogue.undescribed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
findTool(name: string): McpExposedEndpoint | undefined {
|
|
86
|
+
return this.#byToolName.get(name);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns the catalogue entry alongside the endpoint: `prompts/get` validates against the published one. */
|
|
90
|
+
findPrompt(name: string) {
|
|
91
|
+
return this.#byPromptName.get(name);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolves a URI only when the endpoint behind it was published as a template. An endpoint that was never
|
|
96
|
+
* advertised has to fail here rather than fall through to its guards: opting out of MCP means keeping a thing
|
|
97
|
+
* off the wire entirely, not merely making it refuse.
|
|
98
|
+
*/
|
|
99
|
+
resolveResource(uri: string) {
|
|
100
|
+
const target = McpUriTemplate.parse(uri);
|
|
101
|
+
if (!target || !this.#templates.has(target.endpointKey)) return null;
|
|
102
|
+
const exposed = this.#byToolName.get(target.endpointKey);
|
|
103
|
+
return exposed ? { exposed, args: target.args } : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
static structuredContent(endpoint: SerializedEndpoint, value: unknown) {
|
|
107
|
+
if (!endpoint.returns.modelType) return undefined;
|
|
108
|
+
if (endpoint.returns.arrDepth) return { [McpDocument.listKey]: value };
|
|
109
|
+
|
|
110
|
+
return value === null || value === undefined ? undefined : value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Which of the catalogue's candidates this audience takes, and in what shape.
|
|
115
|
+
*
|
|
116
|
+
* The enumeration and the naming policy are `AgentCatalogue`'s — every audience walks the same registry and
|
|
117
|
+
* holds one name per entry. What is MCP's own is only this: that every candidate is published unless a rule
|
|
118
|
+
* refuses it, and that a published one becomes a tool, a prompt, and sometimes an addressable uri.
|
|
119
|
+
*/
|
|
120
|
+
#collect(serializedSignal: Record<string, SerializedSignal>): {
|
|
121
|
+
tools: McpExposedEndpoint[];
|
|
122
|
+
prompts: McpExposedEndpoint[];
|
|
123
|
+
} {
|
|
124
|
+
const tools: McpExposedEndpoint[] = [];
|
|
125
|
+
const prompts: McpExposedEndpoint[] = [];
|
|
126
|
+
for (const candidate of AgentCatalogue.candidates(serializedSignal, {
|
|
127
|
+
excludeSignals: this.#options.excludeSignals,
|
|
128
|
+
})) {
|
|
129
|
+
const item: McpExposedEndpoint = {
|
|
130
|
+
refName: candidate.refName,
|
|
131
|
+
key: candidate.key,
|
|
132
|
+
endpoint: candidate.endpoint,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const reason = mcpRefusalOf(item.endpoint, { readOnly: this.#options.readOnly });
|
|
136
|
+
if (reason) {
|
|
137
|
+
this.#catalogue.refuse(item.key, reason);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (!this.#catalogue.claim(item.key)) continue;
|
|
141
|
+
if (item.endpoint.type === "prompt") {
|
|
142
|
+
|
|
143
|
+
prompts.push(item);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const uriTemplate = McpDocument.#addressable(candidate)
|
|
148
|
+
? McpDocument.#uriTemplate(item.refName, item.key, item.endpoint)
|
|
149
|
+
: undefined;
|
|
150
|
+
this.#byToolName.set(item.key, item);
|
|
151
|
+
if (uriTemplate) this.#templates.set(item.key, uriTemplate);
|
|
152
|
+
tools.push(item);
|
|
153
|
+
}
|
|
154
|
+
return { tools, prompts };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether this candidate is one of the generated reads that has an `akan://` address.
|
|
159
|
+
*
|
|
160
|
+
* Only the list side of a slice is addressable; an insight is an aggregate with nothing to point a URI at, and a
|
|
161
|
+
* custom endpoint has no generated key shape to build one from.
|
|
162
|
+
*/
|
|
163
|
+
static #addressable({ origin, refName, key, baseVerb }: AgentCandidate): boolean {
|
|
164
|
+
if (origin === "base") return baseVerb === "get";
|
|
165
|
+
if (origin === "slice") return key.startsWith(`${refName}List`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
#tool({ refName, key, endpoint }: McpExposedEndpoint): McpTool {
|
|
170
|
+
const { paramArgs, searchArgs, bodyArgs } = FetchClient.classifyHttpArgs(endpoint.args);
|
|
171
|
+
|
|
172
|
+
const args = [...paramArgs, ...searchArgs, ...bodyArgs].filter(isMcpDescribableArg);
|
|
173
|
+
const properties = Object.fromEntries(args.map((arg) => [arg.name, this.#argSchema(refName, key, arg)]));
|
|
174
|
+
|
|
175
|
+
const required = [...paramArgs, ...bodyArgs]
|
|
176
|
+
.filter((arg) => isMcpDescribableArg(arg) && !arg.nullable)
|
|
177
|
+
.map((arg) => arg.name);
|
|
178
|
+
const outputSchema = this.#outputSchema(endpoint);
|
|
179
|
+
return {
|
|
180
|
+
name: key,
|
|
181
|
+
...this.#catalogue.entryTexts(refName, key),
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
properties,
|
|
185
|
+
...(required.length ? { required } : {}),
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
...this.#defs(properties),
|
|
188
|
+
},
|
|
189
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
190
|
+
annotations: mcpHintsOf(key, endpoint) satisfies McpToolAnnotations,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A prompt's arguments are a flat string map on the wire, so there is no schema to publish — only names,
|
|
196
|
+
* descriptions and which ones must be filled. A `param` is a path segment and always required; a `search` is
|
|
197
|
+
* the only way to declare an optional one.
|
|
198
|
+
*/
|
|
199
|
+
#prompt({ refName, key, endpoint }: McpExposedEndpoint): McpPrompt {
|
|
200
|
+
const args = endpoint.args.filter((arg) => arg.type === "param" || arg.type === "search");
|
|
201
|
+
return {
|
|
202
|
+
name: key,
|
|
203
|
+
...this.#catalogue.entryTexts(refName, key),
|
|
204
|
+
...(args.length
|
|
205
|
+
? {
|
|
206
|
+
arguments: args.map((arg) => {
|
|
207
|
+
const description = this.#options.resolveDescription?.(`${refName}.signal.${key}.arg.${arg.name}.desc`);
|
|
208
|
+
return {
|
|
209
|
+
name: arg.name,
|
|
210
|
+
...(description ? { description } : {}),
|
|
211
|
+
required: arg.type === "param",
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
}
|
|
215
|
+
: {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#outputSchema(endpoint: SerializedEndpoint) {
|
|
220
|
+
|
|
221
|
+
if (!endpoint.returns.modelType) return undefined;
|
|
222
|
+
|
|
223
|
+
if (endpoint.returns.nullable && !endpoint.returns.arrDepth) return undefined;
|
|
224
|
+
const returns = this.#schema.returns(endpoint.returns);
|
|
225
|
+
const schema = endpoint.returns.arrDepth
|
|
226
|
+
? {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: { [McpDocument.listKey]: returns },
|
|
229
|
+
required: [McpDocument.listKey],
|
|
230
|
+
additionalProperties: false,
|
|
231
|
+
}
|
|
232
|
+
: returns;
|
|
233
|
+
|
|
234
|
+
return { ...schema, ...this.#defs(schema, { readable: true }) };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
#defs(seed: unknown, { readable = false } = {}) {
|
|
238
|
+
const defs = this.#schema.referencedSchemas(seed, this.#modelSchemas(readable));
|
|
239
|
+
|
|
240
|
+
return Object.keys(defs).length ? { $defs: defs } : {};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Every registered model, built once per shape for the whole document. Narrowing runs twice per tool — input
|
|
245
|
+
* schema and output schema — so deriving the full set inside each call rebuilt every model in the app 2N times.
|
|
246
|
+
*/
|
|
247
|
+
#modelSchemas(readable: boolean) {
|
|
248
|
+
if (!readable) {
|
|
249
|
+
this.#allSchemas ??= this.#schema.allModelSchemas();
|
|
250
|
+
return this.#allSchemas;
|
|
251
|
+
}
|
|
252
|
+
this.#readSchemas ??= this.#schema.allModelSchemas({ readable: true });
|
|
253
|
+
return this.#readSchemas;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
#argSchema(refName: string, key: string, arg: SerializedArg) {
|
|
257
|
+
const description = this.#options.resolveDescription?.(`${refName}.signal.${key}.arg.${arg.name}.desc`);
|
|
258
|
+
return {
|
|
259
|
+
...this.#schema.arg(arg),
|
|
260
|
+
...(description ? { description } : {}),
|
|
261
|
+
...(arg.example !== undefined ? { examples: [arg.example] } : {}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
#template({ refName, key }: McpExposedEndpoint, uriTemplate: string): McpResourceTemplate {
|
|
266
|
+
return {
|
|
267
|
+
uriTemplate,
|
|
268
|
+
name: key,
|
|
269
|
+
...this.#catalogue.entryTexts(refName, key),
|
|
270
|
+
mimeType: "application/json",
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
static #uriTemplate(refName: string, key: string, endpoint: SerializedEndpoint) {
|
|
275
|
+
if (key === refName) return McpUriTemplate.model(refName);
|
|
276
|
+
if (key === `light${capitalize(refName)}`) return McpUriTemplate.light(refName);
|
|
277
|
+
const listPrefix = `${refName}List`;
|
|
278
|
+
if (!key.startsWith(listPrefix)) return undefined;
|
|
279
|
+
const suffix = key.slice(listPrefix.length);
|
|
280
|
+
|
|
281
|
+
const argNames = endpoint.args
|
|
282
|
+
.filter((arg) => (arg.type === "param" || arg.type === "search") && isMcpDescribableArg(arg))
|
|
283
|
+
.map((arg) => arg.name);
|
|
284
|
+
return McpUriTemplate.list(refName, suffix ? `${suffix.charAt(0).toLowerCase()}${suffix.slice(1)}` : "", argNames);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
export interface McpProgressOption {
|
|
4
|
+
/** Denominator for the ratio a client renders. Omit when the amount of work is not known up front. */
|
|
5
|
+
total?: number;
|
|
6
|
+
/** One short line describing the current step. Shown to the user, so keep it prose rather than a status code. */
|
|
7
|
+
message?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface McpProgressReport extends McpProgressOption {
|
|
11
|
+
progress: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The progress channel for one streamed MCP call.
|
|
16
|
+
*
|
|
17
|
+
* Reached through `AsyncLocalStorage` rather than a parameter so an endpoint reports progress from wherever the
|
|
18
|
+
* work actually happens — a service, an adapter, a loop several frames down — without every signature between
|
|
19
|
+
* here and there growing a channel argument. Outside a streamed call `report` is a no-op, so the same endpoint
|
|
20
|
+
* code runs unchanged over plain HTTP, a websocket, or a test.
|
|
21
|
+
*/
|
|
22
|
+
export class McpProgress {
|
|
23
|
+
static readonly #storage = new AsyncLocalStorage<McpProgress>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Reports progress for the call running on this stack. Silent when nothing is listening, which is every call
|
|
27
|
+
* that did not ask for a stream.
|
|
28
|
+
*/
|
|
29
|
+
static report(progress: number, option: McpProgressOption = {}) {
|
|
30
|
+
const channel = McpProgress.#storage.getStore();
|
|
31
|
+
if (channel) channel.#push(progress, option);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True while the caller is streaming, so an expensive progress message can be skipped when nobody reads it. */
|
|
35
|
+
static get streaming() {
|
|
36
|
+
return !!McpProgress.#storage.getStore();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static async run<T>(channel: McpProgress, exec: () => Promise<T>): Promise<T> {
|
|
40
|
+
return await McpProgress.#storage.run(channel, exec);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
readonly #queue: McpProgressReport[] = [];
|
|
44
|
+
readonly #abort = new AbortController();
|
|
45
|
+
#wake: (() => void) | null = null;
|
|
46
|
+
#start: (() => void) | null = null;
|
|
47
|
+
#ended = false;
|
|
48
|
+
|
|
49
|
+
/** Resolves on the first report and never otherwise — a call that reports nothing has nothing to stream. */
|
|
50
|
+
readonly started: Promise<void>;
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
this.started = new Promise<void>((resolve) => {
|
|
54
|
+
this.#start = resolve;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Aborted when the client closes the response stream, which is how cancellation is signalled over this
|
|
60
|
+
* transport. Long-running work may watch it; the framework cannot force an `exec` already in flight to stop.
|
|
61
|
+
*/
|
|
62
|
+
get signal() {
|
|
63
|
+
return this.#abort.signal;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Yields every report until the call finishes. Buffered ones come out first, so none is lost to a late reader. */
|
|
67
|
+
async *reports(): AsyncGenerator<McpProgressReport> {
|
|
68
|
+
|
|
69
|
+
while (!this.#ended || this.#queue.length) {
|
|
70
|
+
for (const report of this.#queue.splice(0)) yield report;
|
|
71
|
+
|
|
72
|
+
if (this.#ended || this.#queue.length) continue;
|
|
73
|
+
await new Promise<void>((resolve) => {
|
|
74
|
+
this.#wake = resolve;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
end() {
|
|
80
|
+
this.#ended = true;
|
|
81
|
+
this.#release();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
abort() {
|
|
85
|
+
this.#abort.abort();
|
|
86
|
+
this.end();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#push(progress: number, { total, message }: McpProgressOption) {
|
|
90
|
+
|
|
91
|
+
if (this.#ended) return;
|
|
92
|
+
this.#queue.push({
|
|
93
|
+
progress,
|
|
94
|
+
...(total === undefined ? {} : { total }),
|
|
95
|
+
...(message ? { message } : {}),
|
|
96
|
+
});
|
|
97
|
+
this.#start?.();
|
|
98
|
+
this.#start = null;
|
|
99
|
+
this.#release();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#release() {
|
|
103
|
+
this.#wake?.();
|
|
104
|
+
this.#wake = null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { capitalize } from "akanjs/common";
|
|
2
|
+
|
|
3
|
+
export interface McpResourceTarget {
|
|
4
|
+
endpointKey: string;
|
|
5
|
+
args: Record<string, string | string[]>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Two-way map between an `akan://` resource URI and the endpoint that answers it.
|
|
10
|
+
*
|
|
11
|
+
* Parsing is done by hand rather than through `URL`: `akan:` is a non-special scheme, and how a runtime
|
|
12
|
+
* normalizes the authority of one (case, percent-decoding) is exactly where a camelCase refName like
|
|
13
|
+
* `agentSession` would quietly stop matching its model.
|
|
14
|
+
*/
|
|
15
|
+
export class McpUriTemplate {
|
|
16
|
+
static readonly scheme = "akan";
|
|
17
|
+
/** Reserved second segment: a model id may never take one of these values, and none is a valid ObjectId. */
|
|
18
|
+
static readonly #reserved = new Set(["light", "list"]);
|
|
19
|
+
|
|
20
|
+
static model(refName: string) {
|
|
21
|
+
return `${McpUriTemplate.scheme}://${refName}/{${refName}Id}`;
|
|
22
|
+
}
|
|
23
|
+
static light(refName: string) {
|
|
24
|
+
return `${McpUriTemplate.scheme}://${refName}/light/{${refName}Id}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The model's own unfiltered list is the bare `…/list`, never `…/list/<token>`. A named slice occupies the
|
|
28
|
+
* third segment, and a slice key is an author-chosen identifier — so any token put there for the root list
|
|
29
|
+
* would be one a slice could also be called, and the two would publish the same uri with only one of them
|
|
30
|
+
* readable. `list` in the *second* segment is already reserved against a model id, so there is nothing to
|
|
31
|
+
* collide with here.
|
|
32
|
+
*/
|
|
33
|
+
static list(refName: string, sliceKey: string, argNames: string[]) {
|
|
34
|
+
const base = `${McpUriTemplate.scheme}://${refName}/list${sliceKey ? `/${sliceKey}` : ""}`;
|
|
35
|
+
return argNames.length ? `${base}{?${argNames.join(",")}}` : base;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static parse(uri: string): McpResourceTarget | null {
|
|
39
|
+
const authority = `${McpUriTemplate.scheme}://`;
|
|
40
|
+
if (!uri.startsWith(authority)) return null;
|
|
41
|
+
const rest = uri.slice(authority.length);
|
|
42
|
+
const queryAt = rest.indexOf("?");
|
|
43
|
+
const segments = (queryAt === -1 ? rest : rest.slice(0, queryAt)).split("/");
|
|
44
|
+
if (segments.some((segment) => !segment)) return null;
|
|
45
|
+
const search = new URLSearchParams(queryAt === -1 ? "" : rest.slice(queryAt + 1));
|
|
46
|
+
const decoded = McpUriTemplate.#decode(segments);
|
|
47
|
+
if (!decoded) return null;
|
|
48
|
+
const [refName, second, third] = decoded as [string, string?, string?];
|
|
49
|
+
|
|
50
|
+
if (segments.length === 2 && second && !McpUriTemplate.#reserved.has(second))
|
|
51
|
+
return { endpointKey: refName, args: { [`${refName}Id`]: second } };
|
|
52
|
+
if (segments.length === 2 && second === "list")
|
|
53
|
+
return { endpointKey: `${refName}List`, args: McpUriTemplate.#searchArgs(search) };
|
|
54
|
+
if (segments.length === 3 && second === "light" && third)
|
|
55
|
+
return { endpointKey: `light${capitalize(refName)}`, args: { [`${refName}Id`]: third } };
|
|
56
|
+
if (segments.length === 3 && second === "list" && third)
|
|
57
|
+
return { endpointKey: `${refName}List${capitalize(third)}`, args: McpUriTemplate.#searchArgs(search) };
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A percent escape the decoder rejects (`akan://banner/%`) makes the uri unreadable, which is the same answer as
|
|
63
|
+
* a uri naming nothing: `resources/read` says `Unknown resource`. Left to throw, `decodeURIComponent`'s `URIError`
|
|
64
|
+
* reached the router's catch and became "the server failed" with a stack in the log — on a method an agent may
|
|
65
|
+
* call with any string it likes, so it was a log-spam path as well as a wrong verdict. The query half needs no
|
|
66
|
+
* such guard: `URLSearchParams` reads a bad escape as literal text.
|
|
67
|
+
*/
|
|
68
|
+
static #decode(segments: string[]) {
|
|
69
|
+
try {
|
|
70
|
+
return segments.map(decodeURIComponent);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Repeated keys become an array so an `arrDepth` search arg round-trips through `{?statuses}` form expansion. */
|
|
77
|
+
static #searchArgs(search: URLSearchParams): Record<string, string | string[]> {
|
|
78
|
+
const args: Record<string, string | string[]> = {};
|
|
79
|
+
for (const key of new Set(search.keys())) {
|
|
80
|
+
const values = search.getAll(key);
|
|
81
|
+
args[key] = values.length > 1 ? values : (values[0] ?? "");
|
|
82
|
+
}
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
}
|