@getjack/jack 0.1.0 → 0.1.1

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.
@@ -1,4 +1,4 @@
1
- import { confirm, input, select } from "@inquirer/prompts";
1
+ import { input, select } from "@inquirer/prompts";
2
2
  import type { DetectedSecret } from "./env-parser.ts";
3
3
  import { info, success, warn } from "./output.ts";
4
4
  import { getSavedSecrets, getSecretsPath, maskSecret, saveSecrets } from "./secrets.ts";
@@ -107,12 +107,16 @@ export async function promptUseSecrets(): Promise<Record<string, string> | null>
107
107
  }
108
108
  console.error("");
109
109
 
110
- const useSecrets = await confirm({
110
+ console.error(" Esc to skip\n");
111
+ const action = await select({
111
112
  message: "Use them for this project?",
112
- default: true,
113
+ choices: [
114
+ { name: "1. Yes", value: "yes" },
115
+ { name: "2. No", value: "no" },
116
+ ],
113
117
  });
114
118
 
115
- if (useSecrets) {
119
+ if (action === "yes") {
116
120
  return saved;
117
121
  }
118
122
 
@@ -147,10 +151,14 @@ export async function promptUseSecretsFromList(
147
151
  return false;
148
152
  }
149
153
 
150
- const useSecrets = await confirm({
154
+ console.error(" Esc to skip\n");
155
+ const action = await select({
151
156
  message: "Use saved secrets for this project?",
152
- default: true,
157
+ choices: [
158
+ { name: "1. Yes", value: "yes" },
159
+ { name: "2. No", value: "no" },
160
+ ],
153
161
  });
154
162
 
155
- return useSecrets;
163
+ return action === "yes";
156
164
  }
@@ -15,7 +15,9 @@ export interface Project {
15
15
  workerId: string;
16
16
  };
17
17
  resources: {
18
- d1Databases: string[];
18
+ services: {
19
+ db: string | null;
20
+ };
19
21
  };
20
22
  }
21
23
 
@@ -112,3 +114,29 @@ export async function getAllProjects(): Promise<Record<string, Project>> {
112
114
  const registry = await readRegistry();
113
115
  return registry.projects;
114
116
  }
117
+
118
+ /**
119
+ * Get database name for a project
120
+ * @returns Database name or null if no database is configured
121
+ */
122
+ export function getProjectDatabaseName(project: Project): string | null {
123
+ return project.resources?.services?.db ?? null;
124
+ }
125
+
126
+ /**
127
+ * Update the database for a project using the new services structure
128
+ */
129
+ export async function updateProjectDatabase(name: string, dbName: string | null): Promise<void> {
130
+ const project = await getProject(name);
131
+ if (!project) return;
132
+
133
+ await updateProject(name, {
134
+ resources: {
135
+ ...project.resources,
136
+ services: {
137
+ ...project.resources.services,
138
+ db: dbName,
139
+ },
140
+ },
141
+ });
142
+ }
@@ -0,0 +1,81 @@
1
+ import { $ } from "bun";
2
+
3
+ export interface DatabaseInfo {
4
+ name: string;
5
+ id: string;
6
+ sizeBytes: number;
7
+ numTables: number;
8
+ }
9
+
10
+ /**
11
+ * Get database info via wrangler d1 info
12
+ */
13
+ export async function getDatabaseInfo(dbName: string): Promise<DatabaseInfo | null> {
14
+ const result = await $`wrangler d1 info ${dbName} --json`.nothrow().quiet();
15
+
16
+ if (result.exitCode !== 0) {
17
+ return null;
18
+ }
19
+
20
+ try {
21
+ const output = result.stdout.toString().trim();
22
+ const data = JSON.parse(output);
23
+
24
+ // wrangler d1 info --json returns:
25
+ // {
26
+ // "uuid": "...",
27
+ // "name": "...",
28
+ // "version": "...",
29
+ // "num_tables": N,
30
+ // "file_size": N
31
+ // }
32
+ return {
33
+ name: data.name || dbName,
34
+ id: data.uuid || "",
35
+ sizeBytes: data.file_size || 0,
36
+ numTables: data.num_tables || 0,
37
+ };
38
+ } catch (error) {
39
+ // Failed to parse JSON output
40
+ return null;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Export database to SQL file
46
+ */
47
+ export async function exportDatabase(dbName: string, outputPath: string): Promise<void> {
48
+ const result = await $`wrangler d1 export ${dbName} --output ${outputPath}`.nothrow().quiet();
49
+
50
+ if (result.exitCode !== 0) {
51
+ const stderr = result.stderr.toString().trim();
52
+ throw new Error(stderr || `Failed to export database ${dbName} to ${outputPath}`);
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Generate export filename with timestamp
58
+ * Format: {db-name}-{YYYY-MM-DD-HHMMSS}.sql
59
+ */
60
+ export function generateExportFilename(dbName: string): string {
61
+ const now = new Date();
62
+ const year = now.getFullYear();
63
+ const month = String(now.getMonth() + 1).padStart(2, "0");
64
+ const day = String(now.getDate()).padStart(2, "0");
65
+ const hours = String(now.getHours()).padStart(2, "0");
66
+ const minutes = String(now.getMinutes()).padStart(2, "0");
67
+ const seconds = String(now.getSeconds()).padStart(2, "0");
68
+ return `${dbName}-${year}-${month}-${day}-${hours}${minutes}${seconds}.sql`;
69
+ }
70
+
71
+ /**
72
+ * Delete database via wrangler
73
+ */
74
+ export async function deleteDatabase(dbName: string): Promise<void> {
75
+ const result = await $`wrangler d1 delete ${dbName} --skip-confirmation`.nothrow().quiet();
76
+
77
+ if (result.exitCode !== 0) {
78
+ const stderr = result.stderr.toString().trim();
79
+ throw new Error(stderr || `Failed to delete database ${dbName}`);
80
+ }
81
+ }
@@ -0,0 +1,27 @@
1
+ // Normalized service type constants
2
+ export const ServiceType = {
3
+ DB: "db",
4
+ KV: "kv",
5
+ CRON: "cron",
6
+ QUEUE: "queue",
7
+ STORAGE: "storage",
8
+ } as const;
9
+
10
+ export type ServiceTypeKey = keyof typeof ServiceType;
11
+ export type ServiceTypeValue = (typeof ServiceType)[ServiceTypeKey];
12
+
13
+ // Template metadata interface for service requirements
14
+ export interface TemplateServiceRequirements {
15
+ requires: ServiceTypeKey[];
16
+ }
17
+
18
+ // Helper to check if template requires a service
19
+ export function templateRequiresService(
20
+ templateMetadata: TemplateServiceRequirements | undefined,
21
+ serviceType: ServiceTypeKey,
22
+ ): boolean {
23
+ if (!templateMetadata) {
24
+ return false;
25
+ }
26
+ return templateMetadata.requires.includes(serviceType);
27
+ }
@@ -159,6 +159,10 @@ export async function track(event: EventName, properties?: Record<string, unknow
159
159
  // THE MAGIC: withTelemetry() wrapper
160
160
  // Commands wrapped with this get automatic tracking
161
161
  // ============================================
162
+ export interface TelemetryOptions {
163
+ platform?: "cli" | "mcp";
164
+ }
165
+
162
166
  /**
163
167
  * Wrap a command function with automatic telemetry tracking
164
168
  * Tracks command_invoked, command_completed, and command_failed events
@@ -167,22 +171,27 @@ export async function track(event: EventName, properties?: Record<string, unknow
167
171
  export function withTelemetry<T extends (...args: any[]) => Promise<any>>(
168
172
  commandName: string,
169
173
  fn: T,
174
+ options?: TelemetryOptions,
170
175
  ): T {
176
+ const platform = options?.platform ?? "cli";
177
+
171
178
  return (async (...args: Parameters<T>) => {
172
179
  // Fire-and-forget: don't await track() to avoid blocking command execution
173
- track(Events.COMMAND_INVOKED, { command: commandName });
180
+ track(Events.COMMAND_INVOKED, { command: commandName, platform });
174
181
  const start = Date.now();
175
182
 
176
183
  try {
177
184
  const result = await fn(...args);
178
185
  track(Events.COMMAND_COMPLETED, {
179
186
  command: commandName,
187
+ platform,
180
188
  duration_ms: Date.now() - start,
181
189
  });
182
190
  return result;
183
191
  } catch (error) {
184
192
  track(Events.COMMAND_FAILED, {
185
193
  command: commandName,
194
+ platform,
186
195
  error_type: classifyError(error),
187
196
  duration_ms: Date.now() - start,
188
197
  });
@@ -0,0 +1,142 @@
1
+ # Jack MCP Server
2
+
3
+ This directory contains the Model Context Protocol (MCP) server implementation for jack CLI.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ src/mcp/
9
+ ├── server.ts # Main MCP server setup and initialization
10
+ ├── types.ts # TypeScript types for MCP responses
11
+ ├── utils.ts # Response formatting utilities
12
+ ├── tools/
13
+ │ └── index.ts # Tool registration and handlers
14
+ └── resources/
15
+ └── index.ts # Resource registration (agents_md)
16
+ ```
17
+
18
+ ## Key Components
19
+
20
+ ### `server.ts`
21
+ - Creates and configures the MCP server instance
22
+ - Registers tools and resources
23
+ - Exports `startMcpServer()` for stdio transport
24
+
25
+ ### `tools/index.ts`
26
+ - Centralized tool registration and dispatch
27
+ - Implements 4 core tools:
28
+ - `create_project` - Create new Cloudflare Workers project
29
+ - `deploy_project` - Deploy existing project
30
+ - `get_project_status` - Get project status info
31
+ - `list_projects` - List all projects with filters
32
+ - Each tool wraps corresponding function from `lib/project-operations.ts`
33
+ - All tools track telemetry with `platform: 'mcp'`
34
+
35
+ ### `resources/index.ts`
36
+ - Registers `agents://context` resource
37
+ - Reads and combines AGENTS.md and CLAUDE.md from project directory
38
+ - Provides AI agents with project-specific context
39
+
40
+ ### `types.ts`
41
+ - Defines `McpToolResponse<T>` interface
42
+ - Enum of error codes for structured error handling
43
+ - `McpServerOptions` configuration interface
44
+
45
+ ### `utils.ts`
46
+ - `formatSuccessResponse()` - Creates success response with metadata
47
+ - `formatErrorResponse()` - Classifies errors and provides suggestions
48
+ - `classifyMcpError()` - Maps error messages to error codes
49
+ - `getSuggestionForError()` - Returns actionable suggestions
50
+
51
+ ## Response Format
52
+
53
+ All tools return structured JSON responses:
54
+
55
+ ```typescript
56
+ {
57
+ success: boolean
58
+ data?: T
59
+ error?: {
60
+ code: string // Machine-readable
61
+ message: string // Human-readable
62
+ suggestion?: string // Actionable next steps
63
+ }
64
+ meta?: {
65
+ duration_ms: number
66
+ jack_version: string
67
+ }
68
+ }
69
+ ```
70
+
71
+ ## Integration with Project Operations
72
+
73
+ The MCP tools are thin wrappers around functions in `lib/project-operations.ts`:
74
+
75
+ - `createProject()` - Handles project creation with templates
76
+ - `deployProject()` - Manages builds and deployments
77
+ - `getProjectStatus()` - Fetches project status
78
+ - `listAllProjects()` - Lists all registered projects
79
+
80
+ All operations run in "silent mode" (no console output) when called from MCP.
81
+
82
+ ## Telemetry
83
+
84
+ All tool calls are tracked via the existing telemetry system:
85
+
86
+ - Automatic tracking: `command_invoked`, `command_completed`, `command_failed`
87
+ - Business events: `project_created`, `deploy_started`
88
+ - All events tagged with `platform: 'mcp'`
89
+
90
+ ## Development
91
+
92
+ ### Running the Server
93
+
94
+ ```bash
95
+ # Start server via CLI (uses stdio transport)
96
+ jack mcp serve
97
+
98
+ # With explicit project path
99
+ jack mcp serve --project /path/to/project
100
+
101
+ # The server communicates via stdin/stdout
102
+ # Do NOT use console.log in MCP code
103
+ ```
104
+
105
+ ### Adding New Tools
106
+
107
+ 1. Add tool definition to `tools/list` handler in `tools/index.ts`
108
+ 2. Add case to `tools/call` handler switch statement
109
+ 3. Create zod schema for tool parameters
110
+ 4. Wrap function from `project-operations.ts` with telemetry
111
+ 5. Return formatted response using utils
112
+
113
+ ### Testing
114
+
115
+ The MCP server can be tested using:
116
+ - Claude Desktop (see `/docs/mcp-configuration.md`)
117
+ - MCP Inspector (https://github.com/modelcontextprotocol/inspector)
118
+ - Manual JSON-RPC over stdio
119
+
120
+ ## Error Handling
121
+
122
+ Errors are classified into categories:
123
+
124
+ - `AUTH_FAILED` - Authentication issues
125
+ - `WRANGLER_AUTH_EXPIRED` - Wrangler needs re-auth
126
+ - `PROJECT_NOT_FOUND` - Project doesn't exist
127
+ - `TEMPLATE_NOT_FOUND` - Invalid template
128
+ - `BUILD_FAILED` - Build errors
129
+ - `DEPLOY_FAILED` - Deployment errors
130
+ - `VALIDATION_ERROR` - Invalid parameters
131
+ - `INTERNAL_ERROR` - Unexpected failures
132
+
133
+ Each error code includes a helpful suggestion for resolution.
134
+
135
+ ## Protocol Compliance
136
+
137
+ This implementation follows the MCP specification:
138
+ - Uses stdio transport for client communication
139
+ - Implements required handlers: `tools/list`, `tools/call`
140
+ - Implements optional handlers: `resources/list`, `resources/read`
141
+ - Returns properly formatted JSON-RPC responses
142
+ - Never writes to stdout except for protocol messages
@@ -0,0 +1,87 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import {
5
+ ListResourcesRequestSchema,
6
+ ReadResourceRequestSchema,
7
+ } from "@modelcontextprotocol/sdk/types.js";
8
+ import type { McpServerOptions } from "../types.ts";
9
+
10
+ export function registerResources(server: McpServer, options: McpServerOptions) {
11
+ // Register resource list handler
12
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
13
+ return {
14
+ resources: [
15
+ {
16
+ uri: "agents://context",
17
+ name: "Agent Context Files",
18
+ description:
19
+ "Project-specific context files (AGENTS.md, CLAUDE.md) for AI agents working on this project",
20
+ mimeType: "text/markdown",
21
+ },
22
+ ],
23
+ };
24
+ });
25
+
26
+ // Register resource read handler
27
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
28
+ const uri = request.params.uri;
29
+
30
+ if (uri === "agents://context") {
31
+ const projectPath = options.projectPath ?? process.cwd();
32
+ const agentsPath = join(projectPath, "AGENTS.md");
33
+ const claudePath = join(projectPath, "CLAUDE.md");
34
+
35
+ const contents: string[] = [];
36
+
37
+ // Try to read AGENTS.md
38
+ if (existsSync(agentsPath)) {
39
+ try {
40
+ const agentsContent = await Bun.file(agentsPath).text();
41
+ contents.push("# AGENTS.md\n\n");
42
+ contents.push(agentsContent);
43
+ } catch {
44
+ // Ignore read errors
45
+ }
46
+ }
47
+
48
+ // Try to read CLAUDE.md
49
+ if (existsSync(claudePath)) {
50
+ try {
51
+ const claudeContent = await Bun.file(claudePath).text();
52
+ if (contents.length > 0) {
53
+ contents.push("\n\n---\n\n");
54
+ }
55
+ contents.push("# CLAUDE.md\n\n");
56
+ contents.push(claudeContent);
57
+ } catch {
58
+ // Ignore read errors
59
+ }
60
+ }
61
+
62
+ if (contents.length === 0) {
63
+ return {
64
+ contents: [
65
+ {
66
+ uri,
67
+ mimeType: "text/plain",
68
+ text: "No agent context files found in project directory",
69
+ },
70
+ ],
71
+ };
72
+ }
73
+
74
+ return {
75
+ contents: [
76
+ {
77
+ uri,
78
+ mimeType: "text/markdown",
79
+ text: contents.join(""),
80
+ },
81
+ ],
82
+ };
83
+ }
84
+
85
+ throw new Error(`Unknown resource URI: ${uri}`);
86
+ });
87
+ }
@@ -0,0 +1,32 @@
1
+ import { Server as McpServer } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import pkg from "../../package.json" with { type: "json" };
4
+ import { registerResources } from "./resources/index.ts";
5
+ import { registerTools } from "./tools/index.ts";
6
+ import type { McpServerOptions } from "./types.ts";
7
+
8
+ export async function createMcpServer(options: McpServerOptions = {}) {
9
+ const server = new McpServer(
10
+ {
11
+ name: "jack",
12
+ version: pkg.version,
13
+ },
14
+ {
15
+ capabilities: {
16
+ tools: {},
17
+ resources: {},
18
+ },
19
+ },
20
+ );
21
+
22
+ registerTools(server, options);
23
+ registerResources(server, options);
24
+
25
+ return server;
26
+ }
27
+
28
+ export async function startMcpServer(options: McpServerOptions = {}) {
29
+ const server = await createMcpServer(options);
30
+ const transport = new StdioServerTransport();
31
+ await server.connect(transport);
32
+ }