@abide/abide 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +365 -320
- package/CHANGELOG.md +80 -0
- package/README.md +203 -168
- package/package.json +12 -8
- package/src/abideLsp.ts +11 -12
- package/src/abideResolverPlugin.ts +9 -4
- package/src/lib/bundle/disconnected.abide +3 -0
- package/src/lib/cli/printCommandHelp.ts +43 -0
- package/src/lib/cli/printSessionHelp.ts +2 -1
- package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
- package/src/lib/cli/runCli.ts +2 -1
- package/src/lib/mcp/buildPrompts.ts +25 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
- package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
- package/src/lib/mcp/mcpSurface.ts +13 -252
- package/src/lib/mcp/mcpTools.ts +187 -0
- package/src/lib/mcp/renderPrompt.ts +25 -0
- package/src/lib/mcp/types/McpSurface.ts +15 -0
- package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
- package/src/lib/mcp/types/PromptMessage.ts +2 -0
- package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
- package/src/lib/mcp/types/ToolResult.ts +2 -0
- package/src/lib/server/agent.ts +1 -1
- package/src/lib/server/jsonl.ts +2 -2
- package/src/lib/server/rpc/defineRpc.ts +5 -0
- package/src/lib/server/runtime/containsTraversal.ts +21 -17
- package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
- package/src/lib/server/runtime/createServer.ts +101 -71
- package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
- package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
- package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
- package/src/lib/server/runtime/finalizeResponse.ts +32 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
- package/src/lib/server/socket.ts +1 -1
- package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
- package/src/lib/server/sockets/defineSocket.ts +26 -7
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
- package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
- package/src/lib/server/sse.ts +2 -2
- package/src/lib/shared/DEFER.ts +8 -0
- package/src/lib/shared/activeCacheStore.ts +2 -2
- package/src/lib/shared/activePage.ts +2 -2
- package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
- package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
- package/src/lib/shared/basePath.ts +2 -2
- package/src/lib/shared/baseSlot.ts +6 -5
- package/src/lib/shared/bodyValueForKind.ts +1 -2
- package/src/lib/shared/buildSocketOverChannel.ts +40 -4
- package/src/lib/shared/cache.ts +477 -110
- package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
- package/src/lib/shared/cacheStoreSlot.ts +9 -5
- package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
- package/src/lib/shared/createRemoteFunction.ts +58 -9
- package/src/lib/shared/createResolverSlot.ts +24 -31
- package/src/lib/shared/createSocketSubRegistry.ts +2 -2
- package/src/lib/shared/decodeRefJson.ts +4 -2
- package/src/lib/shared/decodeResponse.ts +8 -6
- package/src/lib/shared/done.ts +14 -0
- package/src/lib/shared/encodeRefJson.ts +4 -2
- package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
- package/src/lib/shared/hydratingSlot.ts +12 -0
- package/src/lib/shared/isLayoutFile.ts +12 -0
- package/src/lib/shared/keyForRemoteCall.ts +2 -1
- package/src/lib/shared/keyPrefixForRemote.ts +12 -0
- package/src/lib/shared/matchRoute.ts +175 -0
- package/src/lib/shared/normalizePathname.ts +11 -0
- package/src/lib/shared/pageSlot.ts +14 -5
- package/src/lib/shared/pageUrlForFile.ts +3 -3
- package/src/lib/shared/parseRouteSegments.ts +16 -7
- package/src/lib/shared/patch.ts +41 -0
- package/src/lib/shared/peek.ts +35 -0
- package/src/lib/shared/prepareRemoteExport.ts +40 -0
- package/src/lib/shared/prepareRpcModule.ts +98 -16
- package/src/lib/shared/prepareSocketModule.ts +11 -15
- package/src/lib/shared/queryStringFromArgs.ts +11 -0
- package/src/lib/shared/reachable.ts +102 -0
- package/src/lib/shared/refresh.ts +22 -0
- package/src/lib/shared/requestScopeSlot.ts +10 -7
- package/src/lib/shared/routeParamsShape.ts +9 -9
- package/src/lib/shared/rpcErrorRegistry.ts +69 -0
- package/src/lib/shared/selectorPrefix.ts +3 -10
- package/src/lib/shared/setOwnProperty.ts +19 -0
- package/src/lib/shared/snippet.ts +1 -1
- package/src/lib/shared/subscribableProbes.ts +111 -0
- package/src/lib/shared/tailProbeSlot.ts +8 -1
- package/src/lib/shared/types/CacheEntry.ts +9 -15
- package/src/lib/shared/types/CacheOptions.ts +8 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
- package/src/lib/shared/types/RemoteCallable.ts +25 -7
- package/src/lib/shared/types/RemoteFunction.ts +48 -13
- package/src/lib/shared/types/ResolverSlot.ts +7 -4
- package/src/lib/shared/types/RpcError.ts +26 -0
- package/src/lib/shared/types/SmartReadOptions.ts +32 -0
- package/src/lib/shared/types/Socket.ts +54 -0
- package/src/lib/shared/types/SsrBootState.ts +27 -0
- package/src/lib/shared/types/SsrPayload.ts +21 -0
- package/src/lib/shared/types/Subscribable.ts +13 -13
- package/src/lib/shared/types/TailHooks.ts +2 -1
- package/src/lib/shared/url.ts +29 -14
- package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
- package/src/lib/shared/writeTestSocketsDts.ts +1 -1
- package/src/lib/test/createScriptedSurface.ts +1 -1
- package/src/lib/test/createTestApp.ts +2 -2
- package/src/lib/test/createTestSocketChannel.ts +3 -3
- package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
- package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
- package/src/lib/ui/compile/analyzeComponent.ts +28 -6
- package/src/lib/ui/compile/compileModule.ts +137 -11
- package/src/lib/ui/compile/compileShadow.ts +101 -84
- package/src/lib/ui/compile/desugarSignals.ts +120 -107
- package/src/lib/ui/compile/generateBuild.ts +48 -16
- package/src/lib/ui/compile/generateSSR.ts +117 -14
- package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
- package/src/lib/ui/compile/lowerContext.ts +10 -4
- package/src/lib/ui/compile/lowerScript.ts +72 -5
- package/src/lib/ui/compile/parseTemplate.ts +1 -14
- package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
- package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
- package/src/lib/ui/compile/signalCallee.ts +24 -0
- package/src/lib/ui/compile/stripEffects.ts +14 -11
- package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
- package/src/lib/ui/compile/types/TemplateNode.ts +0 -5
- package/src/lib/ui/dom/appendStatic.ts +9 -1
- package/src/lib/ui/dom/appendText.ts +15 -3
- package/src/lib/ui/dom/assertClaimedText.ts +20 -0
- package/src/lib/ui/dom/awaitBlock.ts +19 -83
- package/src/lib/ui/dom/bindSelectValue.ts +48 -0
- package/src/lib/ui/dom/each.ts +12 -1
- package/src/lib/ui/dom/hydrate.ts +10 -0
- package/src/lib/ui/dom/mountChild.ts +2 -5
- package/src/lib/ui/dom/mountRange.ts +0 -75
- package/src/lib/ui/effect.ts +4 -3
- package/src/lib/ui/installHotBridge.ts +4 -2
- package/src/lib/ui/remoteProxy.ts +18 -2
- package/src/lib/ui/renderToStream.ts +9 -13
- package/src/lib/ui/resumeSeedScript.ts +2 -2
- package/src/lib/ui/router.ts +21 -2
- package/src/lib/ui/runtime/RESUME.ts +0 -6
- package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
- package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
- package/src/lib/ui/scope.ts +6 -3
- package/src/lib/ui/seedBootState.ts +53 -0
- package/src/lib/ui/socketChannel.ts +2 -2
- package/src/lib/ui/socketProxy.ts +9 -3
- package/src/lib/ui/startClient.ts +16 -30
- package/src/lib/ui/state.ts +44 -13
- package/src/lib/ui/sync.ts +11 -5
- package/src/lib/ui/tryEncodeResume.ts +2 -5
- package/src/lib/ui/types/Scope.ts +14 -10
- package/src/lib/ui/watch.ts +140 -0
- package/src/serverEntry.ts +6 -6
- package/template/CLAUDE.md +1 -1
- package/template/package.json +1 -1
- package/template/src/server/rpc/getHello.ts +3 -3
- package/template/src/ui/pages/page.abide +2 -2
- package/template/test/app.test.ts +1 -1
- package/src/lib/server/reachable.ts +0 -45
- package/src/lib/server/sockets/types/Socket.ts +0 -23
- package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
- package/src/lib/shared/DEFER_MIN_ARRAY_LENGTH.ts +0 -14
- package/src/lib/shared/baseResolver.ts +0 -10
- package/src/lib/shared/cacheKeyOf.ts +0 -7
- package/src/lib/shared/cacheKeyStore.ts +0 -8
- package/src/lib/shared/cacheStoreResolver.ts +0 -12
- package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
- package/src/lib/shared/pageResolver.ts +0 -16
- package/src/lib/shared/recordCacheKey.ts +0 -6
- package/src/lib/shared/requestScopeResolver.ts +0 -12
- package/src/lib/shared/setBaseResolver.ts +0 -4
- package/src/lib/shared/setCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
- package/src/lib/shared/setPageResolver.ts +0 -4
- package/src/lib/shared/setRequestScopeResolver.ts +0 -4
- package/src/lib/shared/toBunRoutePattern.ts +0 -28
- package/src/lib/shared/types/TailOptions.ts +0 -10
- package/src/lib/ui/deferResume.ts +0 -36
- package/src/lib/ui/dom/firstElementBetween.ts +0 -14
- package/src/lib/ui/matchRoute.ts +0 -106
- package/src/lib/ui/runtime/scheduleWake.ts +0 -28
- package/src/lib/ui/runtime/whenIdle.ts +0 -21
- package/src/lib/ui/runtime/whenVisible.ts +0 -105
- package/src/lib/ui/tail.ts +0 -324
- /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
- /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { dispatchRpcInProcess } from '../server/rpc/dispatchRpcInProcess.ts'
|
|
2
|
+
import { rpcRegistry } from '../server/rpc/rpcRegistry.ts'
|
|
3
|
+
import type { RpcRegistryEntry } from '../server/rpc/types/RpcRegistryEntry.ts'
|
|
4
|
+
import { socketOperations } from '../server/sockets/socketOperations.ts'
|
|
5
|
+
import { socketRegistry } from '../server/sockets/socketRegistry.ts'
|
|
6
|
+
import type { SocketOperation } from '../server/sockets/types/SocketOperation.ts'
|
|
7
|
+
import type { SocketRegistryEntry } from '../server/sockets/types/SocketRegistryEntry.ts'
|
|
8
|
+
import { commandNameForUrl } from '../shared/commandNameForUrl.ts'
|
|
9
|
+
import { forwardHeaders } from '../shared/forwardHeaders.ts'
|
|
10
|
+
import { jsonSchemaForSchema } from '../shared/jsonSchemaForSchema.ts'
|
|
11
|
+
import { messageFromError } from '../shared/messageFromError.ts'
|
|
12
|
+
import { annotationsForMethod } from './annotationsForMethod.ts'
|
|
13
|
+
import { toolResultFromResponse } from './toolResultFromResponse.ts'
|
|
14
|
+
import type { ToolDescriptor } from './types/ToolDescriptor.ts'
|
|
15
|
+
import type { ToolResult } from './types/ToolResult.ts'
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
One MCP-exposed tool, resolved from the live registries: an rpc, or one of a
|
|
19
|
+
socket's operations. The ref carries the tool's identity (name) plus the
|
|
20
|
+
registry entry it projects from; the descriptor and the dispatch below are two
|
|
21
|
+
renderings of the same ref, so tools/list and tools/call can't disagree on
|
|
22
|
+
which tools exist or what they're called.
|
|
23
|
+
*/
|
|
24
|
+
type McpToolRef =
|
|
25
|
+
| { kind: 'rpc'; name: string; entry: RpcRegistryEntry }
|
|
26
|
+
| { kind: 'socket'; name: string; entry: SocketRegistryEntry; operation: SocketOperation }
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
Enumerates the MCP tool namespace — the single decision of which tools exist.
|
|
30
|
+
RPCs with clients.mcp=true become one tool named after the export's URL
|
|
31
|
+
(folder segments joined with `-`; reads auto-expose while mutating rpcs
|
|
32
|
+
require an explicit clients.mcp — see resolveClientFlags). Sockets with
|
|
33
|
+
clients.mcp=true contribute a `<base>-tail` read tool and, when clientPublish
|
|
34
|
+
is set, a `<base>-publish` tool (existence + naming live in socketOperations).
|
|
35
|
+
RPCs enumerate first, so on a name collision the rpc tool wins.
|
|
36
|
+
*/
|
|
37
|
+
function mcpToolRefs(): McpToolRef[] {
|
|
38
|
+
const refs: McpToolRef[] = []
|
|
39
|
+
for (const entry of rpcRegistry.values()) {
|
|
40
|
+
if (!entry.clients.mcp) {
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
refs.push({ kind: 'rpc', name: commandNameForUrl(entry.remote.url), entry })
|
|
44
|
+
}
|
|
45
|
+
for (const entry of socketRegistry.values()) {
|
|
46
|
+
if (!entry.clients.mcp) {
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
for (const operation of socketOperations(entry)) {
|
|
50
|
+
refs.push({ kind: 'socket', name: operation.name, entry, operation })
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return refs
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
The advertised face of one tool. An rpc tool's description favours the
|
|
58
|
+
schema's top-level description (the vendor's JSON Schema conversion carries
|
|
59
|
+
`.describe(...)` through), falling back to `method url` so the tool is still
|
|
60
|
+
labelled when the schema has none; the HTTP method feeds the annotations
|
|
61
|
+
(readOnlyHint / destructiveHint / idempotentHint) so a model can tell a read
|
|
62
|
+
from a write.
|
|
63
|
+
*/
|
|
64
|
+
function describeTool(ref: McpToolRef): ToolDescriptor {
|
|
65
|
+
if (ref.kind === 'rpc') {
|
|
66
|
+
const inputSchema = jsonSchemaForSchema(ref.entry.inputSchema)
|
|
67
|
+
const tool: ToolDescriptor = {
|
|
68
|
+
name: ref.name,
|
|
69
|
+
description:
|
|
70
|
+
(inputSchema.description as string | undefined) ??
|
|
71
|
+
`${ref.entry.remote.method} ${ref.entry.remote.url}`,
|
|
72
|
+
inputSchema,
|
|
73
|
+
annotations: annotationsForMethod(ref.entry.remote.method),
|
|
74
|
+
}
|
|
75
|
+
if (ref.entry.outputSchema) {
|
|
76
|
+
tool.outputSchema = jsonSchemaForSchema(ref.entry.outputSchema)
|
|
77
|
+
}
|
|
78
|
+
return tool
|
|
79
|
+
}
|
|
80
|
+
const payloadSchema = jsonSchemaForSchema(ref.entry.schema)
|
|
81
|
+
if (ref.operation.kind === 'tail') {
|
|
82
|
+
return {
|
|
83
|
+
name: ref.name,
|
|
84
|
+
description: `Read recent messages from the "${ref.operation.socketName}" socket`,
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
count: { type: 'number', description: 'max recent messages to return' },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
outputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: { frames: { type: 'array', items: payloadSchema } },
|
|
94
|
+
},
|
|
95
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
name: ref.name,
|
|
100
|
+
description: `Publish a message to the "${ref.operation.socketName}" socket`,
|
|
101
|
+
inputSchema: payloadSchema,
|
|
102
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function textResult(text: string, isError = false): ToolResult {
|
|
107
|
+
return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
Dispatches an rpc tool by synthesizing a Request (with forwarded auth headers
|
|
112
|
+
from `inbound`) and piping it through rpc.fetch inside the request scope — the
|
|
113
|
+
same seam the HTTP router crosses, so validation, the handler, and the
|
|
114
|
+
request-scoped helpers (per-call cache(), cookies(), request()) behave
|
|
115
|
+
identically. A handler throw is caught by the scope and framed as an isError
|
|
116
|
+
tool result (via the 500 response) rather than escaping. The response
|
|
117
|
+
(buffered or streaming) is framed by toolResultFromResponse.
|
|
118
|
+
*/
|
|
119
|
+
async function callRpcTool(
|
|
120
|
+
entry: RpcRegistryEntry,
|
|
121
|
+
args: Record<string, unknown> | undefined,
|
|
122
|
+
inbound: Request,
|
|
123
|
+
): Promise<ToolResult> {
|
|
124
|
+
const response = await dispatchRpcInProcess({
|
|
125
|
+
remote: entry.remote,
|
|
126
|
+
args,
|
|
127
|
+
baseUrl: `${new URL(inbound.url).origin}/`,
|
|
128
|
+
headers: forwardHeaders(inbound.headers),
|
|
129
|
+
})
|
|
130
|
+
return toolResultFromResponse(response)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
Dispatches a socket tool. tail returns the retained tail (request/response
|
|
135
|
+
can't hold a live subscription); publish validates against the socket schema
|
|
136
|
+
and fans out.
|
|
137
|
+
*/
|
|
138
|
+
function callSocketTool(
|
|
139
|
+
entry: SocketRegistryEntry,
|
|
140
|
+
operation: SocketOperation,
|
|
141
|
+
args: Record<string, unknown> | undefined,
|
|
142
|
+
): Promise<ToolResult> | ToolResult {
|
|
143
|
+
if (operation.kind === 'tail') {
|
|
144
|
+
const count = typeof args?.count === 'number' ? args.count : undefined
|
|
145
|
+
const frames = entry.snapshotTail(count)
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: frames.map((f) => JSON.stringify(f)).join('\n') }],
|
|
148
|
+
structuredContent: { frames },
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
// broadcast() validates the payload against the socket schema and throws on failure.
|
|
153
|
+
entry.socket.broadcast(args)
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return textResult(messageFromError(error), true)
|
|
156
|
+
}
|
|
157
|
+
return textResult('ok')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/*
|
|
161
|
+
The MCP tool surface: what tools/list advertises and what tools/call runs.
|
|
162
|
+
Both faces render from mcpToolRefs — the tool namespace is decided once, so
|
|
163
|
+
everything advertised is callable and everything callable is advertised.
|
|
164
|
+
list() is the only face that builds descriptors (schema projection is
|
|
165
|
+
per-call work dispatch never pays). Read at call time (not cached) so rpcs
|
|
166
|
+
constructed after boot show up, matching the other registry projections.
|
|
167
|
+
*/
|
|
168
|
+
export const mcpTools = {
|
|
169
|
+
list(): ToolDescriptor[] {
|
|
170
|
+
return mcpToolRefs().map(describeTool)
|
|
171
|
+
},
|
|
172
|
+
async call(
|
|
173
|
+
toolName: string,
|
|
174
|
+
args: Record<string, unknown> | undefined,
|
|
175
|
+
inbound: Request,
|
|
176
|
+
): Promise<ToolResult> {
|
|
177
|
+
for (const ref of mcpToolRefs()) {
|
|
178
|
+
if (ref.name !== toolName) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
return ref.kind === 'rpc'
|
|
182
|
+
? callRpcTool(ref.entry, args, inbound)
|
|
183
|
+
: callSocketTool(ref.entry, ref.operation, args)
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`unknown tool: ${toolName}`)
|
|
186
|
+
},
|
|
187
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { promptRegistry } from '../server/prompts/promptRegistry.ts'
|
|
2
|
+
import type { PromptMessage } from './types/PromptMessage.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Renders a prompt: looks it up, interpolates the caller's args, and returns its
|
|
6
|
+
optional description plus the message(s) that seed a conversation. A markdown
|
|
7
|
+
prompt is a single user turn whose text is the interpolated template. Throws on
|
|
8
|
+
an unknown prompt name. The one place prompt rendering lives — dispatchMcpRequest
|
|
9
|
+
wraps this in the prompts/get wire shape, the agent loop reads the messages plain.
|
|
10
|
+
*/
|
|
11
|
+
export function renderPrompt(
|
|
12
|
+
name: string,
|
|
13
|
+
args?: Record<string, unknown>,
|
|
14
|
+
): { description?: string; messages: PromptMessage[] } {
|
|
15
|
+
const entry = promptRegistry.get(name)
|
|
16
|
+
if (!entry) {
|
|
17
|
+
throw new Error(`unknown prompt: ${name}`)
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
...(entry.prompt.description ? { description: entry.prompt.description } : {}),
|
|
21
|
+
messages: [
|
|
22
|
+
{ role: 'user', text: entry.prompt.render((args ?? {}) as Record<string, string>) },
|
|
23
|
+
],
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { McpResourceContents } from './McpResourceContents.ts'
|
|
2
|
+
import type { McpResourceDescriptor } from './McpResourceDescriptor.ts'
|
|
3
|
+
import type { PromptDescriptor } from './PromptDescriptor.ts'
|
|
4
|
+
import type { PromptMessage } from './PromptMessage.ts'
|
|
5
|
+
import type { ToolDescriptor } from './ToolDescriptor.ts'
|
|
6
|
+
import type { ToolResult } from './ToolResult.ts'
|
|
7
|
+
|
|
8
|
+
export type McpSurface = {
|
|
9
|
+
tools: ToolDescriptor[]
|
|
10
|
+
call(name: string, args: Record<string, unknown> | undefined): Promise<ToolResult>
|
|
11
|
+
prompts: PromptDescriptor[]
|
|
12
|
+
getPrompt(name: string, args?: Record<string, unknown>): PromptMessage[]
|
|
13
|
+
listResources(): Promise<McpResourceDescriptor[]>
|
|
14
|
+
readResource(uri: string): Promise<McpResourceContents | undefined>
|
|
15
|
+
}
|
package/src/lib/server/agent.ts
CHANGED
package/src/lib/server/jsonl.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
|
31
31
|
export function jsonl<Frame>(
|
|
32
32
|
iterable: AsyncIterable<Frame>,
|
|
33
33
|
init?: ResponseInit,
|
|
34
|
-
): TypedResponse<Frame
|
|
34
|
+
): TypedResponse<AsyncIterable<Frame>> {
|
|
35
35
|
const body = streamFromIterator(iterable, {
|
|
36
36
|
encodeFrame: (value) => `${JSON.stringify(value)}\n`,
|
|
37
37
|
encodeError: (message) => jsonlErrorFrame.encode(message),
|
|
@@ -43,5 +43,5 @@ export function jsonl<Frame>(
|
|
|
43
43
|
'Cache-Control': NO_STORE,
|
|
44
44
|
'X-Content-Type-Options': 'nosniff',
|
|
45
45
|
}),
|
|
46
|
-
) as TypedResponse<Frame
|
|
46
|
+
) as TypedResponse<AsyncIterable<Frame>>
|
|
47
47
|
}
|
|
@@ -64,6 +64,10 @@ export function defineRpc<Args, Return>(
|
|
|
64
64
|
parked write drains on `rpc.outbox.retry()` (no auto-drain). Mutating methods
|
|
65
65
|
only. */
|
|
66
66
|
outbox?: boolean
|
|
67
|
+
/* Handler returns jsonl()/sse(): the bare call returns the Subscribable directly (for
|
|
68
|
+
isomorphic `for await (… of fn(args))` in server code). Bundler-stamped; the router's
|
|
69
|
+
wire path (.fetch) is unaffected. */
|
|
70
|
+
streaming?: boolean
|
|
67
71
|
},
|
|
68
72
|
): RemoteFunction<Args, Return> {
|
|
69
73
|
/* `outbox: true` is a mutation contract — a read RPC has nothing to durably deliver,
|
|
@@ -183,6 +187,7 @@ export function defineRpc<Args, Return>(
|
|
|
183
187
|
method,
|
|
184
188
|
url,
|
|
185
189
|
clients,
|
|
190
|
+
streaming: opts?.streaming ?? false,
|
|
186
191
|
crossOrigin: opts?.crossOrigin,
|
|
187
192
|
buildRequest,
|
|
188
193
|
invoke,
|
|
@@ -1,37 +1,41 @@
|
|
|
1
1
|
/*
|
|
2
|
-
Inspects the raw request URL (not the parsed pathname) for
|
|
3
|
-
patterns. The WHATWG URL parser decodes `%2E%2E` to `..` and
|
|
4
|
-
dot-segments out of the pathname during normalization, so by
|
|
5
|
-
`url.pathname` is observable any encoded traversal has been masked.
|
|
6
|
-
remaining literal `..` check guards against any future URL-parser quirk
|
|
2
|
+
Inspects the raw request URL's PATH portion (not the parsed pathname) for
|
|
3
|
+
path-traversal patterns. The WHATWG URL parser decodes `%2E%2E` to `..` and
|
|
4
|
+
then collapses dot-segments out of the pathname during normalization, so by
|
|
5
|
+
the time `url.pathname` is observable any encoded traversal has been masked.
|
|
6
|
+
The remaining literal `..` check guards against any future URL-parser quirk
|
|
7
7
|
that lets a normalised path through.
|
|
8
8
|
|
|
9
|
+
Only the path is scanned — an encoded slash or backslash in a QUERY value
|
|
10
|
+
(`/logo.png?from=%2Fblog`) is legitimate and must not 404 a real asset.
|
|
11
|
+
|
|
9
12
|
Hot path early-out: if none of the suspect substrings appear in the raw
|
|
10
|
-
|
|
13
|
+
path we never lowercase it nor walk segments.
|
|
11
14
|
*/
|
|
12
15
|
export function containsTraversal(rawUrl: string): boolean {
|
|
13
|
-
|
|
16
|
+
const rawPath = rawPathPortion(rawUrl)
|
|
17
|
+
if (rawPath.includes('\\')) {
|
|
14
18
|
return true
|
|
15
19
|
}
|
|
16
|
-
if (
|
|
20
|
+
if (rawPath.includes('..') && rawPath.split('/').some((segment) => segment === '..')) {
|
|
17
21
|
return true
|
|
18
22
|
}
|
|
19
|
-
if (
|
|
23
|
+
if (rawPath.indexOf('%') === -1) {
|
|
20
24
|
return false
|
|
21
25
|
}
|
|
22
|
-
const lower =
|
|
26
|
+
const lower = rawPath.toLowerCase()
|
|
23
27
|
return lower.includes('%2e%2e') || lower.includes('%2f') || lower.includes('%5c')
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
/* The raw path of an absolute request URL: first `/` after the authority up
|
|
31
|
+
to the query. Requests never carry a fragment, so `?` is the only cutoff. */
|
|
32
|
+
function rawPathPortion(rawUrl: string): string {
|
|
27
33
|
const queryStart = rawUrl.indexOf('?')
|
|
28
34
|
const pathEnd = queryStart === -1 ? rawUrl.length : queryStart
|
|
29
|
-
const
|
|
35
|
+
const schemeEnd = rawUrl.indexOf('://')
|
|
36
|
+
const pathStart = rawUrl.indexOf('/', schemeEnd === -1 ? 0 : schemeEnd + 3)
|
|
30
37
|
if (pathStart === -1 || pathStart >= pathEnd) {
|
|
31
|
-
return
|
|
38
|
+
return ''
|
|
32
39
|
}
|
|
33
|
-
return rawUrl
|
|
34
|
-
.slice(pathStart, pathEnd)
|
|
35
|
-
.split('/')
|
|
36
|
-
.some((segment) => segment === '..')
|
|
40
|
+
return rawUrl.slice(pathStart, pathEnd)
|
|
37
41
|
}
|
|
@@ -48,12 +48,16 @@ export async function createAppAssetServer({
|
|
|
48
48
|
[...diskPaths].filter((path) => path.endsWith('.gz')).map((path) => path.slice(0, -3)),
|
|
49
49
|
)
|
|
50
50
|
|
|
51
|
+
// Fresh 404 per call — a Response body is single-use, so it can't be hoisted to a const.
|
|
52
|
+
const notFound = (): Response =>
|
|
53
|
+
new Response('Not Found', {
|
|
54
|
+
status: 404,
|
|
55
|
+
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
56
|
+
})
|
|
57
|
+
|
|
51
58
|
return async function serveAppAsset(req, url) {
|
|
52
59
|
if (containsTraversal(req.url)) {
|
|
53
|
-
return
|
|
54
|
-
status: 404,
|
|
55
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
56
|
-
})
|
|
60
|
+
return notFound()
|
|
57
61
|
}
|
|
58
62
|
/* Embed map and gzip Set hold decoded filesystem names; url.pathname stays
|
|
59
63
|
percent-encoded — decode so a chunk/asset with a non-ASCII name matches and
|
|
@@ -62,10 +66,7 @@ export async function createAppAssetServer({
|
|
|
62
66
|
try {
|
|
63
67
|
assetPath = decodeURIComponent(url.pathname)
|
|
64
68
|
} catch {
|
|
65
|
-
return
|
|
66
|
-
status: 404,
|
|
67
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
68
|
-
})
|
|
69
|
+
return notFound()
|
|
69
70
|
}
|
|
70
71
|
if (assets) {
|
|
71
72
|
const compressed = assets[assetPath]
|
|
@@ -73,10 +74,7 @@ export async function createAppAssetServer({
|
|
|
73
74
|
(request-controlled) pathnames, so building bundles for junk
|
|
74
75
|
`/_app/*` probes would grow it without bound. */
|
|
75
76
|
if (!compressed) {
|
|
76
|
-
return
|
|
77
|
-
status: 404,
|
|
78
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
79
|
-
})
|
|
77
|
+
return notFound()
|
|
80
78
|
}
|
|
81
79
|
return respondWithEmbeddedAsset(
|
|
82
80
|
compressed,
|
|
@@ -87,10 +85,7 @@ export async function createAppAssetServer({
|
|
|
87
85
|
/* Miss-check before header work so probes for non-existent chunks can't grow the
|
|
88
86
|
header cache (the embed branch above guards the same way). */
|
|
89
87
|
if (!diskPaths.has(assetPath)) {
|
|
90
|
-
return
|
|
91
|
-
status: 404,
|
|
92
|
-
headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
|
|
93
|
-
})
|
|
88
|
+
return notFound()
|
|
94
89
|
}
|
|
95
90
|
const { base: baseHeaders, gzip: gzipHeaders } = headersForAsset(assetPath)
|
|
96
91
|
const diskPath = distDir + assetPath
|