@abide/abide 0.45.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.
Files changed (176) hide show
  1. package/AGENTS.md +365 -320
  2. package/CHANGELOG.md +96 -0
  3. package/README.md +203 -168
  4. package/package.json +12 -8
  5. package/src/abideLsp.ts +11 -12
  6. package/src/abideResolverPlugin.ts +9 -4
  7. package/src/lib/bundle/disconnected.abide +3 -0
  8. package/src/lib/cli/printCommandHelp.ts +43 -0
  9. package/src/lib/cli/printSessionHelp.ts +2 -1
  10. package/src/lib/cli/{printHelp.ts → printTopLevelHelp.ts} +3 -40
  11. package/src/lib/cli/runCli.ts +2 -1
  12. package/src/lib/mcp/buildPrompts.ts +25 -0
  13. package/src/lib/mcp/dispatchMcpRequest.ts +8 -6
  14. package/src/lib/mcp/mcpResourceServerSlot.ts +5 -10
  15. package/src/lib/mcp/mcpSurface.ts +13 -252
  16. package/src/lib/mcp/mcpTools.ts +187 -0
  17. package/src/lib/mcp/renderPrompt.ts +25 -0
  18. package/src/lib/mcp/types/McpSurface.ts +15 -0
  19. package/src/lib/mcp/types/PromptDescriptor.ts +5 -0
  20. package/src/lib/mcp/types/PromptMessage.ts +2 -0
  21. package/src/lib/mcp/types/ToolDescriptor.ts +7 -0
  22. package/src/lib/mcp/types/ToolResult.ts +2 -0
  23. package/src/lib/server/agent.ts +1 -1
  24. package/src/lib/server/jsonl.ts +2 -2
  25. package/src/lib/server/rpc/defineRpc.ts +7 -1
  26. package/src/lib/server/runtime/containsTraversal.ts +21 -17
  27. package/src/lib/server/runtime/createAppAssetServer.ts +11 -16
  28. package/src/lib/server/runtime/createServer.ts +101 -71
  29. package/src/lib/server/runtime/createUiPageRenderer.ts +23 -4
  30. package/src/lib/server/runtime/devClientFingerprint.ts +3 -3
  31. package/src/lib/server/runtime/devHotModuleResponse.ts +2 -1
  32. package/src/lib/server/runtime/finalizeResponse.ts +32 -0
  33. package/src/lib/server/runtime/snapshotEntryFromCache.ts +8 -17
  34. package/src/lib/server/socket.ts +1 -1
  35. package/src/lib/server/sockets/createSocketDispatcher.ts +28 -5
  36. package/src/lib/server/sockets/defineSocket.ts +26 -7
  37. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +1 -1
  38. package/src/lib/server/sockets/types/SocketRoutes.ts +1 -1
  39. package/src/lib/server/sse.ts +2 -2
  40. package/src/lib/shared/DEFER.ts +8 -0
  41. package/src/lib/shared/activeCacheStore.ts +2 -2
  42. package/src/lib/shared/activePage.ts +2 -2
  43. package/src/lib/shared/attachRpcSelectorMethods.ts +42 -0
  44. package/src/lib/shared/attachSocketSelectorMethods.ts +34 -0
  45. package/src/lib/shared/basePath.ts +2 -2
  46. package/src/lib/shared/baseSlot.ts +6 -5
  47. package/src/lib/shared/bodyValueForKind.ts +1 -2
  48. package/src/lib/shared/buildSocketOverChannel.ts +40 -4
  49. package/src/lib/shared/cache.ts +477 -110
  50. package/src/lib/shared/cacheEntryFromSnapshot.ts +3 -22
  51. package/src/lib/shared/cacheStoreSlot.ts +9 -5
  52. package/src/lib/{server/runtime → shared}/createReachable.ts +2 -2
  53. package/src/lib/shared/createRemoteFunction.ts +58 -9
  54. package/src/lib/shared/createResolverSlot.ts +24 -31
  55. package/src/lib/shared/createSocketSubRegistry.ts +2 -2
  56. package/src/lib/shared/decodeRefJson.ts +4 -2
  57. package/src/lib/shared/decodeResponse.ts +8 -6
  58. package/src/lib/shared/done.ts +14 -0
  59. package/src/lib/shared/encodeRefJson.ts +4 -2
  60. package/src/lib/shared/globalCacheStoreSlot.ts +9 -6
  61. package/src/lib/shared/hydratingSlot.ts +12 -0
  62. package/src/lib/shared/isLayoutFile.ts +12 -0
  63. package/src/lib/shared/keyForRemoteCall.ts +2 -1
  64. package/src/lib/shared/keyPrefixForRemote.ts +12 -0
  65. package/src/lib/shared/matchRoute.ts +175 -0
  66. package/src/lib/shared/normalizePathname.ts +11 -0
  67. package/src/lib/shared/pageSlot.ts +14 -5
  68. package/src/lib/shared/pageUrlForFile.ts +3 -3
  69. package/src/lib/shared/parseRouteSegments.ts +16 -7
  70. package/src/lib/shared/patch.ts +41 -0
  71. package/src/lib/shared/peek.ts +35 -0
  72. package/src/lib/shared/prepareRemoteExport.ts +40 -0
  73. package/src/lib/shared/prepareRpcModule.ts +98 -16
  74. package/src/lib/shared/prepareSocketModule.ts +11 -15
  75. package/src/lib/shared/queryStringFromArgs.ts +11 -0
  76. package/src/lib/shared/reachable.ts +102 -0
  77. package/src/lib/shared/refresh.ts +22 -0
  78. package/src/lib/shared/requestScopeSlot.ts +10 -7
  79. package/src/lib/shared/routeParamsShape.ts +9 -9
  80. package/src/lib/shared/rpcErrorRegistry.ts +69 -0
  81. package/src/lib/shared/selectorPrefix.ts +3 -10
  82. package/src/lib/shared/setOwnProperty.ts +19 -0
  83. package/src/lib/shared/snippet.ts +1 -1
  84. package/src/lib/shared/subscribableProbes.ts +111 -0
  85. package/src/lib/shared/tailProbeSlot.ts +8 -1
  86. package/src/lib/shared/types/CacheEntry.ts +9 -15
  87. package/src/lib/shared/types/CacheOptions.ts +8 -0
  88. package/src/lib/shared/types/CacheSnapshotEntry.ts +0 -5
  89. package/src/lib/shared/types/RemoteCallable.ts +25 -7
  90. package/src/lib/shared/types/RemoteFunction.ts +48 -13
  91. package/src/lib/shared/types/ResolverSlot.ts +7 -4
  92. package/src/lib/shared/types/RpcError.ts +26 -0
  93. package/src/lib/shared/types/SmartReadOptions.ts +32 -0
  94. package/src/lib/shared/types/Socket.ts +54 -0
  95. package/src/lib/shared/types/SsrBootState.ts +27 -0
  96. package/src/lib/shared/types/SsrPayload.ts +21 -0
  97. package/src/lib/shared/types/Subscribable.ts +13 -13
  98. package/src/lib/shared/types/TailHooks.ts +2 -1
  99. package/src/lib/shared/url.ts +29 -14
  100. package/src/lib/shared/wakeHydrationPeeks.ts +20 -0
  101. package/src/lib/shared/writeTestSocketsDts.ts +1 -1
  102. package/src/lib/test/createScriptedSurface.ts +1 -1
  103. package/src/lib/test/createTestApp.ts +2 -2
  104. package/src/lib/test/createTestSocketChannel.ts +3 -3
  105. package/src/lib/ui/compile/REACTIVE_CALLEES.ts +5 -6
  106. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +10 -7
  107. package/src/lib/ui/compile/abideUiPlugin.ts +2 -2
  108. package/src/lib/ui/compile/analyzeComponent.ts +28 -6
  109. package/src/lib/ui/compile/compileModule.ts +137 -11
  110. package/src/lib/ui/compile/compileShadow.ts +101 -84
  111. package/src/lib/ui/compile/desugarSignals.ts +120 -107
  112. package/src/lib/ui/compile/generateBuild.ts +47 -11
  113. package/src/lib/ui/compile/generateSSR.ts +117 -14
  114. package/src/lib/ui/compile/identifierReferencePattern.ts +13 -0
  115. package/src/lib/ui/compile/lowerContext.ts +10 -4
  116. package/src/lib/ui/compile/lowerScript.ts +72 -5
  117. package/src/lib/ui/compile/prepareNestedScript.ts +31 -17
  118. package/src/lib/ui/compile/resolveReactiveExport.ts +95 -0
  119. package/src/lib/ui/compile/signalCallee.ts +24 -0
  120. package/src/lib/ui/compile/stripEffects.ts +14 -11
  121. package/src/lib/ui/compile/types/AnalyzedComponent.ts +5 -0
  122. package/src/lib/ui/dom/appendStatic.ts +9 -1
  123. package/src/lib/ui/dom/appendText.ts +15 -3
  124. package/src/lib/ui/dom/assertClaimedText.ts +20 -0
  125. package/src/lib/ui/dom/awaitBlock.ts +17 -56
  126. package/src/lib/ui/dom/bindSelectValue.ts +48 -0
  127. package/src/lib/ui/dom/each.ts +12 -1
  128. package/src/lib/ui/dom/hydrate.ts +10 -0
  129. package/src/lib/ui/effect.ts +4 -3
  130. package/src/lib/ui/installHotBridge.ts +4 -2
  131. package/src/lib/ui/remoteProxy.ts +18 -2
  132. package/src/lib/ui/renderToStream.ts +9 -13
  133. package/src/lib/ui/resumeSeedScript.ts +2 -2
  134. package/src/lib/ui/router.ts +21 -2
  135. package/src/lib/ui/runtime/RESUME.ts +0 -6
  136. package/src/lib/ui/runtime/ambientScopeBacking.ts +1 -1
  137. package/src/lib/ui/runtime/types/SsrRender.ts +3 -4
  138. package/src/lib/ui/scope.ts +6 -3
  139. package/src/lib/ui/seedBootState.ts +53 -0
  140. package/src/lib/ui/socketChannel.ts +2 -2
  141. package/src/lib/ui/socketProxy.ts +9 -3
  142. package/src/lib/ui/startClient.ts +16 -30
  143. package/src/lib/ui/state.ts +44 -13
  144. package/src/lib/ui/sync.ts +11 -5
  145. package/src/lib/ui/tryEncodeResume.ts +2 -5
  146. package/src/lib/ui/types/Scope.ts +14 -10
  147. package/src/lib/ui/watch.ts +140 -0
  148. package/src/serverEntry.ts +6 -6
  149. package/template/CLAUDE.md +1 -1
  150. package/template/package.json +1 -1
  151. package/template/src/server/rpc/getHello.ts +3 -3
  152. package/template/src/ui/pages/page.abide +2 -2
  153. package/template/test/app.test.ts +1 -1
  154. package/src/lib/server/reachable.ts +0 -45
  155. package/src/lib/server/sockets/types/Socket.ts +0 -23
  156. package/src/lib/shared/CACHE_WRAPPED.ts +0 -9
  157. package/src/lib/shared/baseResolver.ts +0 -10
  158. package/src/lib/shared/cacheKeyOf.ts +0 -7
  159. package/src/lib/shared/cacheKeyStore.ts +0 -8
  160. package/src/lib/shared/cacheStoreResolver.ts +0 -12
  161. package/src/lib/shared/globalCacheStoreResolver.ts +0 -12
  162. package/src/lib/shared/pageResolver.ts +0 -16
  163. package/src/lib/shared/recordCacheKey.ts +0 -6
  164. package/src/lib/shared/requestScopeResolver.ts +0 -12
  165. package/src/lib/shared/setBaseResolver.ts +0 -4
  166. package/src/lib/shared/setCacheStoreResolver.ts +0 -4
  167. package/src/lib/shared/setGlobalCacheStoreResolver.ts +0 -4
  168. package/src/lib/shared/setPageResolver.ts +0 -4
  169. package/src/lib/shared/setRequestScopeResolver.ts +0 -4
  170. package/src/lib/shared/toBunRoutePattern.ts +0 -28
  171. package/src/lib/shared/types/TailOptions.ts +0 -10
  172. package/src/lib/ui/deferResume.ts +0 -29
  173. package/src/lib/ui/matchRoute.ts +0 -106
  174. package/src/lib/ui/tail.ts +0 -324
  175. /package/src/lib/{server/sockets → shared}/types/SocketClientFrame.ts +0 -0
  176. /package/src/lib/{server/sockets → shared}/types/SocketServerFrame.ts +0 -0
package/src/abideLsp.ts CHANGED
@@ -1,4 +1,3 @@
1
- import { fileURLToPath, pathToFileURL } from 'node:url'
2
1
  import ts from 'typescript'
3
2
  import { ABIDE_SEMANTIC_TOKENS_LEGEND } from './lib/ui/compile/ABIDE_SEMANTIC_TOKENS_LEGEND.ts'
4
3
  import type {
@@ -77,7 +76,7 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
77
76
  jsonrpc: '2.0',
78
77
  method: 'textDocument/publishDiagnostics',
79
78
  params: {
80
- uri: pathToFileURL(path).href,
79
+ uri: Bun.pathToFileURL(path).href,
81
80
  diagnostics: serviceFor(path)
82
81
  .diagnostics(path)
83
82
  .map((diagnostic) => toLspDiagnostic(text, diagnostic)),
@@ -109,7 +108,7 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
109
108
  case 'textDocument/didOpen': {
110
109
  const { uri, text } = message.params.textDocument
111
110
  if (isAbide(uri)) {
112
- const path = fileURLToPath(uri)
111
+ const path = Bun.fileURLToPath(uri)
113
112
  documentText.set(path, text)
114
113
  serviceFor(path).update(path, text)
115
114
  publish(path)
@@ -119,7 +118,7 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
119
118
  case 'textDocument/didChange': {
120
119
  const { uri } = message.params.textDocument
121
120
  if (isAbide(uri)) {
122
- const path = fileURLToPath(uri)
121
+ const path = Bun.fileURLToPath(uri)
123
122
  const text = message.params.contentChanges.at(-1)?.text ?? ''
124
123
  documentText.set(path, text)
125
124
  serviceFor(path).update(path, text)
@@ -130,7 +129,7 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
130
129
  case 'textDocument/didSave': {
131
130
  const { uri } = message.params.textDocument
132
131
  if (isAbide(uri)) {
133
- publish(fileURLToPath(uri))
132
+ publish(Bun.fileURLToPath(uri))
134
133
  }
135
134
  return
136
135
  }
@@ -138,10 +137,10 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
138
137
  /* A request: always answer (null when there's nothing to show) so
139
138
  the editor isn't left waiting. */
140
139
  const { uri } = message.params.textDocument
141
- const text = isAbide(uri) ? (documentText.get(fileURLToPath(uri)) ?? '') : ''
140
+ const text = isAbide(uri) ? (documentText.get(Bun.fileURLToPath(uri)) ?? '') : ''
142
141
  const info = isAbide(uri)
143
- ? serviceFor(fileURLToPath(uri)).quickInfo(
144
- fileURLToPath(uri),
142
+ ? serviceFor(Bun.fileURLToPath(uri)).quickInfo(
143
+ Bun.fileURLToPath(uri),
145
144
  positionToOffset(text, message.params.position),
146
145
  )
147
146
  : undefined
@@ -156,9 +155,9 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
156
155
  const { uri } = message.params.textDocument
157
156
  const data = isAbide(uri)
158
157
  ? componentSemanticTokens(
159
- serviceFor(fileURLToPath(uri)),
160
- fileURLToPath(uri),
161
- documentText.get(fileURLToPath(uri)) ?? '',
158
+ serviceFor(Bun.fileURLToPath(uri)),
159
+ Bun.fileURLToPath(uri),
160
+ documentText.get(Bun.fileURLToPath(uri)) ?? '',
162
161
  )
163
162
  : []
164
163
  send({ jsonrpc: '2.0', id: message.id, result: { data } })
@@ -167,7 +166,7 @@ export async function abideLsp({ cwd }: { cwd: string }): Promise<void> {
167
166
  case 'textDocument/didClose': {
168
167
  const { uri } = message.params.textDocument
169
168
  if (isAbide(uri)) {
170
- const path = fileURLToPath(uri)
169
+ const path = Bun.fileURLToPath(uri)
171
170
  serviceFor(path).close(path)
172
171
  documentText.delete(path)
173
172
  send({
@@ -381,11 +381,16 @@ export function abideResolverPlugin({
381
381
  so page imports resolve identically on both sides.
382
382
  */
383
383
  if (target === 'client') {
384
- /* A durable rpc (`outbox: true`) gets the third arg so its client proxy
385
- parks unreachable calls onto the outbox. */
386
- const durableArg = prepared.durable ? ', { outbox: true }' : ''
384
+ /* Build-time flags on the client proxy stub: `outbox: true` parks
385
+ unreachable calls; `streaming: true` (handler returns jsonl()/sse())
386
+ makes the bare call return the Subscribable directly. */
387
+ const flags = [
388
+ prepared.durable ? 'outbox: true' : undefined,
389
+ prepared.streaming ? 'streaming: true' : undefined,
390
+ ].filter(Boolean)
391
+ const optsArg = flags.length > 0 ? `, { ${flags.join(', ')} }` : ''
387
392
  const contents = `import { remoteProxy as __abideRemoteProxy__ } from '${importName}/ui/remoteProxy';
388
- export const ${prepared.exportName} = __abideRemoteProxy__(${JSON.stringify(prepared.method)}, ${JSON.stringify(url)}${durableArg});
393
+ export const ${prepared.exportName} = __abideRemoteProxy__(${JSON.stringify(prepared.method)}, ${JSON.stringify(url)}${optsArg});
389
394
  `
390
395
  return { contents, loader: 'ts' }
391
396
  }
@@ -1,4 +1,7 @@
1
1
  <script>
2
+ import { state } from '@abide/abide/ui/state'
3
+ import { effect } from '@abide/abide/ui/effect'
4
+
2
5
  /*
3
6
  Default bundle connect screen (abide-ui). The launcher serves this (logo baked in
4
7
  at build time, app title injected at runtime) instead of a blank window; override
@@ -0,0 +1,43 @@
1
+ import type { CliManifest } from './types/CliManifest.ts'
2
+
3
+ /*
4
+ Per-command help (`<cmd> --help`): the command's method/url, its description, and
5
+ the flags derived from its JSON Schema. Output goes to stdout; the caller exits
6
+ zero after printing. Top-level help (the command listing) lives in
7
+ `printTopLevelHelp`.
8
+ */
9
+ export function printCommandHelp(programName: string, name: string, manifest: CliManifest): void {
10
+ const entry = manifest[name]
11
+ if (!entry) {
12
+ console.log(`unknown command: ${name}`)
13
+ return
14
+ }
15
+ console.log(`usage: ${programName} ${name} [--flags]\n`)
16
+ console.log(` ${entry.method} ${entry.url}\n`)
17
+ const schema = entry.jsonSchema
18
+ const commandDescription = schema?.description as string | undefined
19
+ if (commandDescription) {
20
+ console.log(`${commandDescription}\n`)
21
+ }
22
+ const properties =
23
+ (schema?.properties as
24
+ | Record<string, { type?: string; description?: string }>
25
+ | undefined) ?? {}
26
+ const required = new Set((schema?.required as string[] | undefined) ?? [])
27
+ if (Object.keys(properties).length === 0) {
28
+ console.log('flags: (none)')
29
+ } else {
30
+ console.log('flags:')
31
+ for (const [key, value] of Object.entries(properties)) {
32
+ const tag =
33
+ value.type === 'boolean'
34
+ ? `--${key} / --no-${key}`
35
+ : `--${key} <${value.type ?? 'value'}>`
36
+ const requiredTag = required.has(key) ? ' (required)' : ''
37
+ const description = value.description ? ` — ${value.description}` : ''
38
+ console.log(` ${tag.padEnd(28)}${requiredTag}${description}`)
39
+ }
40
+ }
41
+ console.log('\n --json <object> full args bag as JSON (overrides flags)')
42
+ console.log(' (stdin) pipe a JSON object as the args bag')
43
+ }
@@ -1,4 +1,5 @@
1
- import { printCommandHelp, printTopLevelHelp } from './printHelp.ts'
1
+ import { printCommandHelp } from './printCommandHelp.ts'
2
+ import { printTopLevelHelp } from './printTopLevelHelp.ts'
2
3
  import type { CliManifest } from './types/CliManifest.ts'
3
4
 
4
5
  /*
@@ -26,10 +26,9 @@ function flagSignature(jsonSchema: CliManifestEntry['jsonSchema']): string {
26
26
  }
27
27
 
28
28
  /*
29
- Top-level help (no subcommand) lists every available command with a
30
- one-line summary. Per-command help (`<cmd> --help`) prints the flags
31
- derived from the command's JSON Schema. Output goes to stdout in both
32
- cases; the caller exits zero after printing.
29
+ Top-level help (no subcommand) lists every available command with a one-line
30
+ summary. Per-command help lives in `printCommandHelp`. Output goes to stdout;
31
+ the caller exits zero after printing.
33
32
  */
34
33
  export function printTopLevelHelp(
35
34
  programName: string,
@@ -81,39 +80,3 @@ export function printTopLevelHelp(
81
80
  printTrimmed(footer)
82
81
  }
83
82
  }
84
-
85
- export function printCommandHelp(programName: string, name: string, manifest: CliManifest): void {
86
- const entry = manifest[name]
87
- if (!entry) {
88
- console.log(`unknown command: ${name}`)
89
- return
90
- }
91
- console.log(`usage: ${programName} ${name} [--flags]\n`)
92
- console.log(` ${entry.method} ${entry.url}\n`)
93
- const schema = entry.jsonSchema
94
- const commandDescription = schema?.description as string | undefined
95
- if (commandDescription) {
96
- console.log(`${commandDescription}\n`)
97
- }
98
- const properties =
99
- (schema?.properties as
100
- | Record<string, { type?: string; description?: string }>
101
- | undefined) ?? {}
102
- const required = new Set((schema?.required as string[] | undefined) ?? [])
103
- if (Object.keys(properties).length === 0) {
104
- console.log('flags: (none)')
105
- } else {
106
- console.log('flags:')
107
- for (const [key, value] of Object.entries(properties)) {
108
- const tag =
109
- value.type === 'boolean'
110
- ? `--${key} / --no-${key}`
111
- : `--${key} <${value.type ?? 'value'}>`
112
- const requiredTag = required.has(key) ? ' (required)' : ''
113
- const description = value.description ? ` — ${value.description}` : ''
114
- console.log(` ${tag.padEnd(28)}${requiredTag}${description}`)
115
- }
116
- }
117
- console.log('\n --json <object> full args bag as JSON (overrides flags)')
118
- console.log(' (stdin) pipe a JSON object as the args bag')
119
- }
@@ -4,7 +4,8 @@ import { messageFromError } from '../shared/messageFromError.ts'
4
4
  import { connectToServer } from './connectToServer.ts'
5
5
  import { dispatchCommand } from './dispatchCommand.ts'
6
6
  import { loadEnvFromBinaryDir } from './loadEnvFromBinaryDir.ts'
7
- import { printCommandHelp, printTopLevelHelp } from './printHelp.ts'
7
+ import { printCommandHelp } from './printCommandHelp.ts'
8
+ import { printTopLevelHelp } from './printTopLevelHelp.ts'
8
9
  import { printTrimmed } from './printTrimmed.ts'
9
10
  import { resolveCliTarget } from './resolveCliTarget.ts'
10
11
  import { runSession } from './runSession.ts'
@@ -0,0 +1,25 @@
1
+ import { promptRegistry } from '../server/prompts/promptRegistry.ts'
2
+ import type { PromptDescriptor } from './types/PromptDescriptor.ts'
3
+
4
+ /*
5
+ MCP prompts derived from src/mcp/prompts. Arguments come from the JSON
6
+ Schema the resolver built from each prompt's frontmatter `arguments` list
7
+ (top-level properties + required flags); the model fills them in and the
8
+ framework interpolates them into the body on getPrompt.
9
+ */
10
+ export function buildPrompts(): PromptDescriptor[] {
11
+ return Array.from(promptRegistry.values()).map((entry) => {
12
+ const jsonSchema = entry.jsonSchema ?? {}
13
+ const properties = (jsonSchema.properties ?? {}) as Record<string, { description?: string }>
14
+ const required = new Set((jsonSchema.required as string[] | undefined) ?? [])
15
+ return {
16
+ name: entry.prompt.name,
17
+ ...(entry.prompt.description ? { description: entry.prompt.description } : {}),
18
+ arguments: Object.entries(properties).map(([argName, prop]) => ({
19
+ name: argName,
20
+ ...(prop?.description ? { description: prop.description } : {}),
21
+ required: required.has(argName),
22
+ })),
23
+ }
24
+ })
25
+ }
@@ -1,7 +1,9 @@
1
1
  import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
2
2
  import { messageFromError } from '../shared/messageFromError.ts'
3
- import { getMcpResourceServer } from './mcpResourceServerSlot.ts'
4
- import { buildPrompts, buildTools, callTool, renderPrompt } from './mcpSurface.ts'
3
+ import { buildPrompts } from './buildPrompts.ts'
4
+ import { mcpResourceServerSlot } from './mcpResourceServerSlot.ts'
5
+ import { mcpTools } from './mcpTools.ts'
6
+ import { renderPrompt } from './renderPrompt.ts'
5
7
  import type { JsonRpcRequest } from './types/JsonRpcRequest.ts'
6
8
  import type { JsonRpcResponse } from './types/JsonRpcResponse.ts'
7
9
  import type { McpServerOptions } from './types/McpServerOptions.ts'
@@ -108,7 +110,7 @@ async function dispatchMethod(
108
110
  case 'ping':
109
111
  return jsonRpcOk(id, {})
110
112
  case 'tools/list':
111
- return jsonRpcOk(id, { tools: buildTools() })
113
+ return jsonRpcOk(id, { tools: mcpTools.list() })
112
114
  case 'tools/call': {
113
115
  const params = envelope.params as
114
116
  | { name?: string; arguments?: Record<string, unknown> }
@@ -116,10 +118,10 @@ async function dispatchMethod(
116
118
  if (!params?.name) {
117
119
  return jsonRpcError(id, -32602, 'Missing tool name')
118
120
  }
119
- return jsonRpcOk(id, await callTool(params.name, params.arguments, request))
121
+ return jsonRpcOk(id, await mcpTools.call(params.name, params.arguments, request))
120
122
  }
121
123
  case 'resources/list': {
122
- const resourceServer = getMcpResourceServer()
124
+ const resourceServer = mcpResourceServerSlot.server
123
125
  return jsonRpcOk(id, {
124
126
  resources: resourceServer ? await resourceServer.list() : [],
125
127
  })
@@ -129,7 +131,7 @@ async function dispatchMethod(
129
131
  if (!params?.uri) {
130
132
  return jsonRpcError(id, -32602, 'Missing resource uri')
131
133
  }
132
- const resourceServer = getMcpResourceServer()
134
+ const resourceServer = mcpResourceServerSlot.server
133
135
  const contents = resourceServer ? await resourceServer.read(params.uri) : undefined
134
136
  if (!contents) {
135
137
  return jsonRpcError(id, -32602, `unknown resource: ${params.uri}`)
@@ -3,16 +3,11 @@ import type { McpResourceServer } from './types/McpResourceServer.ts'
3
3
  /*
4
4
  Process-wide slot for the MCP resource server. createServer assigns it at
5
5
  boot; dispatchMcpRequest reads it on resources/list + resources/read. Mirrors
6
- the registryManifests slot the default MCP server is constructed in the
7
- abide:mcp virtual with no args, so the resource server (which needs the
6
+ the other `*Slot` seams (e.g. logTapSlot) a single object named after the
7
+ file with a mutable field — because the default MCP server is constructed in
8
+ the abide:mcp virtual with no args, so the resource server (which needs the
8
9
  project's resourcesDir + embedded map) is injected out of band.
9
10
  */
10
- let resourceServer: McpResourceServer | undefined
11
-
12
- export function setMcpResourceServer(value: McpResourceServer): void {
13
- resourceServer = value
14
- }
15
-
16
- export function getMcpResourceServer(): McpResourceServer | undefined {
17
- return resourceServer
11
+ export const mcpResourceServerSlot: { server: McpResourceServer | undefined } = {
12
+ server: undefined,
18
13
  }
@@ -1,267 +1,28 @@
1
- import { promptRegistry } from '../server/prompts/promptRegistry.ts'
2
- import { dispatchRpcInProcess } from '../server/rpc/dispatchRpcInProcess.ts'
3
- import { findRpcByCommandName } from '../server/rpc/findRpcByCommandName.ts'
4
- import { rpcRegistry } from '../server/rpc/rpcRegistry.ts'
5
- import { socketOperations } from '../server/sockets/socketOperations.ts'
6
- import { socketRegistry } from '../server/sockets/socketRegistry.ts'
7
1
  import { abideLog } from '../shared/abideLog.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 { getMcpResourceServer } from './mcpResourceServerSlot.ts'
14
- import { toolResultFromResponse } from './toolResultFromResponse.ts'
15
- import type { McpResourceContents } from './types/McpResourceContents.ts'
16
- import type { McpResourceDescriptor } from './types/McpResourceDescriptor.ts'
2
+ import { buildPrompts } from './buildPrompts.ts'
3
+ import { mcpResourceServerSlot } from './mcpResourceServerSlot.ts'
4
+ import { mcpTools } from './mcpTools.ts'
5
+ import { renderPrompt } from './renderPrompt.ts'
6
+ import type { McpSurface } from './types/McpSurface.ts'
7
+ import type { PromptDescriptor } from './types/PromptDescriptor.ts'
8
+ import type { ToolDescriptor } from './types/ToolDescriptor.ts'
17
9
 
18
10
  /*
19
11
  The app's MCP surface, projected for in-process consumers. This is the single
20
12
  source of truth dispatchMcpRequest (the JSON-RPC-over-HTTP transport) and the
21
13
  in-app agent loop (abide/server/agent) both build on — the same tool/prompt/
22
14
  resource derivation, so a model reaching the app over HTTP and a model driven
23
- in-process can't drift on what's exposed.
15
+ in-process can't drift on what's exposed. The individual derivations live in
16
+ sibling modules (mcpTools / buildPrompts / renderPrompt).
24
17
 
25
18
  Internal: there is no package export. The public entry is `agent()`, which
26
19
  calls `mcpSurface(request)` to hand an engine the gated tool set.
27
20
  */
28
21
 
29
- export type ToolDescriptor = {
30
- name: string
31
- description: string
32
- inputSchema: Record<string, unknown>
33
- outputSchema?: Record<string, unknown>
34
- annotations?: Record<string, boolean>
35
- }
36
-
37
- export type PromptDescriptor = {
38
- name: string
39
- description?: string
40
- arguments: Array<{ name: string; description?: string; required: boolean }>
41
- }
42
-
43
- // The MCP tools/call result shape (a text content block plus optional structuredContent).
44
- export type ToolResult = Record<string, unknown>
45
-
46
- // A prompt rendered to plain messages — the user turn(s) that seed a conversation.
47
- export type PromptMessage = { role: 'user'; text: string }
48
-
49
- export type McpSurface = {
50
- tools: ToolDescriptor[]
51
- call(name: string, args: Record<string, unknown> | undefined): Promise<ToolResult>
52
- prompts: PromptDescriptor[]
53
- getPrompt(name: string, args?: Record<string, unknown>): PromptMessage[]
54
- listResources(): Promise<McpResourceDescriptor[]>
55
- readResource(uri: string): Promise<McpResourceContents | undefined>
56
- }
57
-
58
- /*
59
- Builds the array of MCP tool descriptors.
60
-
61
- RPCs: every rpc with clients.mcp=true becomes one tool named after the
62
- export's URL (folder segments joined with `-`). The HTTP method feeds the
63
- tool's annotations (readOnlyHint / destructiveHint / idempotentHint) so
64
- a model can tell a read from a write; reads auto-expose while mutating
65
- rpcs require an explicit clients.mcp (see resolveClientFlags). When the
66
- rpc declares an `outputSchema` it's advertised as the tool outputSchema.
67
-
68
- Sockets: every socket with clients.mcp=true contributes a `<base>-tail`
69
- read tool (recent buffered messages) and, when clientPublish is set, a
70
- `<base>-publish` tool.
71
- */
72
- export function buildTools(): ToolDescriptor[] {
73
- const tools: ToolDescriptor[] = []
74
- for (const entry of rpcRegistry.values()) {
75
- if (!entry.clients.mcp) {
76
- continue
77
- }
78
- /*
79
- Tool description favours the schema's top-level description (the
80
- vendor's JSON Schema conversion carries `.describe(...)` through),
81
- falling back to `method url` so the tool is still labelled when
82
- the schema has none.
83
- */
84
- const inputSchema = jsonSchemaForSchema(entry.inputSchema)
85
- const tool: ToolDescriptor = {
86
- name: commandNameForUrl(entry.remote.url),
87
- description:
88
- (inputSchema.description as string | undefined) ??
89
- `${entry.remote.method} ${entry.remote.url}`,
90
- inputSchema,
91
- annotations: annotationsForMethod(entry.remote.method),
92
- }
93
- if (entry.outputSchema) {
94
- tool.outputSchema = jsonSchemaForSchema(entry.outputSchema)
95
- }
96
- tools.push(tool)
97
- }
98
- for (const entry of socketRegistry.values()) {
99
- if (!entry.clients.mcp) {
100
- continue
101
- }
102
- const payloadSchema = jsonSchemaForSchema(entry.schema)
103
- for (const operation of socketOperations(entry)) {
104
- if (operation.kind === 'tail') {
105
- tools.push({
106
- name: operation.name,
107
- description: `Read recent messages from the "${operation.socketName}" socket`,
108
- inputSchema: {
109
- type: 'object',
110
- properties: {
111
- count: { type: 'number', description: 'max recent messages to return' },
112
- },
113
- },
114
- outputSchema: {
115
- type: 'object',
116
- properties: { frames: { type: 'array', items: payloadSchema } },
117
- },
118
- annotations: { readOnlyHint: true, destructiveHint: false },
119
- })
120
- continue
121
- }
122
- tools.push({
123
- name: operation.name,
124
- description: `Publish a message to the "${operation.socketName}" socket`,
125
- inputSchema: payloadSchema,
126
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
127
- })
128
- }
129
- }
130
- return tools
131
- }
132
-
133
- /*
134
- MCP prompts derived from src/mcp/prompts. Arguments come from the JSON
135
- Schema the resolver built from each prompt's frontmatter `arguments` list
136
- (top-level properties + required flags); the model fills them in and the
137
- framework interpolates them into the body on getPrompt.
138
- */
139
- export function buildPrompts(): PromptDescriptor[] {
140
- return Array.from(promptRegistry.values()).map((entry) => {
141
- const jsonSchema = entry.jsonSchema ?? {}
142
- const properties = (jsonSchema.properties ?? {}) as Record<string, { description?: string }>
143
- const required = new Set((jsonSchema.required as string[] | undefined) ?? [])
144
- return {
145
- name: entry.prompt.name,
146
- ...(entry.prompt.description ? { description: entry.prompt.description } : {}),
147
- arguments: Object.entries(properties).map(([argName, prop]) => ({
148
- name: argName,
149
- ...(prop?.description ? { description: prop.description } : {}),
150
- required: required.has(argName),
151
- })),
152
- }
153
- })
154
- }
155
-
156
22
  /* MCP tool-dispatch spans, opt-in via DEBUG=abide:mcp — a model's tool call,
157
23
  wrapping the underlying rpc dispatch in the same trace. */
158
24
  const mcpLog = abideLog.channel('abide:mcp')
159
25
 
160
- function textResult(text: string, isError = false): ToolResult {
161
- return { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) }
162
- }
163
-
164
- /*
165
- Dispatches the socket tail / publish tools by matching the tool name
166
- against each mcp-exposed socket's operations (socketOperations is the same
167
- projection tools/list advertised, so the publish op only exists when the
168
- socket allows it). tail returns the retained tail (request/response
169
- can't hold a live subscription); publish validates against the socket
170
- schema and fans out. Returns undefined when the name isn't a known socket
171
- tool so callTool can fall through to "unknown tool".
172
- */
173
- function callSocketTool(
174
- toolName: string,
175
- args: Record<string, unknown> | undefined,
176
- ): ToolResult | undefined {
177
- for (const entry of socketRegistry.values()) {
178
- if (!entry.clients.mcp) {
179
- continue
180
- }
181
- const operation = socketOperations(entry).find((op) => op.name === toolName)
182
- if (!operation) {
183
- continue
184
- }
185
- if (operation.kind === 'tail') {
186
- const count = typeof args?.count === 'number' ? args.count : undefined
187
- const frames = entry.snapshotTail(count)
188
- return {
189
- content: [{ type: 'text', text: frames.map((f) => JSON.stringify(f)).join('\n') }],
190
- structuredContent: { frames },
191
- }
192
- }
193
- try {
194
- // publish() validates the payload against the socket schema and throws on failure.
195
- entry.socket.publish(args)
196
- } catch (error) {
197
- return textResult(messageFromError(error), true)
198
- }
199
- return textResult('ok')
200
- }
201
- return undefined
202
- }
203
-
204
- /*
205
- Tool dispatch. RPC tools synthesize a Request (with forwarded auth
206
- headers from `inbound`) and pipe it through rpc.fetch inside the request
207
- scope — the same seam the HTTP router crosses, so validation, the handler,
208
- and the request-scoped helpers (per-call cache(), cookies(), request())
209
- behave identically. A handler throw is caught by the scope and framed as
210
- an isError tool result (via the 500 response) rather than escaping. The
211
- response (buffered or streaming) is framed by toolResultFromResponse.
212
- Socket tools (`<base>-tail` / `<base>-publish`) fall through to the socket
213
- dispatcher.
214
- */
215
- export async function callTool(
216
- toolName: string,
217
- args: Record<string, unknown> | undefined,
218
- inbound: Request,
219
- ): Promise<ToolResult> {
220
- const entry = findRpcByCommandName(toolName)
221
- if (entry) {
222
- /* A rpc owns this name. If it isn't mcp-exposed it's still unavailable —
223
- don't fall through to a socket op that happens to share the name. */
224
- if (!entry.clients.mcp) {
225
- throw new Error(`unknown tool: ${toolName}`)
226
- }
227
- const response = await dispatchRpcInProcess({
228
- remote: entry.remote,
229
- args,
230
- baseUrl: `${new URL(inbound.url).origin}/`,
231
- headers: forwardHeaders(inbound.headers),
232
- })
233
- return toolResultFromResponse(response)
234
- }
235
- const socketResult = callSocketTool(toolName, args)
236
- if (socketResult) {
237
- return socketResult
238
- }
239
- throw new Error(`unknown tool: ${toolName}`)
240
- }
241
-
242
- /*
243
- Renders a prompt: looks it up, interpolates the caller's args, and returns its
244
- optional description plus the message(s) that seed a conversation. A markdown
245
- prompt is a single user turn whose text is the interpolated template. Throws on
246
- an unknown prompt name. The one place prompt rendering lives — dispatchMcpRequest
247
- wraps this in the prompts/get wire shape, the agent loop reads the messages plain.
248
- */
249
- export function renderPrompt(
250
- name: string,
251
- args?: Record<string, unknown>,
252
- ): { description?: string; messages: PromptMessage[] } {
253
- const entry = promptRegistry.get(name)
254
- if (!entry) {
255
- throw new Error(`unknown prompt: ${name}`)
256
- }
257
- return {
258
- ...(entry.prompt.description ? { description: entry.prompt.description } : {}),
259
- messages: [
260
- { role: 'user', text: entry.prompt.render((args ?? {}) as Record<string, string>) },
261
- ],
262
- }
263
- }
264
-
265
26
  /*
266
27
  Projects the app's MCP surface for an in-process consumer bound to `request`
267
28
  — tool calls forward that request's auth headers into the rpc handler, so
@@ -274,22 +35,22 @@ export function mcpSurface(request: Request): McpSurface {
274
35
  let prompts: PromptDescriptor[] | undefined
275
36
  return {
276
37
  get tools() {
277
- tools ??= buildTools()
38
+ tools ??= mcpTools.list()
278
39
  return tools
279
40
  },
280
41
  get prompts() {
281
42
  prompts ??= buildPrompts()
282
43
  return prompts
283
44
  },
284
- call: (name, args) => mcpLog.trace(`mcp ${name}`, () => callTool(name, args, request)),
45
+ call: (name, args) => mcpLog.trace(`mcp ${name}`, () => mcpTools.call(name, args, request)),
285
46
  /* The conversation-seeding messages, without the wire-shape wrapping. */
286
47
  getPrompt: (name, args) => renderPrompt(name, args).messages,
287
48
  async listResources() {
288
- const server = getMcpResourceServer()
49
+ const server = mcpResourceServerSlot.server
289
50
  return server ? server.list() : []
290
51
  },
291
52
  async readResource(uri) {
292
- const server = getMcpResourceServer()
53
+ const server = mcpResourceServerSlot.server
293
54
  return server ? server.read(uri) : undefined
294
55
  },
295
56
  }