@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.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/bin/belte.ts +136 -0
- package/package.json +80 -0
- package/src/App.svelte +31 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +832 -0
- package/src/build.ts +144 -0
- package/src/buildCli.ts +160 -0
- package/src/cliEntry.ts +31 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/devEntry.ts +33 -0
- package/src/discoveryEntry.ts +33 -0
- package/src/lib/browser/cache.ts +191 -0
- package/src/lib/browser/page.svelte.ts +215 -0
- package/src/lib/browser/remoteProxy.ts +44 -0
- package/src/lib/browser/socketChannel.ts +182 -0
- package/src/lib/browser/socketProxy.ts +64 -0
- package/src/lib/browser/startClient.ts +132 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/cli/createClient.ts +126 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +44 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +70 -0
- package/src/lib/cli/runCli.ts +88 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +12 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +40 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +294 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +25 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/HttpError.ts +19 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/cli/buildEnvContent.ts +18 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +124 -0
- package/src/lib/server/cli/handleCliInstall.ts +20 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +27 -0
- package/src/lib/server/error.ts +56 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +40 -0
- package/src/lib/server/prompt.ts +30 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/types/Prompt.ts +14 -0
- package/src/lib/server/prompts/types/PromptMessage.ts +10 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +17 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +15 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +37 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +103 -0
- package/src/lib/server/rpc/parseArgs.ts +60 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/HttpVerb.ts +1 -0
- package/src/lib/server/rpc/types/RawRemoteFunction.ts +13 -0
- package/src/lib/server/rpc/types/RemoteFunction.ts +35 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +22 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/StandardSchemaV1.ts +57 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +39 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +17 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +66 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +17 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +66 -0
- package/src/lib/server/runtime/createServer.ts +555 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/registryManifests.ts +48 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +84 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/streamFromIterator.ts +76 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/CompileTarget.ts +6 -0
- package/src/lib/server/runtime/types/RequestStore.ts +15 -0
- package/src/lib/server/runtime/types/SvelteConfig.ts +5 -0
- package/src/lib/server/server.ts +19 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +267 -0
- package/src/lib/server/sockets/defineSocket.ts +160 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +18 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +47 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/buildRpcRequest.ts +61 -0
- package/src/lib/shared/cacheControlValues.ts +8 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +24 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +42 -0
- package/src/lib/shared/createPushIterator.ts +77 -0
- package/src/lib/shared/createRemoteFunction.ts +89 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +28 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +38 -0
- package/src/lib/shared/keyForRemoteCall.ts +38 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/preparePromptModule.ts +36 -0
- package/src/lib/shared/prepareRpcModule.ts +51 -0
- package/src/lib/shared/prepareSocketModule.ts +37 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +18 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/streamingContentTypes.ts +11 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +333 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +16 -0
- package/src/lib/shared/types/CacheOptions.ts +10 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +15 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/writeRoutesDts.ts +64 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverEntry.ts +47 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/package.json +16 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/rpc/getHello.ts +33 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- 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,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')
|