velocious 1.0.692 → 1.0.693
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/build/background-jobs/scheduled-enqueue-bound.js +18 -0
- package/build/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
- package/build/background-jobs/scheduler.js +121 -4
- package/build/cli/commands/mcp.js +7 -0
- package/build/cli/index.js +1 -1
- package/build/configuration-types.js +2 -0
- package/build/configuration.js +16 -1
- package/build/database/advisory-lock-bound.js +16 -0
- package/build/database/drivers/base.js +23 -2
- package/build/database/drivers/pgsql/index.js +27 -4
- package/build/environment-handlers/base.js +9 -0
- package/build/environment-handlers/node/cli/commands/mcp.js +87 -0
- package/build/environment-handlers/node.js +10 -0
- package/build/mcp/dispatcher.js +271 -0
- package/build/mcp/json-rpc-server.js +167 -0
- package/build/mcp/manifest-tools.js +310 -0
- package/build/routes/catch-all-route.js +58 -0
- package/build/routes/resolver.js +21 -8
- package/build/routes/root-route.js +26 -1
- package/build/src/background-jobs/scheduled-enqueue-bound.d.ts +15 -0
- package/build/src/background-jobs/scheduled-enqueue-bound.d.ts.map +1 -0
- package/build/src/background-jobs/scheduled-enqueue-bound.js +17 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts +24 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.d.ts.map +1 -0
- package/build/src/background-jobs/scheduled-enqueue-timeout-error.js +23 -0
- package/build/src/background-jobs/scheduler.d.ts +40 -0
- package/build/src/background-jobs/scheduler.d.ts.map +1 -1
- package/build/src/background-jobs/scheduler.js +111 -5
- package/build/src/cli/commands/mcp.d.ts +5 -0
- package/build/src/cli/commands/mcp.d.ts.map +1 -0
- package/build/src/cli/commands/mcp.js +7 -0
- package/build/src/cli/index.js +2 -2
- package/build/src/configuration-types.d.ts +10 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +3 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +13 -2
- package/build/src/database/advisory-lock-bound.d.ts +15 -0
- package/build/src/database/advisory-lock-bound.d.ts.map +1 -0
- package/build/src/database/advisory-lock-bound.js +16 -0
- package/build/src/database/drivers/base.d.ts +13 -1
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +23 -3
- package/build/src/database/drivers/pgsql/index.d.ts +15 -3
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +23 -5
- package/build/src/environment-handlers/base.d.ts +6 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +9 -1
- package/build/src/environment-handlers/node/cli/commands/mcp.d.ts +19 -0
- package/build/src/environment-handlers/node/cli/commands/mcp.d.ts.map +1 -0
- package/build/src/environment-handlers/node/cli/commands/mcp.js +70 -0
- package/build/src/environment-handlers/node.d.ts +6 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +10 -1
- package/build/src/mcp/dispatcher.d.ts +133 -0
- package/build/src/mcp/dispatcher.d.ts.map +1 -0
- package/build/src/mcp/dispatcher.js +232 -0
- package/build/src/mcp/json-rpc-server.d.ts +47 -0
- package/build/src/mcp/json-rpc-server.d.ts.map +1 -0
- package/build/src/mcp/json-rpc-server.js +142 -0
- package/build/src/mcp/manifest-tools.d.ts +76 -0
- package/build/src/mcp/manifest-tools.d.ts.map +1 -0
- package/build/src/mcp/manifest-tools.js +273 -0
- package/build/src/routes/catch-all-route.d.ts +48 -0
- package/build/src/routes/catch-all-route.d.ts.map +1 -0
- package/build/src/routes/catch-all-route.js +50 -0
- package/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +23 -9
- package/build/src/routes/root-route.d.ts +15 -1
- package/build/src/routes/root-route.d.ts.map +1 -1
- package/build/src/routes/root-route.js +24 -2
- package/package.json +1 -1
- package/src/background-jobs/scheduled-enqueue-bound.js +18 -0
- package/src/background-jobs/scheduled-enqueue-timeout-error.js +25 -0
- package/src/background-jobs/scheduler.js +121 -4
- package/src/cli/commands/mcp.js +7 -0
- package/src/cli/index.js +1 -1
- package/src/configuration-types.js +2 -0
- package/src/configuration.js +16 -1
- package/src/database/advisory-lock-bound.js +16 -0
- package/src/database/drivers/base.js +23 -2
- package/src/database/drivers/pgsql/index.js +27 -4
- package/src/environment-handlers/base.js +9 -0
- package/src/environment-handlers/node/cli/commands/mcp.js +87 -0
- package/src/environment-handlers/node.js +10 -0
- package/src/mcp/dispatcher.js +271 -0
- package/src/mcp/json-rpc-server.js +167 -0
- package/src/mcp/manifest-tools.js +310 -0
- package/src/routes/catch-all-route.js +58 -0
- package/src/routes/resolver.js +21 -8
- package/src/routes/root-route.js +26 -1
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {object} McpToolResult
|
|
5
|
+
* @property {Array<{text: string, type: "text"}>} content - Tool result content blocks.
|
|
6
|
+
* @property {boolean} isError - Whether the tool call failed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} FrontendModelRequestEntry
|
|
11
|
+
* @property {string} [commandType] - Built-in command type.
|
|
12
|
+
* @property {string} [customPath] - Custom command path.
|
|
13
|
+
* @property {string} model - Backend model name.
|
|
14
|
+
* @property {Record<string, unknown>} payload - Command payload.
|
|
15
|
+
* @property {Record<string, string | number | boolean> | undefined} [requestContext] - Remote request context.
|
|
16
|
+
* @property {string} requestId - Request correlation id.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {Record<string, unknown> & {formatVersion?: number, resources?: Record<string, unknown>}} ApiManifest
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} ManifestCommandEntry
|
|
25
|
+
* @property {Array<{name: string, type: string}>} [args] - Declared typed command arguments.
|
|
26
|
+
* @property {boolean} [isBuiltIn] - Whether the entry is a synthesized built-in command.
|
|
27
|
+
* @property {string} methodName - Command method name.
|
|
28
|
+
* @property {string} [path] - Manifest command path (member paths carry `<id>`).
|
|
29
|
+
* @property {string | null} [returnType] - Declared JSDoc return type.
|
|
30
|
+
* @property {"collection" | "member"} scope - Command scope.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {object} ManifestResourceEntry
|
|
35
|
+
* @property {Record<string, ManifestCommandEntry[]>} [commands] - Scope-keyed custom command entries.
|
|
36
|
+
* @property {Record<string, Record<string, string>>} [builtInCommands] - Scope-keyed built-in command maps.
|
|
37
|
+
* @property {string} modelName - Backend model name.
|
|
38
|
+
* @property {string | string[]} [primaryKey] - Primary key attribute name(s).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import FrontendModelController from "../frontend-model-controller.js"
|
|
42
|
+
import Request from "../http-server/client/request.js"
|
|
43
|
+
import Response from "../http-server/client/response.js"
|
|
44
|
+
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "../frontend-models/transport-serialization.js"
|
|
45
|
+
import {generateMcpTools, metaToolName, parseMcpToolName} from "./manifest-tools.js"
|
|
46
|
+
|
|
47
|
+
const MCP_REQUEST_PATH = "/frontend-models"
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Lists the MCP tools exposed by a configuration's frontend-model API manifest.
|
|
51
|
+
* @param {object} args - Arguments.
|
|
52
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
53
|
+
* @returns {Promise<Record<string, unknown>[]>} - MCP `tools/list` tool definitions.
|
|
54
|
+
*/
|
|
55
|
+
export async function listMcpTools({configuration}) {
|
|
56
|
+
const manifest = /** @type {ApiManifest} */ (await configuration.getApiManifest())
|
|
57
|
+
|
|
58
|
+
return generateMcpTools(manifest)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Executes one MCP `tools/call` against the frontend-model dispatcher.
|
|
63
|
+
* The tool call is translated into the exact shared frontend-model request entry
|
|
64
|
+
* (`commandType`/`customPath`, `model`, `payload`, `requestContext`) and runs
|
|
65
|
+
* through `FrontendModelController.frontendApi()` in-process, so tenant and
|
|
66
|
+
* ability resolution stay inside the owning dispatcher — MCP never bypasses them.
|
|
67
|
+
* @param {object} args - Arguments.
|
|
68
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
69
|
+
* @param {Record<string, string | number | boolean> | undefined} [args.requestContext] - Remote request context forwarded to the app's ability/tenant resolvers.
|
|
70
|
+
* @param {Record<string, unknown>} args.toolArguments - Tool arguments from the MCP client.
|
|
71
|
+
* @param {string} args.toolName - MCP tool name.
|
|
72
|
+
* @returns {Promise<McpToolResult>} - MCP tool call result.
|
|
73
|
+
*/
|
|
74
|
+
export async function callMcpTool({configuration, requestContext, toolArguments, toolName}) {
|
|
75
|
+
const manifest = /** @type {ApiManifest} */ (await configuration.getApiManifest())
|
|
76
|
+
|
|
77
|
+
if (toolName === metaToolName()) {
|
|
78
|
+
return textResult({value: manifest})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const identity = parseMcpToolName({manifest, toolName})
|
|
82
|
+
|
|
83
|
+
if (!identity) return errorResult(`Unknown MCP tool '${toolName}'.`)
|
|
84
|
+
|
|
85
|
+
const manifestResources = /** @type {Record<string, unknown>} */ (manifest.resources || {})
|
|
86
|
+
const resource = /** @type {ManifestResourceEntry} */ (manifestResources[identity.modelKey])
|
|
87
|
+
const commandEntry = findManifestCommandEntry({commandName: identity.commandName, resource, scope: identity.scope})
|
|
88
|
+
|
|
89
|
+
if (!commandEntry) return errorResult(`Manifest command entry missing for tool '${toolName}'.`)
|
|
90
|
+
|
|
91
|
+
const requestEntry = buildFrontendModelRequestEntry({commandEntry, model: String(resource.modelName), requestContext, toolArguments})
|
|
92
|
+
const responsePayload = await runFrontendApiRequest({configuration, requestEntry})
|
|
93
|
+
const responseEntry = Array.isArray(responsePayload?.responses) ? responsePayload.responses[0] : undefined
|
|
94
|
+
const commandResponse = responseEntry?.response
|
|
95
|
+
|
|
96
|
+
if (!commandResponse) return errorResult("Frontend-model dispatcher returned no response.")
|
|
97
|
+
|
|
98
|
+
if (commandResponse.status === "error") {
|
|
99
|
+
return errorResult(commandResponse.errorMessage || "Frontend-model command failed.", commandResponse)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return textResult({value: commandResponse})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Finds the manifest command entry matching a parsed tool identity.
|
|
107
|
+
* @param {object} args - Arguments.
|
|
108
|
+
* @param {string} args.commandName - Command method name.
|
|
109
|
+
* @param {ManifestResourceEntry} args.resource - Manifest resource entry.
|
|
110
|
+
* @param {"collection" | "member"} args.scope - Command scope.
|
|
111
|
+
* @returns {ManifestCommandEntry | undefined} - Manifest command entry.
|
|
112
|
+
*/
|
|
113
|
+
function findManifestCommandEntry({commandName, resource, scope}) {
|
|
114
|
+
if (resource.builtInCommands?.[scope]?.[commandName]) {
|
|
115
|
+
return {isBuiltIn: true, methodName: commandName, scope}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const scopeCommands = resource.commands?.[scope]
|
|
119
|
+
|
|
120
|
+
if (!Array.isArray(scopeCommands)) return undefined
|
|
121
|
+
|
|
122
|
+
return scopeCommands.find((entry) => entry.methodName === commandName)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds the shared frontend-model request entry for one tool call.
|
|
127
|
+
* @param {object} args - Arguments.
|
|
128
|
+
* @param {ManifestCommandEntry} args.commandEntry - Manifest command entry.
|
|
129
|
+
* @param {string} args.model - Backend model name.
|
|
130
|
+
* @param {Record<string, string | number | boolean> | undefined} args.requestContext - Remote request context.
|
|
131
|
+
* @param {Record<string, unknown>} args.toolArguments - Tool arguments.
|
|
132
|
+
* @returns {FrontendModelRequestEntry} - frontendApi request entry.
|
|
133
|
+
*/
|
|
134
|
+
function buildFrontendModelRequestEntry({commandEntry, model, requestContext, toolArguments}) {
|
|
135
|
+
const isBuiltInCommand = ["attach", "attachmentList", "create", "destroy", "download", "find", "index", "update", "url"].includes(commandEntry.methodName)
|
|
136
|
+
|
|
137
|
+
if (isBuiltInCommand) {
|
|
138
|
+
return {
|
|
139
|
+
commandType: commandEntry.methodName,
|
|
140
|
+
model,
|
|
141
|
+
payload: toolArguments,
|
|
142
|
+
requestId: "mcp-1",
|
|
143
|
+
requestContext
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
customPath: resolveCustomCommandPath({commandEntry, toolArguments}),
|
|
149
|
+
model,
|
|
150
|
+
payload: toolArguments,
|
|
151
|
+
requestId: "mcp-1",
|
|
152
|
+
requestContext
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Substitutes the `<id>` placeholder of a manifest command path with the tool's
|
|
158
|
+
* primary key argument, matching the member route the dispatcher resolves.
|
|
159
|
+
* @param {object} args - Arguments.
|
|
160
|
+
* @param {ManifestCommandEntry} args.commandEntry - Manifest command entry (carries `path` and `scope`).
|
|
161
|
+
* @param {Record<string, unknown>} args.toolArguments - Tool arguments.
|
|
162
|
+
* @returns {string} - Custom command path.
|
|
163
|
+
*/
|
|
164
|
+
function resolveCustomCommandPath({commandEntry, toolArguments}) {
|
|
165
|
+
const path = String(commandEntry.path || "")
|
|
166
|
+
|
|
167
|
+
if (!path.includes("<id>")) return path
|
|
168
|
+
|
|
169
|
+
const idValue = toolArguments.id ?? toolArguments.recordId
|
|
170
|
+
|
|
171
|
+
return path.replace("<id>", encodeURIComponent(String(idValue ?? "")))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Builds the synthetic HTTP request lines for one MCP dispatch. The request-context
|
|
176
|
+
* `authorization` key is forwarded as the request's `Authorization` header so apps
|
|
177
|
+
* whose ability resolver authenticates from headers (Bearer service tokens or
|
|
178
|
+
* personal access tokens) authenticate MCP tool calls exactly like HTTP calls.
|
|
179
|
+
* Carriage returns and line feeds are stripped so a context value can never inject
|
|
180
|
+
* extra request lines.
|
|
181
|
+
* @param {object} args - Arguments.
|
|
182
|
+
* @param {Record<string, string | number | boolean> | undefined} args.requestContext - Remote request context.
|
|
183
|
+
* @returns {string} - Raw HTTP request head.
|
|
184
|
+
*/
|
|
185
|
+
export function buildSyntheticRequestLines({requestContext}) {
|
|
186
|
+
const lines = [
|
|
187
|
+
`POST ${MCP_REQUEST_PATH} HTTP/1.1`,
|
|
188
|
+
"Host: velocious-mcp",
|
|
189
|
+
"Content-Length: 0",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
if (requestContext && typeof requestContext.authorization === "string") {
|
|
193
|
+
const authorization = requestContext.authorization.replace(/[\r\n]/g, "").trim()
|
|
194
|
+
|
|
195
|
+
if (authorization.length > 0) lines.push(`Authorization: ${authorization}`)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
lines.push("", "")
|
|
199
|
+
|
|
200
|
+
return lines.join("\r\n")
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Runs one frontend-model request entry through the controller in-process,
|
|
205
|
+
* mirroring the synthetic-request pattern used by the controller specs.
|
|
206
|
+
* @param {object} args - Arguments.
|
|
207
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
208
|
+
* @param {FrontendModelRequestEntry} args.requestEntry - frontendApi request entry.
|
|
209
|
+
* @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>>} - Parsed frontendApi response payload.
|
|
210
|
+
*/
|
|
211
|
+
async function runFrontendApiRequest({configuration, requestEntry}) {
|
|
212
|
+
const client = /** @type {import("../http-server/client/index.js").default} */ ({remoteAddress: "127.0.0.1"})
|
|
213
|
+
const request = new Request({client, configuration})
|
|
214
|
+
const donePromise = new Promise((resolve) => request.requestParser.events.on("done", resolve))
|
|
215
|
+
|
|
216
|
+
request.feed(Buffer.from(buildSyntheticRequestLines({requestContext: requestEntry.requestContext}), "utf8"))
|
|
217
|
+
|
|
218
|
+
await donePromise
|
|
219
|
+
|
|
220
|
+
const response = new Response({configuration})
|
|
221
|
+
const controller = new FrontendModelController({
|
|
222
|
+
action: "frontendApi",
|
|
223
|
+
configuration,
|
|
224
|
+
controller: "frontend-models",
|
|
225
|
+
params: {requests: [requestEntry]},
|
|
226
|
+
request,
|
|
227
|
+
response,
|
|
228
|
+
viewPath: `${configuration.getDirectory()}/src/routes/frontend-models`
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
await controller.frontendApi()
|
|
232
|
+
|
|
233
|
+
const body = response.getBody()
|
|
234
|
+
const responseText = body === null || body === undefined
|
|
235
|
+
? ""
|
|
236
|
+
: (typeof body === "string" ? body : Buffer.from(body).toString("utf8"))
|
|
237
|
+
const responseJson = responseText.length > 0 ? JSON.parse(responseText) : {}
|
|
238
|
+
|
|
239
|
+
return /** @type {Record<string, ReturnType<typeof JSON.parse>>} */ (deserializeFrontendModelTransportValue(responseJson))
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Wraps a value as an MCP text content result.
|
|
244
|
+
* @param {object} args - Arguments.
|
|
245
|
+
* @param {unknown} args.value - Value to serialize as JSON text.
|
|
246
|
+
* @returns {McpToolResult} - MCP tool result.
|
|
247
|
+
*/
|
|
248
|
+
function textResult({value}) {
|
|
249
|
+
const serialized = serializeFrontendModelTransportValue(value)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
content: [{text: JSON.stringify(serialized, null, 2), type: "text"}],
|
|
253
|
+
isError: false
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Builds an MCP error result, optionally including the dispatcher's error
|
|
259
|
+
* payload (validation summaries stay faithful — nothing is swallowed).
|
|
260
|
+
* @param {string} message - Error message.
|
|
261
|
+
* @param {Record<string, unknown>} [payload] - Full dispatcher error payload.
|
|
262
|
+
* @returns {McpToolResult} - MCP tool result.
|
|
263
|
+
*/
|
|
264
|
+
function errorResult(message, payload) {
|
|
265
|
+
const detail = payload ? `\n${JSON.stringify(payload, null, 2)}` : ""
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
content: [{text: `${message}${detail}`, type: "text"}],
|
|
269
|
+
isError: true
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Record<string, unknown> & {error?: {code: number, message: string}, id?: number | string | null, jsonrpc: string, method?: string, params?: Record<string, unknown>, result?: Record<string, unknown>}} JsonRpcMessage
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import readline from "node:readline"
|
|
8
|
+
import {callMcpTool, listMcpTools} from "./dispatcher.js"
|
|
9
|
+
|
|
10
|
+
const MCP_PROTOCOL_VERSION = "2025-03-26"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Handles one JSON-RPC message of the MCP protocol (initialize / tools/list /
|
|
14
|
+
* tools/call / ping) against a Velocious configuration. Returns null for
|
|
15
|
+
* notifications (no response expected).
|
|
16
|
+
* @param {object} args - Arguments.
|
|
17
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
18
|
+
* @param {JsonRpcMessage} args.request - Parsed JSON-RPC request.
|
|
19
|
+
* @param {Record<string, string | number | boolean> | undefined} [args.requestContext] - Remote request context forwarded to every tool call.
|
|
20
|
+
* @returns {Promise<JsonRpcMessage | null>} - JSON-RPC response, or null for notifications.
|
|
21
|
+
*/
|
|
22
|
+
export async function handleMcpRequest({configuration, request, requestContext}) {
|
|
23
|
+
if (!request || typeof request !== "object" || request.jsonrpc !== "2.0" || typeof request.method !== "string") {
|
|
24
|
+
return jsonRpcError(null, -32600, "Invalid Request")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const id = request.id
|
|
28
|
+
|
|
29
|
+
if (request.method.startsWith("notifications/")) return null
|
|
30
|
+
|
|
31
|
+
if (request.method === "initialize") {
|
|
32
|
+
return jsonRpcResult(id, {
|
|
33
|
+
capabilities: {tools: {listChanged: false}},
|
|
34
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
35
|
+
serverInfo: {name: "velocious-mcp", title: "Velocious frontend-model MCP server", version: "1.0.0"}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (request.method === "ping") return jsonRpcResult(id, {})
|
|
40
|
+
|
|
41
|
+
if (request.method === "tools/list") {
|
|
42
|
+
return jsonRpcResult(id, {tools: await listMcpTools({configuration})})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (request.method === "tools/call") {
|
|
46
|
+
const params = request.params || {}
|
|
47
|
+
|
|
48
|
+
if (typeof params.name !== "string") return jsonRpcError(id, -32602, "Expected tools/call params.name")
|
|
49
|
+
|
|
50
|
+
const toolArguments = params.arguments && typeof params.arguments === "object" && !Array.isArray(params.arguments)
|
|
51
|
+
? /** @type {Record<string, unknown>} */ (params.arguments)
|
|
52
|
+
: {}
|
|
53
|
+
|
|
54
|
+
return jsonRpcResult(id, await callMcpTool({configuration, requestContext, toolArguments, toolName: params.name}))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return jsonRpcError(id, -32601, `Method not found: ${request.method}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Starts a line-delimited JSON-RPC MCP server over stdio (the primary MCP
|
|
62
|
+
* transport for local tool servers). Each input line is one JSON-RPC message;
|
|
63
|
+
* each response is written as one JSON line.
|
|
64
|
+
* @param {object} args - Arguments.
|
|
65
|
+
* @param {import("../configuration.js").default} args.configuration - Configuration instance.
|
|
66
|
+
* @param {import("node:stream").Readable} [args.input] - Input stream (defaults to stdin).
|
|
67
|
+
* @param {import("node:stream").Writable} [args.output] - Output stream (defaults to stdout).
|
|
68
|
+
* @param {Record<string, string | number | boolean> | undefined} [args.requestContext] - Remote request context forwarded to every tool call.
|
|
69
|
+
* @returns {{close: () => void, done: Promise<void>}} - Server handle; `done` resolves when the input stream closes.
|
|
70
|
+
*/
|
|
71
|
+
export function startMcpStdioServer({configuration, input = process.stdin, output = process.stdout, requestContext}) {
|
|
72
|
+
let closed = false
|
|
73
|
+
let doneResolved = false
|
|
74
|
+
/** @type {((value: void) => void) | undefined} */
|
|
75
|
+
let resolveDone
|
|
76
|
+
|
|
77
|
+
const done = new Promise((resolve) => { resolveDone = resolve })
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Writes one JSON-RPC message as a line.
|
|
81
|
+
* @param {JsonRpcMessage | null} message - Message to write.
|
|
82
|
+
* @returns {void} - No return value.
|
|
83
|
+
*/
|
|
84
|
+
const writeMessage = (message) => {
|
|
85
|
+
// Responses for in-flight requests are still written after the input
|
|
86
|
+
// stream closed; only after `done` resolved are writes suppressed.
|
|
87
|
+
if (message === null || doneResolved) return
|
|
88
|
+
|
|
89
|
+
output.write(`${JSON.stringify(message)}\n`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const readlineInterface = readline.createInterface({input})
|
|
93
|
+
|
|
94
|
+
let pendingRequests = 0
|
|
95
|
+
|
|
96
|
+
const finishIfClosed = () => {
|
|
97
|
+
if (closed && pendingRequests === 0 && resolveDone && !doneResolved) {
|
|
98
|
+
doneResolved = true
|
|
99
|
+
resolveDone(undefined)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
readlineInterface.on("line", (/** @type {string} */ line) => {
|
|
104
|
+
const trimmed = line.trim()
|
|
105
|
+
|
|
106
|
+
if (trimmed.length === 0) return
|
|
107
|
+
|
|
108
|
+
/** @type {JsonRpcMessage} */
|
|
109
|
+
let request
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
request = JSON.parse(trimmed)
|
|
113
|
+
} catch {
|
|
114
|
+
writeMessage(jsonRpcError(null, -32700, "Parse error"))
|
|
115
|
+
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pendingRequests++
|
|
120
|
+
|
|
121
|
+
handleMcpRequest({configuration, request, requestContext}).then((response) => {
|
|
122
|
+
writeMessage(response)
|
|
123
|
+
}).catch((error) => {
|
|
124
|
+
writeMessage(jsonRpcError(request.id ?? null, -32603, `Internal error: ${error instanceof Error ? error.message : String(error)}`))
|
|
125
|
+
}).finally(() => {
|
|
126
|
+
pendingRequests--
|
|
127
|
+
finishIfClosed()
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
readlineInterface.on("close", () => {
|
|
132
|
+
closed = true
|
|
133
|
+
finishIfClosed()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
close() {
|
|
138
|
+
if (closed) return
|
|
139
|
+
|
|
140
|
+
closed = true
|
|
141
|
+
readlineInterface.close()
|
|
142
|
+
finishIfClosed()
|
|
143
|
+
},
|
|
144
|
+
done
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds a JSON-RPC success response.
|
|
150
|
+
* @param {number | string | null | undefined} id - Request id.
|
|
151
|
+
* @param {Record<string, unknown>} result - Result payload.
|
|
152
|
+
* @returns {JsonRpcMessage} - JSON-RPC response.
|
|
153
|
+
*/
|
|
154
|
+
function jsonRpcResult(id, result) {
|
|
155
|
+
return {id, jsonrpc: "2.0", result}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Builds a JSON-RPC error response.
|
|
160
|
+
* @param {number | string | null | undefined} id - Request id.
|
|
161
|
+
* @param {number} code - JSON-RPC error code.
|
|
162
|
+
* @param {string} message - Error message.
|
|
163
|
+
* @returns {JsonRpcMessage} - JSON-RPC response.
|
|
164
|
+
*/
|
|
165
|
+
function jsonRpcError(id, code, message) {
|
|
166
|
+
return {error: {code, message}, id, jsonrpc: "2.0"}
|
|
167
|
+
}
|