robodev 0.51.1 → 0.53.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/package.json +3 -3
- package/src/collect-files.test.ts +11 -0
- package/src/collect-files.ts +13 -3
- package/src/dev-inspect.test.ts +15 -0
- package/src/dev-inspect.ts +14 -1
- package/src/dev-project.test.ts +3 -0
- package/src/dev-project.ts +7 -3
- package/src/dev-server.ts +202 -2
- package/src/dev.ts +18 -2
- package/templates/auth-chat/package.json +2 -2
- package/templates/backend/package.json +2 -2
- package/templates/empty/package.json +3 -3
- package/templates/marketplace/package.json +2 -2
- package/templates/povio/package.json +2 -2
- package/templates/shadcn/package.json +3 -3
- package/templates/space/package.json +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robodev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "CLI for Robodev Starbase — create, auth, link, deploy, load, and run a project locally with robodev dev",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"embedded-postgres": "17.10.0-beta.17",
|
|
27
27
|
"fastify": "^5.12.1",
|
|
28
28
|
"tsx": "^4.20.5",
|
|
29
|
-
"@robodev-ai/
|
|
30
|
-
"@robodev-ai/
|
|
29
|
+
"@robodev-ai/runtime": "0.13.0",
|
|
30
|
+
"@robodev-ai/sdk": "0.17.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^24.3.0",
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
collectFiles,
|
|
9
9
|
collectSourceFiles,
|
|
10
10
|
mergeCollectedFiles,
|
|
11
|
+
mixedBackendLayoutError,
|
|
11
12
|
shouldCollectPath,
|
|
12
13
|
shouldCollectSourcePath,
|
|
13
14
|
} from "./collect-files.js";
|
|
@@ -221,3 +222,13 @@ test("backend tests are not collected for deploy", () => {
|
|
|
221
222
|
assert.equal(shouldCollectPath("robodev/api/_lib.ts"), true);
|
|
222
223
|
assert.equal(shouldCollectPath("robodev/api/latest.ts"), true);
|
|
223
224
|
});
|
|
225
|
+
|
|
226
|
+
test("robodev/mcp.ts (defineMcp) is collected beside the mcp/ tools directory", () => {
|
|
227
|
+
assert.equal(shouldCollectPath("robodev/mcp.ts"), true);
|
|
228
|
+
assert.equal(shouldCollectPath("robodev/mcp/ping.ts"), true);
|
|
229
|
+
assert.equal(shouldCollectPath("mcp.ts"), true);
|
|
230
|
+
assert.equal(shouldCollectPath("robodev/mcp.js"), false);
|
|
231
|
+
assert.ok(mixedBackendLayoutError(["robodev/database.ts", "mcp.ts"]));
|
|
232
|
+
assert.ok(mixedBackendLayoutError(["database.ts", "robodev/mcp.ts"]));
|
|
233
|
+
assert.equal(mixedBackendLayoutError(["robodev/database.ts", "robodev/mcp.ts"]), null);
|
|
234
|
+
});
|
package/src/collect-files.ts
CHANGED
|
@@ -145,11 +145,14 @@ function isBackendPath(path: string): boolean {
|
|
|
145
145
|
if (inner === "database.ts" && (path === "database.ts" || path === "robodev/database.ts")) {
|
|
146
146
|
return true;
|
|
147
147
|
}
|
|
148
|
+
// `defineMcp` settings; exactly `mcp.ts`, never a tool under `mcp/`.
|
|
149
|
+
if (path === "mcp.ts" || path === "robodev/mcp.ts") return true;
|
|
148
150
|
if (
|
|
149
151
|
inner.startsWith("api/") ||
|
|
150
152
|
inner.startsWith("jobs/") ||
|
|
151
153
|
inner.startsWith("hooks/") ||
|
|
152
|
-
inner.startsWith("sockets/")
|
|
154
|
+
inner.startsWith("sockets/") ||
|
|
155
|
+
inner.startsWith("mcp/")
|
|
153
156
|
) {
|
|
154
157
|
return inner.endsWith(".ts") && !isBackendTestPath(inner);
|
|
155
158
|
}
|
|
@@ -175,13 +178,20 @@ export function mixedBackendLayoutError(paths: Iterable<string>): string | null
|
|
|
175
178
|
const hasNewDb = list.includes("robodev/database.ts");
|
|
176
179
|
const hasLegacyDb = list.includes("database.ts");
|
|
177
180
|
const hasLegacyApiJobs = list.some(
|
|
178
|
-
(path) =>
|
|
181
|
+
(path) =>
|
|
182
|
+
path.startsWith("api/") ||
|
|
183
|
+
path.startsWith("jobs/") ||
|
|
184
|
+
path.startsWith("sockets/") ||
|
|
185
|
+
path.startsWith("mcp/") ||
|
|
186
|
+
path === "mcp.ts",
|
|
179
187
|
);
|
|
180
188
|
const hasNewApiJobs = list.some(
|
|
181
189
|
(path) =>
|
|
182
190
|
path.startsWith("robodev/api/") ||
|
|
183
191
|
path.startsWith("robodev/jobs/") ||
|
|
184
|
-
path.startsWith("robodev/sockets/")
|
|
192
|
+
path.startsWith("robodev/sockets/") ||
|
|
193
|
+
path.startsWith("robodev/mcp/") ||
|
|
194
|
+
path === "robodev/mcp.ts",
|
|
185
195
|
);
|
|
186
196
|
if (hasNewDb && (hasLegacyDb || hasLegacyApiJobs)) {
|
|
187
197
|
return "Do not mix robodev/ backend files with root database.ts, api/, or jobs/";
|
package/src/dev-inspect.test.ts
CHANGED
|
@@ -429,3 +429,18 @@ test("pdf inspect records byte length and format, not HTML text", () => {
|
|
|
429
429
|
assert.equal(event.error, "pdf_failed");
|
|
430
430
|
assert.equal(JSON.stringify(event).includes("secret invoice"), false);
|
|
431
431
|
});
|
|
432
|
+
|
|
433
|
+
test("dashboard links the MCP endpoint only when the project has tools", async () => {
|
|
434
|
+
const runtime = {
|
|
435
|
+
routes: [{ method: "get", path: "/api/planets", file: "api/planets.ts" }],
|
|
436
|
+
jobs: [],
|
|
437
|
+
};
|
|
438
|
+
const without = await buildDashboardState({ inspect: createDevInspect(), runtime });
|
|
439
|
+
assert.equal(without.overview.links.mcp, undefined);
|
|
440
|
+
const withTools = await buildDashboardState({
|
|
441
|
+
inspect: createDevInspect(),
|
|
442
|
+
runtime: { ...runtime, mcpTools: [{ name: "get_planets" }] },
|
|
443
|
+
});
|
|
444
|
+
assert.equal(withTools.overview.links.mcp, "/mcp");
|
|
445
|
+
assert.equal(isHistogramExcludedPath("/mcp"), true);
|
|
446
|
+
});
|
package/src/dev-inspect.ts
CHANGED
|
@@ -42,6 +42,7 @@ const HISTOGRAM_EXCLUDED_PATHS = new Set([
|
|
|
42
42
|
"/openapi.json",
|
|
43
43
|
"/robodev-enterprise-spec.json",
|
|
44
44
|
"/_robodev/ready",
|
|
45
|
+
"/mcp",
|
|
45
46
|
]);
|
|
46
47
|
|
|
47
48
|
export const RESERVED_AUTH_ROUTES: Array<{ method: string; path: string; file: string }> = [
|
|
@@ -214,7 +215,14 @@ export type DashboardState = {
|
|
|
214
215
|
google: DashboardGoogle;
|
|
215
216
|
requestHistogram: RequestHistogramBucket[];
|
|
216
217
|
jobHistogram: JobHistogramBucket[];
|
|
217
|
-
links: {
|
|
218
|
+
links: {
|
|
219
|
+
swagger: string;
|
|
220
|
+
scalar: string;
|
|
221
|
+
openapi: string;
|
|
222
|
+
enterprise: string;
|
|
223
|
+
/** Present when the project has MCP tools. */
|
|
224
|
+
mcp?: string;
|
|
225
|
+
};
|
|
218
226
|
};
|
|
219
227
|
routes: Array<{ method: string; path: string; file: string }>;
|
|
220
228
|
calls: Array<{ at: number; method: string; path: string; status: number; durationMs: number }>;
|
|
@@ -246,6 +254,7 @@ export type DashboardRuntime = {
|
|
|
246
254
|
routes: Array<{ method: string; path: string; file: string }>;
|
|
247
255
|
jobs: Array<{ name: string; file: string; def: { cron?: string } }>;
|
|
248
256
|
sockets?: Array<{ name: string; path: string; file: string; def: { auth?: ApiAuth } }>;
|
|
257
|
+
mcpTools?: Array<{ name: string }>;
|
|
249
258
|
pool?: {
|
|
250
259
|
query: <R extends Record<string, unknown> = Record<string, unknown>>(
|
|
251
260
|
sql: string,
|
|
@@ -602,6 +611,7 @@ export async function buildDashboardState(input: {
|
|
|
602
611
|
scalar: "/_robodev/scalar",
|
|
603
612
|
openapi: "/openapi.json",
|
|
604
613
|
enterprise: "/robodev-enterprise-spec.json",
|
|
614
|
+
...(input.runtime?.mcpTools?.length ? { mcp: "/mcp" } : {}),
|
|
605
615
|
},
|
|
606
616
|
},
|
|
607
617
|
routes,
|
|
@@ -1076,6 +1086,9 @@ export function inspectHtml(): string {
|
|
|
1076
1086
|
{ href: state.overview.links.openapi, label: "openapi.json" },
|
|
1077
1087
|
{ href: state.overview.links.enterprise, label: "Enterprise spec" }
|
|
1078
1088
|
];
|
|
1089
|
+
if (state.overview.links.mcp) {
|
|
1090
|
+
items.push({ href: state.overview.links.mcp, label: "MCP (POST)" });
|
|
1091
|
+
}
|
|
1079
1092
|
for (var i = 0; i < items.length; i++) {
|
|
1080
1093
|
var a = document.createElement("a");
|
|
1081
1094
|
a.href = items[i].href;
|
package/src/dev-project.test.ts
CHANGED
|
@@ -64,6 +64,9 @@ test("isBackendDevPath accepts backend sources only", () => {
|
|
|
64
64
|
assert.equal(isBackendDevPath("jobs/digest.ts"), true);
|
|
65
65
|
assert.equal(isBackendDevPath("hooks/auth.ts"), true);
|
|
66
66
|
assert.equal(isBackendDevPath("sockets/chat.ts"), true);
|
|
67
|
+
assert.equal(isBackendDevPath("mcp.ts"), true);
|
|
68
|
+
assert.equal(isBackendDevPath("mcp/ping.ts"), true);
|
|
69
|
+
assert.equal(isBackendDevPath("other.ts"), false);
|
|
67
70
|
assert.equal(isBackendDevPath("src/main.tsx"), false);
|
|
68
71
|
assert.equal(isBackendDevPath("api/planets.js"), false);
|
|
69
72
|
assert.equal(isBackendDevPath("package.json"), false);
|
package/src/dev-project.ts
CHANGED
|
@@ -7,14 +7,14 @@ export type DevFile = { path: string; content: string };
|
|
|
7
7
|
export type DevProject = {
|
|
8
8
|
/** Where `.robodev/dev/` and the root `package.json` live. */
|
|
9
9
|
root: string;
|
|
10
|
-
/** Absolute directory holding `database.ts` plus `api/`, `jobs/`, `hooks/`, `sockets/`. */
|
|
10
|
+
/** Absolute directory holding `database.ts` plus `api/`, `jobs/`, `hooks/`, `sockets/`, `mcp/`. */
|
|
11
11
|
backendDir: string;
|
|
12
12
|
/** Backend layout that was matched, for the startup banner. */
|
|
13
13
|
layout: "root" | "robodev" | "packages/be";
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
/** Directories under the backend root that hold compilable project code. */
|
|
17
|
-
export const BACKEND_DIRS = ["api", "jobs", "hooks", "sockets"] as const;
|
|
17
|
+
export const BACKEND_DIRS = ["api", "jobs", "hooks", "sockets", "mcp"] as const;
|
|
18
18
|
|
|
19
19
|
const SKIP_DIRS = new Set(["node_modules", "dist", ".git", ".robodev"]);
|
|
20
20
|
const MAX_BACKEND_FILES = 400;
|
|
@@ -69,7 +69,7 @@ export function isBackendDevPath(path: string): boolean {
|
|
|
69
69
|
const normalized = path.replaceAll("\\", "/");
|
|
70
70
|
if (normalized.includes("..")) return false;
|
|
71
71
|
if (isBackendTestPath(normalized)) return false;
|
|
72
|
-
if (normalized === "database.ts") return true;
|
|
72
|
+
if (normalized === "database.ts" || normalized === "mcp.ts") return true;
|
|
73
73
|
if (!normalized.endsWith(".ts")) return false;
|
|
74
74
|
const top = normalized.split("/")[0] ?? "";
|
|
75
75
|
return (BACKEND_DIRS as readonly string[]).includes(top);
|
|
@@ -112,6 +112,10 @@ export async function collectBackendFiles(backendDir: string): Promise<DevFile[]
|
|
|
112
112
|
content: await readFile(join(backendDir, "database.ts"), "utf8"),
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
|
+
// `defineMcp` settings sit beside the `mcp/` tools directory.
|
|
116
|
+
if (await isFile(join(backendDir, "mcp.ts"))) {
|
|
117
|
+
files.push({ path: "mcp.ts", content: await readFile(join(backendDir, "mcp.ts"), "utf8") });
|
|
118
|
+
}
|
|
115
119
|
for (const name of BACKEND_DIRS) {
|
|
116
120
|
await walk(join(backendDir, name));
|
|
117
121
|
}
|
package/src/dev-server.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { Readable } from "node:stream";
|
|
2
3
|
import cors from "@fastify/cors";
|
|
3
4
|
import websocket from "@fastify/websocket";
|
|
@@ -13,7 +14,28 @@ import {
|
|
|
13
14
|
buildOpenApi,
|
|
14
15
|
buildEnterpriseSpec,
|
|
15
16
|
handleLocalAuth,
|
|
17
|
+
handleProjectMcpHttp,
|
|
18
|
+
handleProjectOAuth,
|
|
16
19
|
isLocalAuthPath,
|
|
20
|
+
isProjectMcpRequest,
|
|
21
|
+
isProjectOAuthRequestPath,
|
|
22
|
+
PROJECT_MCP_PATH,
|
|
23
|
+
resolveConsentUrl,
|
|
24
|
+
localPasswordSignIn,
|
|
25
|
+
localPasswordSignUp,
|
|
26
|
+
authenticateProjectMcpToken,
|
|
27
|
+
isProjectMcpAccessToken,
|
|
28
|
+
projectGoogleOverrideMode,
|
|
29
|
+
projectMcpResource,
|
|
30
|
+
revokeRefresh,
|
|
31
|
+
runMcpOp,
|
|
32
|
+
runtimeVersion,
|
|
33
|
+
verifyProjectUserToken,
|
|
34
|
+
type McpCaller,
|
|
35
|
+
type OAuthHttpResponse,
|
|
36
|
+
type ProjectMcpHttpResponse,
|
|
37
|
+
type RuntimeMcpConfig,
|
|
38
|
+
type RuntimeMcpTool,
|
|
17
39
|
isReservedStoragePath,
|
|
18
40
|
ENTERPRISE_SPEC_PATH,
|
|
19
41
|
matchProjectRoute,
|
|
@@ -53,6 +75,9 @@ export type DevRuntime = {
|
|
|
53
75
|
routes: RuntimeRoute[];
|
|
54
76
|
jobs: RuntimeJob[];
|
|
55
77
|
sockets: RuntimeSocket[];
|
|
78
|
+
mcpTools: RuntimeMcpTool[];
|
|
79
|
+
/** `robodev/mcp.ts` (`defineMcp`). */
|
|
80
|
+
mcpConfig?: RuntimeMcpConfig;
|
|
56
81
|
openapi: Record<string, unknown>;
|
|
57
82
|
};
|
|
58
83
|
|
|
@@ -70,13 +95,76 @@ export type DevServerOptions = {
|
|
|
70
95
|
googleClientSecret?: string;
|
|
71
96
|
storageDir: string;
|
|
72
97
|
projectRoot?: string;
|
|
98
|
+
/** Shown on the MCP consent page and as the MCP server name. */
|
|
99
|
+
projectName?: string;
|
|
73
100
|
};
|
|
74
101
|
|
|
75
102
|
const SWAGGER_PATHS = new Set(["/_robodev/docs", "/_robodev/swagger", "/docs", "/swagger"]);
|
|
76
103
|
const SCALAR_PATHS = new Set(["/_robodev/scalar", "/scalar"]);
|
|
77
104
|
|
|
78
|
-
export function buildDevOpenApi(
|
|
79
|
-
|
|
105
|
+
export function buildDevOpenApi(
|
|
106
|
+
projectName: string,
|
|
107
|
+
routes: RuntimeRoute[],
|
|
108
|
+
mcpTools: RuntimeMcpTool[] = [],
|
|
109
|
+
) {
|
|
110
|
+
return buildOpenApi({ title: `${projectName} APIs`, projectId: "local", routes, mcpTools });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The URL MCP clients connect to, printed in the banner. */
|
|
114
|
+
export function devMcpUrl(listenOrigin: string): string {
|
|
115
|
+
return `${listenOrigin}${PROJECT_MCP_PATH}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function devOAuthSecret(options: DevServerOptions): string {
|
|
119
|
+
return createHash("sha256")
|
|
120
|
+
.update(`${options.jwtSecret}:${options.projectId}:project-mcp-oauth`)
|
|
121
|
+
.digest("base64url");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function sendRuntimeResponse(
|
|
125
|
+
reply: FastifyReply,
|
|
126
|
+
response: ProjectMcpHttpResponse | OAuthHttpResponse,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
reply.code(response.status);
|
|
129
|
+
for (const [name, value] of Object.entries(response.headers)) reply.header(name, value);
|
|
130
|
+
await (response.body === undefined ? reply.send() : reply.send(response.body));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* `robodev dev` has no project Auth settings store: Auth is on, password sign-in and sign-up
|
|
135
|
+
* are on, and Google is on unless the project `.env` sets only one of its two keys.
|
|
136
|
+
*/
|
|
137
|
+
function devOAuthSettings(options: DevServerOptions) {
|
|
138
|
+
return {
|
|
139
|
+
enabled: true,
|
|
140
|
+
emailPasswordEnabled: true,
|
|
141
|
+
allowSignups: true,
|
|
142
|
+
googleEnabled:
|
|
143
|
+
projectGoogleOverrideMode(options.googleClientId, options.googleClientSecret) !== "one",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function devMcpCaller(
|
|
148
|
+
options: DevServerOptions,
|
|
149
|
+
runtime: DevRuntime,
|
|
150
|
+
token: string,
|
|
151
|
+
): Promise<McpCaller> {
|
|
152
|
+
if (isProjectMcpAccessToken(token)) {
|
|
153
|
+
const principal = await authenticateProjectMcpToken(
|
|
154
|
+
runtime.pool,
|
|
155
|
+
token,
|
|
156
|
+
projectMcpResource(options.listenOrigin),
|
|
157
|
+
);
|
|
158
|
+
return principal
|
|
159
|
+
? { user: principal.user, via: "mcp_token", invalid: false }
|
|
160
|
+
: { user: null, via: null, invalid: true };
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const user = await verifyProjectUserToken(token, options.projectId, options.jwtSecret);
|
|
164
|
+
return { user, via: "project_jwt", invalid: false };
|
|
165
|
+
} catch {
|
|
166
|
+
return { user: null, via: null, invalid: true };
|
|
167
|
+
}
|
|
80
168
|
}
|
|
81
169
|
|
|
82
170
|
function pathnameOf(url: string): string {
|
|
@@ -453,6 +541,8 @@ export async function startDevServer(
|
|
|
453
541
|
origin: true,
|
|
454
542
|
credentials: true,
|
|
455
543
|
methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
544
|
+
// Browser MCP clients (MCP Inspector) read the 401 challenge to find the OAuth server.
|
|
545
|
+
exposedHeaders: ["WWW-Authenticate", "Mcp-Session-Id"],
|
|
456
546
|
});
|
|
457
547
|
await app.register(websocket);
|
|
458
548
|
app.get("/sockets/*", { websocket: true }, (socket, request) => {
|
|
@@ -631,6 +721,116 @@ export async function startDevServer(
|
|
|
631
721
|
return;
|
|
632
722
|
}
|
|
633
723
|
|
|
724
|
+
// A browser opening /mcp falls through to the normal 404 (the FE runs on its own dev server).
|
|
725
|
+
if (
|
|
726
|
+
isProjectMcpRequest(
|
|
727
|
+
pathname,
|
|
728
|
+
request.method,
|
|
729
|
+
request.headers as Record<string, string | string[] | undefined>,
|
|
730
|
+
)
|
|
731
|
+
) {
|
|
732
|
+
const headers = request.headers as Record<string, string | string[] | undefined>;
|
|
733
|
+
const clients = () => routeClients(options, getRuntime);
|
|
734
|
+
const response = await handleProjectMcpHttp(
|
|
735
|
+
{ method: request.method, headers, body: request.body },
|
|
736
|
+
{
|
|
737
|
+
tools: runtime.mcpTools,
|
|
738
|
+
serverName: options.projectName ?? "robodev-dev",
|
|
739
|
+
serverVersion: runtimeVersion(),
|
|
740
|
+
authEnabled: true,
|
|
741
|
+
origin: options.listenOrigin,
|
|
742
|
+
resolveCaller: (token) => devMcpCaller(options, runtime, token),
|
|
743
|
+
verifyTools: async (names, requestHeaders) => {
|
|
744
|
+
const outcome = await runMcpOp({
|
|
745
|
+
db: runtime.db,
|
|
746
|
+
mcpTools: runtime.mcpTools,
|
|
747
|
+
op: { op: "verify", tools: names },
|
|
748
|
+
headers: requestHeaders,
|
|
749
|
+
clients,
|
|
750
|
+
});
|
|
751
|
+
const payload = outcome.kind === "ok" ? outcome.encoded.payload : null;
|
|
752
|
+
return Array.isArray(payload) ? payload.map(Boolean) : names.map(() => false);
|
|
753
|
+
},
|
|
754
|
+
invokeRoute: async (input) => {
|
|
755
|
+
const started = Date.now();
|
|
756
|
+
const outcome = await runRoute({
|
|
757
|
+
db: runtime.db,
|
|
758
|
+
routes: runtime.routes,
|
|
759
|
+
method: input.method,
|
|
760
|
+
pathname: input.pathname,
|
|
761
|
+
query: input.query,
|
|
762
|
+
body: input.body,
|
|
763
|
+
headers: input.headers,
|
|
764
|
+
authorization: request.headers.authorization,
|
|
765
|
+
resolveUser: async () => null,
|
|
766
|
+
user: input.user,
|
|
767
|
+
clients,
|
|
768
|
+
mapError: (error) => codedErrorBody(error),
|
|
769
|
+
});
|
|
770
|
+
const status = outcome.kind === "ok" ? outcome.encoded.status : outcome.status;
|
|
771
|
+
const durationMs = Date.now() - started;
|
|
772
|
+
options.inspect.record({
|
|
773
|
+
kind: "request",
|
|
774
|
+
method: input.method.toUpperCase(),
|
|
775
|
+
path: input.pathname,
|
|
776
|
+
status,
|
|
777
|
+
durationMs,
|
|
778
|
+
});
|
|
779
|
+
console.log(
|
|
780
|
+
`${input.method.toUpperCase()} ${input.pathname} ${status} ${durationMs}ms (mcp ${input.tool.name})`,
|
|
781
|
+
);
|
|
782
|
+
return outcome;
|
|
783
|
+
},
|
|
784
|
+
invokeTool: (input) =>
|
|
785
|
+
runMcpOp({
|
|
786
|
+
db: runtime.db,
|
|
787
|
+
mcpTools: runtime.mcpTools,
|
|
788
|
+
op: { op: "tool", tool: input.tool.name, input: input.input, user: input.user },
|
|
789
|
+
headers: input.headers,
|
|
790
|
+
clients,
|
|
791
|
+
mapError: (error) => codedErrorBody(error),
|
|
792
|
+
}),
|
|
793
|
+
},
|
|
794
|
+
);
|
|
795
|
+
await sendRuntimeResponse(reply, response);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (isProjectOAuthRequestPath(pathname)) {
|
|
800
|
+
const response = await handleProjectOAuth(
|
|
801
|
+
{
|
|
802
|
+
method: request.method,
|
|
803
|
+
pathname,
|
|
804
|
+
query: (request.query ?? {}) as Record<string, unknown>,
|
|
805
|
+
body: request.body,
|
|
806
|
+
headers: request.headers as Record<string, string | string[] | undefined>,
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
db: runtime.pool,
|
|
810
|
+
projectName: options.projectName ?? "robodev dev",
|
|
811
|
+
origin: options.listenOrigin,
|
|
812
|
+
secret: devOAuthSecret(options),
|
|
813
|
+
consentUrl: resolveConsentUrl(runtime.mcpConfig?.consentUrl, options.listenOrigin),
|
|
814
|
+
settings: devOAuthSettings(options),
|
|
815
|
+
signIn: (input) => localPasswordSignIn(runtime.pool, input),
|
|
816
|
+
signUp: (input) => localPasswordSignUp(runtime.pool, input),
|
|
817
|
+
verifyAppAccessToken: async (token) => {
|
|
818
|
+
try {
|
|
819
|
+
return await verifyProjectUserToken(token, options.projectId, options.jwtSecret);
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
revokeAppRefreshToken: (token) => revokeRefresh(runtime.pool, token),
|
|
825
|
+
clientIp: request.ip,
|
|
826
|
+
},
|
|
827
|
+
);
|
|
828
|
+
if (response) {
|
|
829
|
+
await sendRuntimeResponse(reply, response);
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
634
834
|
if (isLocalAuthPath(pathname)) {
|
|
635
835
|
const result = await handleLocalAuth(
|
|
636
836
|
{
|
package/src/dev.ts
CHANGED
|
@@ -36,7 +36,7 @@ import { externalDevPostgres, startDevPostgres, type DevPostgres } from "./dev-p
|
|
|
36
36
|
import { starbaseUrl } from "./config.js";
|
|
37
37
|
import { devRouteClients } from "./dev-clients.js";
|
|
38
38
|
import { createDevInspect } from "./dev-inspect.js";
|
|
39
|
-
import { buildDevOpenApi, startDevServer, type DevRuntime } from "./dev-server.js";
|
|
39
|
+
import { buildDevOpenApi, devMcpUrl, startDevServer, type DevRuntime } from "./dev-server.js";
|
|
40
40
|
|
|
41
41
|
export type DevArgs = {
|
|
42
42
|
port: number;
|
|
@@ -162,6 +162,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
162
162
|
await mkdir(storageDir, { recursive: true });
|
|
163
163
|
|
|
164
164
|
let current: DevRuntime | null = null;
|
|
165
|
+
let projectName = "robodev dev";
|
|
165
166
|
let lastSchemaSource = "";
|
|
166
167
|
let closing = false;
|
|
167
168
|
|
|
@@ -232,6 +233,8 @@ export async function main(args: string[]): Promise<void> {
|
|
|
232
233
|
const loaded = await loadModules(distDir, files, undefined, {
|
|
233
234
|
mode: "deploy",
|
|
234
235
|
onWarning: (message) => log(`Warning: ${message}`),
|
|
236
|
+
// Locally the consent page may be the Vite dev server on http://localhost.
|
|
237
|
+
allowLocalConsentUrl: true,
|
|
235
238
|
});
|
|
236
239
|
assertJobCrons(loaded.jobs, { mode: "deploy" });
|
|
237
240
|
// Any edit to database.ts can change columns, not just tables, so compare its source.
|
|
@@ -266,8 +269,11 @@ export async function main(args: string[]): Promise<void> {
|
|
|
266
269
|
routes: loaded.routes,
|
|
267
270
|
jobs: loaded.jobs,
|
|
268
271
|
sockets: loaded.sockets,
|
|
269
|
-
|
|
272
|
+
mcpTools: loaded.mcpTools,
|
|
273
|
+
...(loaded.mcpConfig ? { mcpConfig: loaded.mcpConfig } : {}),
|
|
274
|
+
openapi: buildDevOpenApi(loaded.database.name, loaded.routes, loaded.mcpTools),
|
|
270
275
|
};
|
|
276
|
+
projectName = loaded.database.name;
|
|
271
277
|
setProjectSocketNames(
|
|
272
278
|
DEV_PROJECT_ID,
|
|
273
279
|
loaded.sockets.map((socket) => socket.name),
|
|
@@ -287,6 +293,12 @@ export async function main(args: string[]): Promise<void> {
|
|
|
287
293
|
for (const socket of loaded.sockets) {
|
|
288
294
|
log(` WS ${socket.path}`);
|
|
289
295
|
}
|
|
296
|
+
for (const tool of loaded.mcpTools) {
|
|
297
|
+
log(` MCP ${tool.name}`);
|
|
298
|
+
}
|
|
299
|
+
if (loaded.mcpConfig?.consentUrl) {
|
|
300
|
+
log(` MCP consent page ${loaded.mcpConfig.consentUrl}`);
|
|
301
|
+
}
|
|
290
302
|
if (previous) {
|
|
291
303
|
await previous.pool.end().catch(() => undefined);
|
|
292
304
|
}
|
|
@@ -316,6 +328,7 @@ export async function main(args: string[]): Promise<void> {
|
|
|
316
328
|
googleClientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
317
329
|
storageDir,
|
|
318
330
|
projectRoot: project.root,
|
|
331
|
+
projectName,
|
|
319
332
|
},
|
|
320
333
|
() => current,
|
|
321
334
|
);
|
|
@@ -340,6 +353,9 @@ export async function main(args: string[]): Promise<void> {
|
|
|
340
353
|
console.log(`API ${apiUrl}`);
|
|
341
354
|
console.log(`Swagger ${apiUrl}/_robodev/docs`);
|
|
342
355
|
console.log(`Dashboard ${apiUrl}/_robodev`);
|
|
356
|
+
if ((current as DevRuntime | null)?.mcpTools.length) {
|
|
357
|
+
console.log(`MCP ${devMcpUrl(apiUrl)}`);
|
|
358
|
+
}
|
|
343
359
|
console.log(
|
|
344
360
|
`Project ${relative(process.cwd(), project.backendDir) || "."} (${project.layout})`,
|
|
345
361
|
);
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"rules:gen": "rulesync generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@robodev-ai/sdk": "^0.
|
|
12
|
+
"@robodev-ai/sdk": "^0.17.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"robodev": "^0.
|
|
15
|
+
"robodev": "^0.53.0",
|
|
16
16
|
"rulesync": "^8.18.0",
|
|
17
17
|
"typescript": "^5.9.2"
|
|
18
18
|
}
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"build": "vite build"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@robodev-ai/sdk": "^0.
|
|
14
|
-
"@robodev-ai/client": "^0.
|
|
13
|
+
"@robodev-ai/sdk": "^0.17.0",
|
|
14
|
+
"@robodev-ai/client": "^0.6.0",
|
|
15
15
|
"react": "^18.3.1",
|
|
16
16
|
"react-dom": "^18.3.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"robodev": "^0.
|
|
19
|
+
"robodev": "^0.53.0",
|
|
20
20
|
"typescript": "^5.9.2",
|
|
21
21
|
"vite": "^6.0.0"
|
|
22
22
|
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"rules:gen": "rulesync generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@robodev-ai/sdk": "^0.
|
|
12
|
+
"@robodev-ai/sdk": "^0.17.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"robodev": "^0.
|
|
15
|
+
"robodev": "^0.53.0",
|
|
16
16
|
"rulesync": "^8.18.0",
|
|
17
17
|
"typescript": "^5.9.2"
|
|
18
18
|
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"rules:gen": "rulesync generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@robodev-ai/sdk": "^0.
|
|
12
|
+
"@robodev-ai/sdk": "^0.17.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"robodev": "^0.
|
|
15
|
+
"robodev": "^0.53.0",
|
|
16
16
|
"rulesync": "^8.18.0",
|
|
17
17
|
"typescript": "^5.9.2"
|
|
18
18
|
}
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"generate:api": "orval"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@robodev-ai/sdk": "^0.
|
|
15
|
-
"@robodev-ai/client": "^0.
|
|
14
|
+
"@robodev-ai/sdk": "^0.17.0",
|
|
15
|
+
"@robodev-ai/client": "^0.6.0",
|
|
16
16
|
"react": "^18.3.1",
|
|
17
17
|
"react-dom": "^18.3.1",
|
|
18
18
|
"react-router-dom": "^6.30.1"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"@types/react": "^18.3.12",
|
|
22
22
|
"@types/react-dom": "^18.3.1",
|
|
23
23
|
"orval": "^7.11.2",
|
|
24
|
-
"robodev": "^0.
|
|
24
|
+
"robodev": "^0.53.0",
|
|
25
25
|
"typescript": "^5.9.2",
|
|
26
26
|
"vite": "^6.0.0"
|
|
27
27
|
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"rules:gen": "rulesync generate"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@robodev-ai/sdk": "^0.
|
|
12
|
+
"@robodev-ai/sdk": "^0.17.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"robodev": "^0.
|
|
15
|
+
"robodev": "^0.53.0",
|
|
16
16
|
"rulesync": "^8.18.0",
|
|
17
17
|
"typescript": "^5.9.2"
|
|
18
18
|
}
|