@briancray/belte 0.1.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 (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/belte.ts +136 -0
  4. package/package.json +80 -0
  5. package/src/App.svelte +31 -0
  6. package/src/assets/app.html +14 -0
  7. package/src/belteResolverPlugin.ts +832 -0
  8. package/src/build.ts +144 -0
  9. package/src/buildCli.ts +160 -0
  10. package/src/cliEntry.ts +31 -0
  11. package/src/clientEntry.ts +7 -0
  12. package/src/compile.ts +64 -0
  13. package/src/devEntry.ts +33 -0
  14. package/src/discoveryEntry.ts +33 -0
  15. package/src/lib/browser/cache.ts +191 -0
  16. package/src/lib/browser/page.svelte.ts +215 -0
  17. package/src/lib/browser/remoteProxy.ts +44 -0
  18. package/src/lib/browser/socketChannel.ts +182 -0
  19. package/src/lib/browser/socketProxy.ts +64 -0
  20. package/src/lib/browser/startClient.ts +132 -0
  21. package/src/lib/browser/subscribe.ts +131 -0
  22. package/src/lib/browser/types/Layouts.ts +7 -0
  23. package/src/lib/browser/types/Pages.ts +7 -0
  24. package/src/lib/cli/createClient.ts +126 -0
  25. package/src/lib/cli/loadEnvFromBinaryDir.ts +44 -0
  26. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  27. package/src/lib/cli/printHelp.ts +70 -0
  28. package/src/lib/cli/runCli.ts +88 -0
  29. package/src/lib/cli/types/CliManifest.ts +9 -0
  30. package/src/lib/cli/types/CliManifestEntry.ts +12 -0
  31. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  32. package/src/lib/mcp/createMcpServer.ts +40 -0
  33. package/src/lib/mcp/dispatchMcpRequest.ts +294 -0
  34. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  35. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  36. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  37. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  38. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  39. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  40. package/src/lib/mcp/types/McpServer.ts +9 -0
  41. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  42. package/src/lib/server/AppModule.ts +25 -0
  43. package/src/lib/server/DELETE.ts +9 -0
  44. package/src/lib/server/GET.ts +9 -0
  45. package/src/lib/server/HEAD.ts +9 -0
  46. package/src/lib/server/HttpError.ts +19 -0
  47. package/src/lib/server/PATCH.ts +9 -0
  48. package/src/lib/server/POST.ts +9 -0
  49. package/src/lib/server/PUT.ts +9 -0
  50. package/src/lib/server/cli/buildEnvContent.ts +18 -0
  51. package/src/lib/server/cli/createTarGz.ts +76 -0
  52. package/src/lib/server/cli/handleCliDownload.ts +124 -0
  53. package/src/lib/server/cli/handleCliInstall.ts +20 -0
  54. package/src/lib/server/cli/installScript.ts +29 -0
  55. package/src/lib/server/cli/maxSourceMtime.ts +27 -0
  56. package/src/lib/server/error.ts +56 -0
  57. package/src/lib/server/json.ts +28 -0
  58. package/src/lib/server/jsonl.ts +40 -0
  59. package/src/lib/server/prompt.ts +30 -0
  60. package/src/lib/server/prompts/definePrompt.ts +20 -0
  61. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  62. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  63. package/src/lib/server/prompts/types/Prompt.ts +14 -0
  64. package/src/lib/server/prompts/types/PromptMessage.ts +10 -0
  65. package/src/lib/server/prompts/types/PromptOptions.ts +17 -0
  66. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +15 -0
  67. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  68. package/src/lib/server/redirect.ts +37 -0
  69. package/src/lib/server/request.ts +18 -0
  70. package/src/lib/server/rpc/defineVerb.ts +103 -0
  71. package/src/lib/server/rpc/parseArgs.ts +60 -0
  72. package/src/lib/server/rpc/registerVerb.ts +6 -0
  73. package/src/lib/server/rpc/types/HttpVerb.ts +1 -0
  74. package/src/lib/server/rpc/types/RawRemoteFunction.ts +13 -0
  75. package/src/lib/server/rpc/types/RemoteFunction.ts +35 -0
  76. package/src/lib/server/rpc/types/RemoteHandler.ts +22 -0
  77. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  78. package/src/lib/server/rpc/types/StandardSchemaV1.ts +57 -0
  79. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  80. package/src/lib/server/rpc/types/VerbHelper.ts +39 -0
  81. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +17 -0
  82. package/src/lib/server/rpc/unprocessed.ts +14 -0
  83. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  84. package/src/lib/server/runtime/buildOpenApiSpec.ts +66 -0
  85. package/src/lib/server/runtime/cacheControlForAsset.ts +17 -0
  86. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  87. package/src/lib/server/runtime/createPublicAssetServer.ts +66 -0
  88. package/src/lib/server/runtime/createServer.ts +555 -0
  89. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  90. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  91. package/src/lib/server/runtime/registryManifests.ts +48 -0
  92. package/src/lib/server/runtime/requestContext.ts +5 -0
  93. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  94. package/src/lib/server/runtime/serializeCacheSnapshot.ts +84 -0
  95. package/src/lib/server/runtime/serverSlot.ts +13 -0
  96. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  97. package/src/lib/server/runtime/streamFromIterator.ts +76 -0
  98. package/src/lib/server/runtime/types/Assets.ts +1 -0
  99. package/src/lib/server/runtime/types/CompileTarget.ts +6 -0
  100. package/src/lib/server/runtime/types/RequestStore.ts +15 -0
  101. package/src/lib/server/runtime/types/SvelteConfig.ts +5 -0
  102. package/src/lib/server/server.ts +19 -0
  103. package/src/lib/server/socket.ts +31 -0
  104. package/src/lib/server/sockets/createSocketDispatcher.ts +267 -0
  105. package/src/lib/server/sockets/defineSocket.ts +160 -0
  106. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  107. package/src/lib/server/sockets/registerSocket.ts +6 -0
  108. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  109. package/src/lib/server/sockets/types/Socket.ts +21 -0
  110. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  111. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  112. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +18 -0
  113. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  114. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  115. package/src/lib/server/sse.ts +47 -0
  116. package/src/lib/shared/activeCacheStore.ts +20 -0
  117. package/src/lib/shared/buildRpcRequest.ts +61 -0
  118. package/src/lib/shared/cacheControlValues.ts +8 -0
  119. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  120. package/src/lib/shared/canonicalJson.ts +24 -0
  121. package/src/lib/shared/commandNameForUrl.ts +17 -0
  122. package/src/lib/shared/createCacheStore.ts +42 -0
  123. package/src/lib/shared/createPushIterator.ts +77 -0
  124. package/src/lib/shared/createRemoteFunction.ts +89 -0
  125. package/src/lib/shared/decodeResponse.ts +47 -0
  126. package/src/lib/shared/detectTarget.ts +27 -0
  127. package/src/lib/shared/findExportCallSite.ts +479 -0
  128. package/src/lib/shared/forwardHeaders.ts +28 -0
  129. package/src/lib/shared/getRemoteMeta.ts +5 -0
  130. package/src/lib/shared/isDebugEnabled.ts +23 -0
  131. package/src/lib/shared/jsonSchemaForSchema.ts +38 -0
  132. package/src/lib/shared/keyForRemoteCall.ts +38 -0
  133. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  134. package/src/lib/shared/log.ts +104 -0
  135. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  136. package/src/lib/shared/normalizeTarget.ts +10 -0
  137. package/src/lib/shared/pageUrlForFile.ts +14 -0
  138. package/src/lib/shared/parseRouteSegments.ts +22 -0
  139. package/src/lib/shared/preparePromptModule.ts +36 -0
  140. package/src/lib/shared/prepareRpcModule.ts +51 -0
  141. package/src/lib/shared/prepareSocketModule.ts +37 -0
  142. package/src/lib/shared/programNameForPackage.ts +14 -0
  143. package/src/lib/shared/promptNameForFile.ts +10 -0
  144. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  145. package/src/lib/shared/remoteMetaStore.ts +16 -0
  146. package/src/lib/shared/resolveClientFlags.ts +18 -0
  147. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  148. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  149. package/src/lib/shared/socketNameForFile.ts +11 -0
  150. package/src/lib/shared/streamingContentTypes.ts +11 -0
  151. package/src/lib/shared/stripImport.ts +27 -0
  152. package/src/lib/shared/subscribableFromResponse.ts +333 -0
  153. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  154. package/src/lib/shared/types/CacheEntry.ts +16 -0
  155. package/src/lib/shared/types/CacheOptions.ts +10 -0
  156. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  157. package/src/lib/shared/types/CacheStore.ts +15 -0
  158. package/src/lib/shared/types/ClientFlags.ts +11 -0
  159. package/src/lib/shared/types/Subscribable.ts +15 -0
  160. package/src/lib/shared/writeRoutesDts.ts +64 -0
  161. package/src/preload.ts +20 -0
  162. package/src/scaffold.ts +92 -0
  163. package/src/serverEntry.ts +47 -0
  164. package/src/sveltePlugin.ts +58 -0
  165. package/src/tailwindStylePreprocessor.ts +62 -0
  166. package/template/package.json +16 -0
  167. package/template/src/app.ts +23 -0
  168. package/template/src/browser/app.css +21 -0
  169. package/template/src/browser/app.html +24 -0
  170. package/template/src/browser/pages/about/page.svelte +5 -0
  171. package/template/src/browser/pages/layout.svelte +26 -0
  172. package/template/src/browser/pages/page.svelte +20 -0
  173. package/template/src/cli/banner.txt +3 -0
  174. package/template/src/cli/footer.txt +1 -0
  175. package/template/src/server/rpc/getHello.ts +33 -0
  176. package/template/svelte.config.js +12 -0
  177. package/template/tsconfig.json +18 -0
  178. package/tsconfig.app.json +16 -0
@@ -0,0 +1,101 @@
1
+ // node:fs existsSync — cheap sync presence check, mirrors createPublicAssetServer
2
+ import { existsSync } from 'node:fs'
3
+ import { Glob } from 'bun'
4
+ import { mimeForExtension } from '../server/runtime/mimeForExtension.ts'
5
+ import type { Assets } from '../server/runtime/types/Assets.ts'
6
+ import type { McpResourceContents } from './types/McpResourceContents.ts'
7
+ import type { McpResourceDescriptor } from './types/McpResourceDescriptor.ts'
8
+ import type { McpResourceServer } from './types/McpResourceServer.ts'
9
+
10
+ /*
11
+ The belte:// URI namespace for file-based resources. A resource's URI is this
12
+ prefix followed by its path relative to src/mcp/resources.
13
+ */
14
+ const URI_PREFIX = 'belte://resources/'
15
+
16
+ /*
17
+ MIME essences returned inline as UTF-8 `text` in resources/read; everything
18
+ else is returned as a base64 `blob`. The essence is taken before any `;charset`
19
+ parameter that Bun.file().type appends.
20
+ */
21
+ function isTextMime(mime: string): boolean {
22
+ const essence = mime.split(';')[0].trim()
23
+ return (
24
+ essence.startsWith('text/') ||
25
+ essence === 'application/json' ||
26
+ essence === 'application/xml' ||
27
+ essence === 'image/svg+xml' ||
28
+ essence.endsWith('+json') ||
29
+ essence.endsWith('+xml')
30
+ )
31
+ }
32
+
33
+ function descriptorFor(relativePath: string): McpResourceDescriptor {
34
+ return {
35
+ uri: `${URI_PREFIX}${relativePath}`,
36
+ name: relativePath,
37
+ mimeType: mimeForExtension(relativePath),
38
+ }
39
+ }
40
+
41
+ function contentsFor(relativePath: string, bytes: Uint8Array): McpResourceContents {
42
+ const mimeType = mimeForExtension(relativePath)
43
+ const uri = `${URI_PREFIX}${relativePath}`
44
+ if (isTextMime(mimeType)) {
45
+ return { uri, mimeType, text: new TextDecoder().decode(bytes) }
46
+ }
47
+ return { uri, mimeType, blob: bytes.toBase64() }
48
+ }
49
+
50
+ /*
51
+ Serves files under src/mcp/resources as MCP resources. Two sources, picked at
52
+ construction (mirrors createPublicAssetServer):
53
+ - `mcpResources` (standalone compile): a map of relative-path → zstd bytes
54
+ embedded into the binary.
55
+ - `resourcesDir` on disk (dev + `belte start`): files read straight from
56
+ `${cwd}/src/mcp/resources`.
57
+ */
58
+ export function createMcpResourceServer({
59
+ resourcesDir,
60
+ mcpResources,
61
+ }: {
62
+ resourcesDir: string
63
+ mcpResources?: Assets
64
+ }): McpResourceServer {
65
+ return {
66
+ async list(): Promise<McpResourceDescriptor[]> {
67
+ if (mcpResources) {
68
+ return Object.keys(mcpResources).map(descriptorFor)
69
+ }
70
+ if (!existsSync(resourcesDir)) {
71
+ return []
72
+ }
73
+ const files = await Array.fromAsync(
74
+ new Glob('**/*').scan({ cwd: resourcesDir, onlyFiles: true }),
75
+ )
76
+ return files.map(descriptorFor)
77
+ },
78
+ async read(uri: string): Promise<McpResourceContents | undefined> {
79
+ if (!uri.startsWith(URI_PREFIX)) {
80
+ return undefined
81
+ }
82
+ const relativePath = uri.slice(URI_PREFIX.length)
83
+ // reject `..` traversal in the requested uri before any disk read
84
+ if (relativePath.split('/').includes('..')) {
85
+ return undefined
86
+ }
87
+ if (mcpResources) {
88
+ const compressed = mcpResources[relativePath]
89
+ if (!compressed) {
90
+ return undefined
91
+ }
92
+ return contentsFor(relativePath, Bun.zstdDecompressSync(compressed))
93
+ }
94
+ const file = Bun.file(`${resourcesDir}/${relativePath}`)
95
+ if (!(await file.exists())) {
96
+ return undefined
97
+ }
98
+ return contentsFor(relativePath, await file.bytes())
99
+ },
100
+ }
101
+ }
@@ -0,0 +1,40 @@
1
+ import { dispatchMcpRequest, MCP_NO_STORE_HEADERS } from './dispatchMcpRequest.ts'
2
+ import type { McpServer } from './types/McpServer.ts'
3
+ import type { McpServerOptions } from './types/McpServerOptions.ts'
4
+
5
+ const DEFAULT_NAME = 'belte-app'
6
+ const DEFAULT_VERSION = '0.0.0'
7
+
8
+ /*
9
+ Constructs an MCP server bound to the project's rpc registry. Returns an
10
+ object whose `handle(request)` is the function the bun route at
11
+ /__belte/mcp invokes. Framework-internal — the belte:mcp virtual
12
+ default-constructs it; there is no user-authored server module. Server
13
+ name/version default from package.json.
14
+
15
+ Tools are derived from every verb with `clients.mcp: true` (auto-on when
16
+ the verb carries a schema) — one tool per rpc regardless of HTTP verb.
17
+ Sockets are not exposed to MCP. Auth inherits from the inbound request —
18
+ bearer / cookie headers
19
+ flow into the synthesized Request that hits each rpc handler. An optional
20
+ `authorize` hook in opts can short-circuit the request before any tool
21
+ dispatches.
22
+ */
23
+ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
24
+ const serverInfo = {
25
+ name: opts.name ?? DEFAULT_NAME,
26
+ version: opts.version ?? DEFAULT_VERSION,
27
+ }
28
+ return {
29
+ async handle(request: Request): Promise<Response> {
30
+ if (request.method !== 'POST') {
31
+ return new Response('Method Not Allowed', {
32
+ status: 405,
33
+ headers: { Allow: 'POST', 'Cache-Control': 'no-store' },
34
+ })
35
+ }
36
+ const envelope = await dispatchMcpRequest(request, opts, serverInfo)
37
+ return new Response(JSON.stringify(envelope), { headers: MCP_NO_STORE_HEADERS })
38
+ },
39
+ }
40
+ }
@@ -0,0 +1,294 @@
1
+ import { promptRegistry } from '../server/prompts/promptRegistry.ts'
2
+ import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
3
+ import { verbRegistry } from '../server/rpc/verbRegistry.ts'
4
+ import { ensureRegistriesLoaded } from '../server/runtime/registryManifests.ts'
5
+ import { buildRpcRequest } from '../shared/buildRpcRequest.ts'
6
+ import { NO_STORE } from '../shared/cacheControlValues.ts'
7
+ import { commandNameForUrl } from '../shared/commandNameForUrl.ts'
8
+ import { decodeResponse } from '../shared/decodeResponse.ts'
9
+ import { forwardHeaders } from '../shared/forwardHeaders.ts'
10
+ import { jsonSchemaForSchema } from '../shared/jsonSchemaForSchema.ts'
11
+ import { getMcpResourceServer } from './mcpResourceServerSlot.ts'
12
+ import type { JsonRpcRequest } from './types/JsonRpcRequest.ts'
13
+ import type { JsonRpcResponse } from './types/JsonRpcResponse.ts'
14
+ import type { McpServerOptions } from './types/McpServerOptions.ts'
15
+
16
+ const PROTOCOL_VERSION = '2025-06-18'
17
+
18
+ function jsonRpcError(
19
+ id: string | number | null,
20
+ code: number,
21
+ message: string,
22
+ data?: unknown,
23
+ ): JsonRpcResponse {
24
+ return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } }
25
+ }
26
+
27
+ function jsonRpcOk(id: string | number | null, result: unknown): JsonRpcResponse {
28
+ return { jsonrpc: '2.0', id, result }
29
+ }
30
+
31
+ /*
32
+ Builds the array of MCP tool descriptors. Every rpc with clients.mcp=true
33
+ becomes one tool named after the export's URL (folder segments joined
34
+ with `-`), regardless of HTTP verb — GET reads and mutating verbs alike.
35
+ Sockets are never exposed to MCP.
36
+ */
37
+ function buildTools(): Array<{
38
+ name: string
39
+ description: string
40
+ inputSchema: Record<string, unknown>
41
+ }> {
42
+ const tools: Array<{
43
+ name: string
44
+ description: string
45
+ inputSchema: Record<string, unknown>
46
+ }> = []
47
+ for (const entry of verbRegistry.values()) {
48
+ if (!entry.clients.mcp) {
49
+ continue
50
+ }
51
+ tools.push({
52
+ name: commandNameForUrl(entry.remote.url),
53
+ description: `${entry.remote.method} ${entry.remote.url}`,
54
+ inputSchema: jsonSchemaForSchema(entry.schema, entry.jsonSchema),
55
+ })
56
+ }
57
+ return tools
58
+ }
59
+
60
+ /*
61
+ MCP prompts derived from src/mcp/prompts. Arguments come from the
62
+ prompt's schema (top-level properties + required flags); the model fills
63
+ them in and the framework validates + renders on prompts/get.
64
+ */
65
+ function buildPrompts(): Array<{
66
+ name: string
67
+ description?: string
68
+ arguments: Array<{ name: string; description?: string; required: boolean }>
69
+ }> {
70
+ return Array.from(promptRegistry.values()).map((entry) => {
71
+ const jsonSchema = jsonSchemaForSchema(entry.schema, entry.jsonSchema)
72
+ const properties = (jsonSchema.properties ?? {}) as Record<string, { description?: string }>
73
+ const required = new Set((jsonSchema.required as string[] | undefined) ?? [])
74
+ return {
75
+ name: entry.prompt.name,
76
+ ...(entry.prompt.description ? { description: entry.prompt.description } : {}),
77
+ arguments: Object.entries(properties).map(([argName, prop]) => ({
78
+ name: argName,
79
+ ...(prop?.description ? { description: prop.description } : {}),
80
+ required: required.has(argName),
81
+ })),
82
+ }
83
+ })
84
+ }
85
+
86
+ /*
87
+ Tool dispatch. Synthesizes a Request (with forwarded auth headers) and
88
+ pipes it through verb.fetch — the same code path the HTTP router uses, so
89
+ validation + handler + error helpers behave identically. Every rpc is a
90
+ tool regardless of verb.
91
+ */
92
+ async function callTool(
93
+ toolName: string,
94
+ args: Record<string, unknown> | undefined,
95
+ inbound: Request,
96
+ ): Promise<Record<string, unknown>> {
97
+ let found: ReturnType<(typeof verbRegistry)['get']> | undefined
98
+ for (const entry of verbRegistry.values()) {
99
+ if (!entry.clients.mcp) {
100
+ continue
101
+ }
102
+ if (commandNameForUrl(entry.remote.url) === toolName) {
103
+ found = entry
104
+ break
105
+ }
106
+ }
107
+ if (!found) {
108
+ throw new Error(`unknown tool: ${toolName}`)
109
+ }
110
+ const response = await runRpc(found.remote.method, found.remote.url, args, inbound)
111
+ if (!response.ok) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: 'text',
116
+ text: `${response.status} ${response.statusText}: ${await response.text()}`,
117
+ },
118
+ ],
119
+ isError: true,
120
+ }
121
+ }
122
+ const body = await decodeResponse(response.clone())
123
+ return {
124
+ content: [
125
+ {
126
+ type: 'text',
127
+ text: typeof body === 'string' ? body : JSON.stringify(body),
128
+ },
129
+ ],
130
+ }
131
+ }
132
+
133
+ /*
134
+ Synthesizes the rpc Request and dispatches through verb.fetch, forwarding
135
+ the inbound MCP request's auth headers so session/bearer middleware keeps
136
+ working. Shared by tool calls (non-GET) and resource reads (GET).
137
+ */
138
+ function runRpc(
139
+ method: HttpVerb,
140
+ url: string,
141
+ args: Record<string, unknown> | undefined,
142
+ inbound: Request,
143
+ ): Promise<Response> {
144
+ const inboundUrl = new URL(inbound.url)
145
+ const baseUrl = `${inboundUrl.protocol}//${inboundUrl.host}/`
146
+ const request = buildRpcRequest({
147
+ method,
148
+ url,
149
+ args,
150
+ baseUrl,
151
+ headers: forwardHeaders(inbound.headers),
152
+ })
153
+ const entry = verbRegistry.get(url)
154
+ if (entry && entry.remote.method === method) {
155
+ return entry.remote.fetch(request)
156
+ }
157
+ throw new Error(`unknown rpc: ${method} ${url}`)
158
+ }
159
+
160
+ /*
161
+ Validates prompt arguments against the prompt's schema (when present),
162
+ renders the messages, and maps them to the MCP prompts/get wire shape.
163
+ A bare string render result becomes a single user message.
164
+ */
165
+ async function getPrompt(
166
+ name: string,
167
+ args: Record<string, unknown> | undefined,
168
+ ): Promise<Record<string, unknown>> {
169
+ const entry = promptRegistry.get(name)
170
+ if (!entry) {
171
+ throw new Error(`unknown prompt: ${name}`)
172
+ }
173
+ let value: unknown = args ?? {}
174
+ if (entry.schema) {
175
+ const result = await entry.schema['~standard'].validate(value)
176
+ if (result.issues) {
177
+ throw new Error(
178
+ `prompt "${name}" arguments failed validation: ${JSON.stringify(result.issues)}`,
179
+ )
180
+ }
181
+ value = result.value
182
+ }
183
+ const rendered = await entry.prompt.render(value as Record<string, string>)
184
+ const messages =
185
+ typeof rendered === 'string'
186
+ ? [{ role: 'user', content: { type: 'text', text: rendered } }]
187
+ : rendered.map((message) => ({
188
+ role: message.role,
189
+ content: { type: 'text', text: message.text },
190
+ }))
191
+ return {
192
+ ...(entry.prompt.description ? { description: entry.prompt.description } : {}),
193
+ messages,
194
+ }
195
+ }
196
+
197
+ /*
198
+ Parses a single JSON-RPC envelope and dispatches by method. Errors
199
+ become JSON-RPC error responses (the HTTP layer always returns 200 with
200
+ an envelope for JSON-RPC over HTTP; transport errors are different).
201
+ */
202
+ export async function dispatchMcpRequest(
203
+ request: Request,
204
+ opts: McpServerOptions,
205
+ serverInfo: { name: string; version: string },
206
+ ): Promise<JsonRpcResponse> {
207
+ let envelope: JsonRpcRequest
208
+ try {
209
+ envelope = (await request.clone().json()) as JsonRpcRequest
210
+ } catch {
211
+ return jsonRpcError(null, -32700, 'Parse error')
212
+ }
213
+ const id = envelope.id ?? null
214
+ if (envelope.jsonrpc !== '2.0' || typeof envelope.method !== 'string') {
215
+ return jsonRpcError(id, -32600, 'Invalid Request')
216
+ }
217
+
218
+ if (opts.authorize) {
219
+ try {
220
+ await opts.authorize(request)
221
+ } catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error)
223
+ return jsonRpcError(id, -32001, message)
224
+ }
225
+ }
226
+
227
+ try {
228
+ await ensureRegistriesLoaded()
229
+ switch (envelope.method) {
230
+ case 'initialize':
231
+ return jsonRpcOk(id, {
232
+ protocolVersion: PROTOCOL_VERSION,
233
+ capabilities: {
234
+ tools: { listChanged: false },
235
+ prompts: { listChanged: false },
236
+ resources: { listChanged: false },
237
+ },
238
+ serverInfo,
239
+ })
240
+ case 'ping':
241
+ return jsonRpcOk(id, {})
242
+ case 'tools/list':
243
+ return jsonRpcOk(id, { tools: buildTools() })
244
+ case 'tools/call': {
245
+ const params = envelope.params as
246
+ | { name?: string; arguments?: Record<string, unknown> }
247
+ | undefined
248
+ if (!params?.name) {
249
+ return jsonRpcError(id, -32602, 'Missing tool name')
250
+ }
251
+ return jsonRpcOk(id, await callTool(params.name, params.arguments, request))
252
+ }
253
+ case 'resources/list': {
254
+ const resourceServer = getMcpResourceServer()
255
+ return jsonRpcOk(id, {
256
+ resources: resourceServer ? await resourceServer.list() : [],
257
+ })
258
+ }
259
+ case 'resources/read': {
260
+ const params = envelope.params as { uri?: string } | undefined
261
+ if (!params?.uri) {
262
+ return jsonRpcError(id, -32602, 'Missing resource uri')
263
+ }
264
+ const resourceServer = getMcpResourceServer()
265
+ const contents = resourceServer ? await resourceServer.read(params.uri) : undefined
266
+ if (!contents) {
267
+ return jsonRpcError(id, -32602, `unknown resource: ${params.uri}`)
268
+ }
269
+ return jsonRpcOk(id, { contents: [contents] })
270
+ }
271
+ case 'prompts/list':
272
+ return jsonRpcOk(id, { prompts: buildPrompts() })
273
+ case 'prompts/get': {
274
+ const params = envelope.params as
275
+ | { name?: string; arguments?: Record<string, unknown> }
276
+ | undefined
277
+ if (!params?.name) {
278
+ return jsonRpcError(id, -32602, 'Missing prompt name')
279
+ }
280
+ return jsonRpcOk(id, await getPrompt(params.name, params.arguments))
281
+ }
282
+ default:
283
+ return jsonRpcError(id, -32601, `Method not found: ${envelope.method}`)
284
+ }
285
+ } catch (error) {
286
+ const message = error instanceof Error ? error.message : String(error)
287
+ return jsonRpcError(id, -32603, message)
288
+ }
289
+ }
290
+
291
+ export const MCP_NO_STORE_HEADERS = {
292
+ 'Content-Type': 'application/json',
293
+ 'Cache-Control': NO_STORE,
294
+ } as const
@@ -0,0 +1,18 @@
1
+ import type { McpResourceServer } from './types/McpResourceServer.ts'
2
+
3
+ /*
4
+ Process-wide slot for the MCP resource server. createServer assigns it at
5
+ boot; dispatchMcpRequest reads it on resources/list + resources/read. Mirrors
6
+ the registryManifests slot — the default MCP server is constructed in the
7
+ belte:mcp virtual with no args, so the resource server (which needs the
8
+ project's resourcesDir + embedded map) is injected out of band.
9
+ */
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
18
+ }
@@ -0,0 +1,12 @@
1
+ /*
2
+ JSON-RPC 2.0 request frame as MCP delivers it over Streamable HTTP. The
3
+ `id` is absent for notifications (which we don't currently receive from
4
+ clients but accept silently). `method` is a string like "tools/list" or
5
+ "resources/read".
6
+ */
7
+ export type JsonRpcRequest = {
8
+ jsonrpc: '2.0'
9
+ id?: string | number
10
+ method: string
11
+ params?: Record<string, unknown>
12
+ }
@@ -0,0 +1,20 @@
1
+ /*
2
+ JSON-RPC 2.0 response frame. Exactly one of `result` / `error` is set
3
+ per request. The `id` echoes the inbound request id (null when the
4
+ request id was malformed and the error is being returned).
5
+ */
6
+ export type JsonRpcResponse =
7
+ | {
8
+ jsonrpc: '2.0'
9
+ id: string | number | null
10
+ result: unknown
11
+ }
12
+ | {
13
+ jsonrpc: '2.0'
14
+ id: string | number | null
15
+ error: {
16
+ code: number
17
+ message: string
18
+ data?: unknown
19
+ }
20
+ }
@@ -0,0 +1,10 @@
1
+ /*
2
+ One entry in an MCP resources/read result. Text-typed resources carry `text`;
3
+ everything else carries base64 `blob` — exactly one is present.
4
+ */
5
+ export type McpResourceContents = {
6
+ uri: string
7
+ mimeType: string
8
+ text?: string
9
+ blob?: string
10
+ }
@@ -0,0 +1,6 @@
1
+ /* One entry in an MCP resources/list result — a file under src/mcp/resources. */
2
+ export type McpResourceDescriptor = {
3
+ uri: string
4
+ name: string
5
+ mimeType: string
6
+ }
@@ -0,0 +1,12 @@
1
+ import type { McpResourceContents } from './McpResourceContents.ts'
2
+ import type { McpResourceDescriptor } from './McpResourceDescriptor.ts'
3
+
4
+ /*
5
+ Serves the project's src/mcp/resources files to the MCP dispatcher. `list`
6
+ backs resources/list; `read` backs resources/read and resolves to undefined
7
+ for an unknown uri.
8
+ */
9
+ export type McpResourceServer = {
10
+ list(): Promise<McpResourceDescriptor[]>
11
+ read(uri: string): Promise<McpResourceContents | undefined>
12
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ Public shape returned by createMcpServer. The bun route handler at
3
+ /__belte/mcp delegates inbound requests to `handle(request)`, which
4
+ parses the JSON-RPC envelope, dispatches to tools/resources, and returns
5
+ a Response carrying the JSON-RPC reply.
6
+ */
7
+ export type McpServer = {
8
+ handle(request: Request): Promise<Response>
9
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ User-facing options for createMcpServer. All fields optional — the
3
+ zero-arg call works for any belte project (server info is derived from
4
+ package.json by the bundler when MCP is wired into createServer).
5
+
6
+ - `name` / `version`: identify the server in the MCP `initialize`
7
+ response. Defaults come from the project's package.json.
8
+ - `authorize`: optional boundary check. Runs once per MCP request before
9
+ any tool/resource dispatch. Throw HttpError (or any Error) to reject.
10
+ Per-tool authorization stays in the underlying verb handler.
11
+ */
12
+ export type McpServerOptions = {
13
+ name?: string
14
+ version?: string
15
+ authorize?: (request: Request) => Promise<void> | void
16
+ }
@@ -0,0 +1,25 @@
1
+ import type { Server } from 'bun'
2
+
3
+ /*
4
+ Optional hooks exported from src/app.ts. All hooks are optional; defaults
5
+ kick in when an export is missing. init returns an optional cleanup
6
+ function that runs on SIGINT/SIGTERM. handle is single-middleware with
7
+ next so user code can mutate the response or branch on the URL.
8
+
9
+ WebSockets are not exposed here — belte's only native WebSocket
10
+ surface is the sockets hub (see `belte/sockets`), multiplexed onto a
11
+ single framework-owned connection per client at `/__belte/sockets`.
12
+ Inside request scopes, the live Bun.Server is reachable via the
13
+ exported `server` proxy from `belte/server`; `init` receives it
14
+ explicitly because it runs outside a request.
15
+ */
16
+ export type AppModule = {
17
+ init?: (ctx: {
18
+ server: Server<unknown>
19
+ }) => void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>
20
+ handle?: (
21
+ request: Request,
22
+ next: (req: Request) => Promise<Response>,
23
+ ) => Promise<Response> | Response
24
+ handleError?: (error: unknown, request: Request) => Promise<Response> | Response
25
+ }
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ DELETE verb helper. The bundler rewrites every `export const x = DELETE(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const DELETE: VerbHelper = (_fn: any, _opts?: any) => unprocessed('DELETE')
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ GET verb helper. The bundler rewrites every `export const x = GET(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const GET: VerbHelper = (_fn: any, _opts?: any) => unprocessed('GET')
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ HEAD verb helper. The bundler rewrites every `export const x = HEAD(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const HEAD: VerbHelper = (_fn: any, _opts?: any) => unprocessed('HEAD')
@@ -0,0 +1,19 @@
1
+ /*
2
+ Thrown by remote-function calls when the server responds with a non-2xx
3
+ status. Carries the raw Response so callers can inspect body, headers, or
4
+ status text — useful for showing user-friendly error UI without having to
5
+ opt every call site into the `.raw()` escape hatch.
6
+ */
7
+ export class HttpError extends Error {
8
+ readonly status: number
9
+ readonly statusText: string
10
+ readonly response: Response
11
+
12
+ constructor(response: Response) {
13
+ super(`HTTP ${response.status} ${response.statusText || 'error'}`)
14
+ this.name = 'HttpError'
15
+ this.status = response.status
16
+ this.statusText = response.statusText
17
+ this.response = response
18
+ }
19
+ }
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ PATCH verb helper. The bundler rewrites every `export const x = PATCH(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const PATCH: VerbHelper = (_fn: any, _opts?: any) => unprocessed('PATCH')
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ POST verb helper. The bundler rewrites every `export const x = POST(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const POST: VerbHelper = (_fn: any, _opts?: any) => unprocessed('POST')
@@ -0,0 +1,9 @@
1
+ import type { VerbHelper } from './rpc/types/VerbHelper.ts'
2
+ import { unprocessed } from './rpc/unprocessed.ts'
3
+
4
+ /*
5
+ PUT verb helper. The bundler rewrites every `export const x = PUT(fn)` inside
6
+ `src/server/rpc/<file>.ts` into a defineVerb call (server target) or a
7
+ remoteProxy stub (client target). Calling this directly throws.
8
+ */
9
+ export const PUT: VerbHelper = (_fn: any, _opts?: any) => unprocessed('PUT')