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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "robodev",
3
- "version": "0.51.1",
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/sdk": "0.15.1",
30
- "@robodev-ai/runtime": "0.11.1"
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
+ });
@@ -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) => path.startsWith("api/") || path.startsWith("jobs/") || path.startsWith("sockets/"),
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/";
@@ -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
+ });
@@ -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: { swagger: string; scalar: string; openapi: string; enterprise: string };
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;
@@ -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);
@@ -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(projectName: string, routes: RuntimeRoute[]) {
79
- return buildOpenApi({ title: `${projectName} APIs`, projectId: "local", routes });
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
- openapi: buildDevOpenApi(loaded.database.name, loaded.routes),
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.15.1"
12
+ "@robodev-ai/sdk": "^0.17.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.51.1",
15
+ "robodev": "^0.53.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }
@@ -8,10 +8,10 @@
8
8
  "deploy": "robodev deploy"
9
9
  },
10
10
  "dependencies": {
11
- "@robodev-ai/sdk": "^0.15.1"
11
+ "@robodev-ai/sdk": "^0.17.0"
12
12
  },
13
13
  "devDependencies": {
14
- "robodev": "^0.51.1",
14
+ "robodev": "^0.53.0",
15
15
  "typescript": "^5.9.2"
16
16
  }
17
17
  }
@@ -10,13 +10,13 @@
10
10
  "build": "vite build"
11
11
  },
12
12
  "dependencies": {
13
- "@robodev-ai/sdk": "^0.15.1",
14
- "@robodev-ai/client": "^0.5.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.51.1",
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.15.1"
12
+ "@robodev-ai/sdk": "^0.17.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.51.1",
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.15.1"
12
+ "@robodev-ai/sdk": "^0.17.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.51.1",
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.1",
15
- "@robodev-ai/client": "^0.5.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.51.1",
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.15.1"
12
+ "@robodev-ai/sdk": "^0.17.0"
13
13
  },
14
14
  "devDependencies": {
15
- "robodev": "^0.51.1",
15
+ "robodev": "^0.53.0",
16
16
  "rulesync": "^8.18.0",
17
17
  "typescript": "^5.9.2"
18
18
  }