@getjack/jack 0.1.26 → 0.1.27

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.
@@ -2,6 +2,9 @@ import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index
2
2
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
3
  import { z } from "zod";
4
4
  import { JackError, JackErrorCode } from "../../lib/errors.ts";
5
+ import { authFetch } from "../../lib/auth/index.ts";
6
+ import { getControlApiUrl, startLogSession } from "../../lib/control-plane.ts";
7
+ import { getDeployMode, getProjectId } from "../../lib/project-link.ts";
5
8
  import { createProject, deployProject, getProjectStatus } from "../../lib/project-operations.ts";
6
9
  import { listAllProjects } from "../../lib/project-resolver.ts";
7
10
  import { createDatabase } from "../../lib/services/db-create.ts";
@@ -12,6 +15,10 @@ import {
12
15
  wrapResultsForMcp,
13
16
  } from "../../lib/services/db-execute.ts";
14
17
  import { listDatabases } from "../../lib/services/db-list.ts";
18
+ import { createStorageBucket } from "../../lib/services/storage-create.ts";
19
+ import { deleteStorageBucket } from "../../lib/services/storage-delete.ts";
20
+ import { getStorageBucketInfo } from "../../lib/services/storage-info.ts";
21
+ import { listStorageBuckets } from "../../lib/services/storage-list.ts";
15
22
  import { createVectorizeIndex } from "../../lib/services/vectorize-create.ts";
16
23
  import { deleteVectorizeIndex } from "../../lib/services/vectorize-delete.ts";
17
24
  import { getVectorizeInfo } from "../../lib/services/vectorize-info.ts";
@@ -119,6 +126,69 @@ const GetVectorizeInfoSchema = z.object({
119
126
  .describe("Path to project directory (defaults to current directory)"),
120
127
  });
121
128
 
129
+ const CreateStorageBucketSchema = z.object({
130
+ name: z.string().optional().describe("Bucket name (auto-generated if not provided)"),
131
+ project_path: z
132
+ .string()
133
+ .optional()
134
+ .describe("Path to project directory (defaults to current directory)"),
135
+ });
136
+
137
+ const ListStorageBucketsSchema = z.object({
138
+ project_path: z
139
+ .string()
140
+ .optional()
141
+ .describe("Path to project directory (defaults to current directory)"),
142
+ });
143
+
144
+ const GetStorageInfoSchema = z.object({
145
+ name: z.string().optional().describe("Bucket name (defaults to first bucket if not provided)"),
146
+ project_path: z
147
+ .string()
148
+ .optional()
149
+ .describe("Path to project directory (defaults to current directory)"),
150
+ });
151
+
152
+ const DeleteStorageBucketSchema = z.object({
153
+ name: z.string().describe("Bucket name to delete"),
154
+ project_path: z
155
+ .string()
156
+ .optional()
157
+ .describe("Path to project directory (defaults to current directory)"),
158
+ });
159
+
160
+ const StartLogSessionSchema = z.object({
161
+ project_path: z
162
+ .string()
163
+ .optional()
164
+ .describe("Path to project directory (defaults to current directory)"),
165
+ label: z.string().optional().describe("Optional short tag/description for the log session"),
166
+ });
167
+
168
+ const TailLogsSchema = z.object({
169
+ project_path: z
170
+ .string()
171
+ .optional()
172
+ .describe("Path to project directory (defaults to current directory)"),
173
+ label: z.string().optional().describe("Optional short tag/description for the log session"),
174
+ max_events: z
175
+ .number()
176
+ .int()
177
+ .min(1)
178
+ .max(200)
179
+ .optional()
180
+ .default(50)
181
+ .describe("Maximum number of log events to collect (default: 50, max: 200)"),
182
+ duration_ms: z
183
+ .number()
184
+ .int()
185
+ .min(100)
186
+ .max(10_000)
187
+ .optional()
188
+ .default(2_000)
189
+ .describe("How long to listen before returning (default: 2000ms, max: 10000ms)"),
190
+ });
191
+
122
192
  export function registerTools(server: McpServer, _options: McpServerOptions, debug: DebugLogger) {
123
193
  // Register tool list handler
124
194
  server.setRequestHandler(ListToolsRequestSchema, async () => {
@@ -190,6 +260,50 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
190
260
  },
191
261
  },
192
262
  },
263
+ {
264
+ name: "start_log_session",
265
+ description:
266
+ "Start or renew a 1-hour real-time log session for a managed (jack cloud) project. Returns an SSE stream URL.",
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ project_path: {
271
+ type: "string",
272
+ description: "Path to project directory (defaults to current directory)",
273
+ },
274
+ label: {
275
+ type: "string",
276
+ description: "Optional short tag/description for the session",
277
+ },
278
+ },
279
+ },
280
+ },
281
+ {
282
+ name: "tail_logs",
283
+ description:
284
+ "Collect a short sample of live log events (JSON) from a managed (jack cloud) project. Useful for agentic debugging.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ project_path: {
289
+ type: "string",
290
+ description: "Path to project directory (defaults to current directory)",
291
+ },
292
+ label: {
293
+ type: "string",
294
+ description: "Optional short tag/description for the session",
295
+ },
296
+ max_events: {
297
+ type: "number",
298
+ description: "Maximum number of events to collect (default: 50, max: 200)",
299
+ },
300
+ duration_ms: {
301
+ type: "number",
302
+ description: "How long to listen before returning (default: 2000ms, max: 10000ms)",
303
+ },
304
+ },
305
+ },
306
+ },
193
307
  {
194
308
  name: "create_database",
195
309
  description:
@@ -332,6 +446,72 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
332
446
  required: ["name"],
333
447
  },
334
448
  },
449
+ {
450
+ name: "create_storage_bucket",
451
+ description:
452
+ "Create an R2 storage bucket for a project. Returns deploy_required=true since binding needs deploy to activate.",
453
+ inputSchema: {
454
+ type: "object",
455
+ properties: {
456
+ name: {
457
+ type: "string",
458
+ description: "Bucket name (auto-generated if not provided)",
459
+ },
460
+ project_path: {
461
+ type: "string",
462
+ description: "Path to project directory (defaults to current directory)",
463
+ },
464
+ },
465
+ },
466
+ },
467
+ {
468
+ name: "list_storage_buckets",
469
+ description: "List all R2 storage buckets for a project.",
470
+ inputSchema: {
471
+ type: "object",
472
+ properties: {
473
+ project_path: {
474
+ type: "string",
475
+ description: "Path to project directory (defaults to current directory)",
476
+ },
477
+ },
478
+ },
479
+ },
480
+ {
481
+ name: "get_storage_info",
482
+ description: "Get information about an R2 storage bucket.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ name: {
487
+ type: "string",
488
+ description: "Bucket name (defaults to first bucket if not provided)",
489
+ },
490
+ project_path: {
491
+ type: "string",
492
+ description: "Path to project directory (defaults to current directory)",
493
+ },
494
+ },
495
+ },
496
+ },
497
+ {
498
+ name: "delete_storage_bucket",
499
+ description: "Delete an R2 storage bucket.",
500
+ inputSchema: {
501
+ type: "object",
502
+ properties: {
503
+ name: {
504
+ type: "string",
505
+ description: "Bucket name to delete",
506
+ },
507
+ project_path: {
508
+ type: "string",
509
+ description: "Path to project directory (defaults to current directory)",
510
+ },
511
+ },
512
+ required: ["name"],
513
+ },
514
+ },
335
515
  ],
336
516
  };
337
517
  });
@@ -492,6 +672,162 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
492
672
  };
493
673
  }
494
674
 
675
+ case "start_log_session": {
676
+ const args = StartLogSessionSchema.parse(request.params.arguments ?? {});
677
+ const projectPath = args.project_path ?? process.cwd();
678
+
679
+ const deployMode = await getDeployMode(projectPath);
680
+ if (deployMode !== "managed") {
681
+ throw new JackError(
682
+ JackErrorCode.VALIDATION_ERROR,
683
+ "Real-time logs are only available for managed (jack cloud) projects",
684
+ "For BYOC projects, use Cloudflare dashboard logs or 'wrangler tail'.",
685
+ );
686
+ }
687
+
688
+ const projectId = await getProjectId(projectPath);
689
+ if (!projectId) {
690
+ throw new JackError(
691
+ JackErrorCode.PROJECT_NOT_FOUND,
692
+ "Project not found",
693
+ "Run this from a linked jack cloud project directory (has .jack/project.json).",
694
+ );
695
+ }
696
+
697
+ const wrappedStartLogSession = withTelemetry(
698
+ "start_log_session",
699
+ async (id: string, label?: string) => startLogSession(id, label),
700
+ { platform: "mcp" },
701
+ );
702
+
703
+ const result = await wrappedStartLogSession(projectId, args.label);
704
+
705
+ return {
706
+ content: [
707
+ {
708
+ type: "text",
709
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
710
+ },
711
+ ],
712
+ };
713
+ }
714
+
715
+ case "tail_logs": {
716
+ const args = TailLogsSchema.parse(request.params.arguments ?? {});
717
+ const projectPath = args.project_path ?? process.cwd();
718
+
719
+ const deployMode = await getDeployMode(projectPath);
720
+ if (deployMode !== "managed") {
721
+ throw new JackError(
722
+ JackErrorCode.VALIDATION_ERROR,
723
+ "Real-time logs are only available for managed (jack cloud) projects",
724
+ "For BYOC projects, use Cloudflare dashboard logs or 'wrangler tail'.",
725
+ );
726
+ }
727
+
728
+ const projectId = await getProjectId(projectPath);
729
+ if (!projectId) {
730
+ throw new JackError(
731
+ JackErrorCode.PROJECT_NOT_FOUND,
732
+ "Project not found",
733
+ "Run this from a linked jack cloud project directory (has .jack/project.json).",
734
+ );
735
+ }
736
+
737
+ const wrappedTailLogs = withTelemetry(
738
+ "tail_logs",
739
+ async (id: string, label: string | undefined, maxEvents: number, durationMs: number) => {
740
+ const session = await startLogSession(id, label);
741
+ const streamUrl = `${getControlApiUrl()}${session.stream.url}`;
742
+
743
+ const controller = new AbortController();
744
+ const timeout = setTimeout(() => controller.abort(), durationMs);
745
+
746
+ const events: unknown[] = [];
747
+ let truncated = false;
748
+
749
+ try {
750
+ const response = await authFetch(streamUrl, {
751
+ method: "GET",
752
+ headers: { Accept: "text/event-stream" },
753
+ signal: controller.signal,
754
+ });
755
+
756
+ if (!response.ok || !response.body) {
757
+ const err = (await response
758
+ .json()
759
+ .catch(() => ({ message: "Failed to open log stream" }))) as {
760
+ message?: string;
761
+ };
762
+ throw new Error(err.message || `Failed to open log stream: ${response.status}`);
763
+ }
764
+
765
+ const reader = response.body.getReader();
766
+ const decoder = new TextDecoder();
767
+ let buffer = "";
768
+
769
+ while (events.length < maxEvents) {
770
+ const { done, value } = await reader.read();
771
+ if (done) break;
772
+
773
+ buffer += decoder.decode(value, { stream: true });
774
+ const lines = buffer.split("\n");
775
+ buffer = lines.pop() || "";
776
+
777
+ for (const line of lines) {
778
+ if (!line.startsWith("data:")) continue;
779
+ const data = line.slice(5).trim();
780
+ if (!data) continue;
781
+
782
+ let parsed: { type?: string } | null = null;
783
+ try {
784
+ parsed = JSON.parse(data) as { type?: string };
785
+ } catch {
786
+ continue;
787
+ }
788
+
789
+ if (parsed?.type !== "event") continue;
790
+ events.push(parsed);
791
+
792
+ if (events.length >= maxEvents) {
793
+ truncated = true;
794
+ controller.abort();
795
+ break;
796
+ }
797
+ }
798
+ }
799
+ } catch (error) {
800
+ // Treat abort as a normal exit (duration elapsed or max events reached).
801
+ if (!(error instanceof Error && error.name === "AbortError")) {
802
+ throw error;
803
+ }
804
+ } finally {
805
+ clearTimeout(timeout);
806
+ controller.abort();
807
+ }
808
+
809
+ return { session: session.session, events, truncated };
810
+ },
811
+ { platform: "mcp" },
812
+ );
813
+
814
+ const result = await wrappedTailLogs(
815
+ projectId,
816
+ args.label,
817
+ args.max_events,
818
+ args.duration_ms,
819
+ );
820
+
821
+ return {
822
+ content: [
823
+ {
824
+ type: "text",
825
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
826
+ },
827
+ ],
828
+ };
829
+ }
830
+
495
831
  case "create_database": {
496
832
  const args = CreateDatabaseSchema.parse(request.params.arguments ?? {});
497
833
  const projectPath = args.project_path ?? process.cwd();
@@ -832,6 +1168,150 @@ export function registerTools(server: McpServer, _options: McpServerOptions, deb
832
1168
  };
833
1169
  }
834
1170
 
1171
+ case "create_storage_bucket": {
1172
+ const args = CreateStorageBucketSchema.parse(request.params.arguments ?? {});
1173
+ const projectPath = args.project_path ?? process.cwd();
1174
+
1175
+ const wrappedCreateStorageBucket = withTelemetry(
1176
+ "create_storage_bucket",
1177
+ async (projectDir: string, name?: string) => {
1178
+ const result = await createStorageBucket(projectDir, {
1179
+ name,
1180
+ interactive: false,
1181
+ });
1182
+
1183
+ // Track business event
1184
+ track(Events.SERVICE_CREATED, {
1185
+ service_type: "r2",
1186
+ platform: "mcp",
1187
+ });
1188
+
1189
+ return {
1190
+ bucket_name: result.bucketName,
1191
+ binding_name: result.bindingName,
1192
+ created: result.created,
1193
+ deploy_required: true,
1194
+ };
1195
+ },
1196
+ { platform: "mcp" },
1197
+ );
1198
+
1199
+ const result = await wrappedCreateStorageBucket(projectPath, args.name);
1200
+
1201
+ return {
1202
+ content: [
1203
+ {
1204
+ type: "text",
1205
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1206
+ },
1207
+ ],
1208
+ };
1209
+ }
1210
+
1211
+ case "list_storage_buckets": {
1212
+ const args = ListStorageBucketsSchema.parse(request.params.arguments ?? {});
1213
+ const projectPath = args.project_path ?? process.cwd();
1214
+
1215
+ const wrappedListStorageBuckets = withTelemetry(
1216
+ "list_storage_buckets",
1217
+ async (projectDir: string) => {
1218
+ const buckets = await listStorageBuckets(projectDir);
1219
+ return {
1220
+ buckets: buckets.map((bucket) => ({
1221
+ name: bucket.name,
1222
+ binding: bucket.binding,
1223
+ })),
1224
+ };
1225
+ },
1226
+ { platform: "mcp" },
1227
+ );
1228
+
1229
+ const result = await wrappedListStorageBuckets(projectPath);
1230
+
1231
+ return {
1232
+ content: [
1233
+ {
1234
+ type: "text",
1235
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1236
+ },
1237
+ ],
1238
+ };
1239
+ }
1240
+
1241
+ case "get_storage_info": {
1242
+ const args = GetStorageInfoSchema.parse(request.params.arguments ?? {});
1243
+ const projectPath = args.project_path ?? process.cwd();
1244
+
1245
+ const wrappedGetStorageInfo = withTelemetry(
1246
+ "get_storage_info",
1247
+ async (projectDir: string, bucketName?: string) => {
1248
+ const info = await getStorageBucketInfo(projectDir, bucketName);
1249
+
1250
+ if (!info) {
1251
+ throw new JackError(
1252
+ JackErrorCode.RESOURCE_NOT_FOUND,
1253
+ bucketName ? `Storage bucket '${bucketName}' not found` : "No storage buckets found",
1254
+ "Use list_storage_buckets to see available buckets or create_storage_bucket to create one",
1255
+ );
1256
+ }
1257
+
1258
+ return {
1259
+ name: info.name,
1260
+ binding: info.binding,
1261
+ source: info.source,
1262
+ };
1263
+ },
1264
+ { platform: "mcp" },
1265
+ );
1266
+
1267
+ const result = await wrappedGetStorageInfo(projectPath, args.name);
1268
+
1269
+ return {
1270
+ content: [
1271
+ {
1272
+ type: "text",
1273
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1274
+ },
1275
+ ],
1276
+ };
1277
+ }
1278
+
1279
+ case "delete_storage_bucket": {
1280
+ const args = DeleteStorageBucketSchema.parse(request.params.arguments ?? {});
1281
+ const projectPath = args.project_path ?? process.cwd();
1282
+
1283
+ const wrappedDeleteStorageBucket = withTelemetry(
1284
+ "delete_storage_bucket",
1285
+ async (projectDir: string, bucketName: string) => {
1286
+ const result = await deleteStorageBucket(projectDir, bucketName);
1287
+
1288
+ // Track business event
1289
+ track(Events.SERVICE_DELETED, {
1290
+ service_type: "r2",
1291
+ platform: "mcp",
1292
+ });
1293
+
1294
+ return {
1295
+ bucket_name: result.bucketName,
1296
+ deleted: result.deleted,
1297
+ binding_removed: result.bindingRemoved,
1298
+ };
1299
+ },
1300
+ { platform: "mcp" },
1301
+ );
1302
+
1303
+ const result = await wrappedDeleteStorageBucket(projectPath, args.name);
1304
+
1305
+ return {
1306
+ content: [
1307
+ {
1308
+ type: "text",
1309
+ text: JSON.stringify(formatSuccessResponse(result, startTime), null, 2),
1310
+ },
1311
+ ],
1312
+ };
1313
+ }
1314
+
835
1315
  default:
836
1316
  throw new Error(`Unknown tool: ${toolName}`);
837
1317
  }