Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,12 @@ When uploading Workers to Cloudflare's dispatch namespace, upload ALL files from

## Jack MCP (for AI Agents)

**CRITICAL:** When Jack MCP is connected, always prefer `mcp__jack__*` tools over CLI commands or wrangler. MCP tools are cloud-aware and work with Jack Cloud mode where wrangler won't.
When Jack MCP is connected, prefer `mcp__jack__*` tools for **cloud operations** (deploy, databases, logs, crons, domains, storage, vectorize) over CLI commands or wrangler. MCP tools are cloud-aware and work with Jack Cloud mode where wrangler won't.

- Check your available tools for anything prefixed with `mcp__jack__`
- Use those instead of `jack` CLI commands or `wrangler` commands
- For deployments, databases, logs, and services: use `mcp__jack__*` tools
- For file editing: always use built-in Read/Edit/Write tools, never remote MCP tools like `stage_file` or `read_deployed_file`
- If a capability isn't available via MCP, ask the user to run it via CLI
- The local MCP (`mcp__jack__*`) and remote MCP (`mcp__jack_cloud__*`) are mutually exclusive — never connect both simultaneously

## Deploy Mode: Managed vs BYO

Expand Down Expand Up @@ -238,3 +239,13 @@ Rules:
- `findWranglerConfig()` in `wrangler-config.ts` — wrangler config path resolution (supports .jsonc, .json, .toml)
- `parseJsonc()` in `jsonc.ts` — JSONC parsing (never use regex comment stripping)
- `readProjectLink()` + `getDeployMode()` in `project-link.ts` — project link and deploy mode

**After modifying shell integration (`shell-integration.ts`), regenerate the shell file for local testing.**

`jack update` only regenerates `~/.config/jack/shell.sh` after a successful npm update — it won't pick up local dev changes. After editing `shell-integration.ts`, run:

```bash
bun -e "import { writeShellFile } from './apps/cli/src/lib/shell-integration.ts'; writeShellFile();"
```

Then open a new terminal (or `source ~/.zshrc`) to load the updated shell function. Without this, you'll be testing against the stale shell.sh from the last published version.
4 changes: 2 additions & 2 deletions apps/cli/src/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ async function outputProjectContext(): Promise<void> {
lines.push("- Status: `mcp__jack__get_project_status` or `jack info`");
lines.push("");
lines.push(
"Prefer `mcp__jack__*` tools or `jack` CLI over raw `wrangler` commands for consistency.",
"For cloud operations, prefer `mcp__jack__*` tools or `jack` CLI over raw `wrangler` commands. For file editing, use built-in Read/Edit/Write tools.",
);
}

Expand All @@ -272,7 +272,7 @@ async function outputProjectContext(): Promise<void> {
);
lines.push("");
lines.push(
"**Always prefer `mcp__jack__*` tools over CLI commands or wrangler** — they are cloud-aware and work in all deploy modes.",
"**For cloud operations (deploy, databases, logs, services), prefer `mcp__jack__*` tools over CLI commands or wrangler** — they are cloud-aware and work in all deploy modes. For file editing, always use built-in Read/Edit/Write tools.",
);
sections.push(lines.join("\n"));

Expand Down
42 changes: 42 additions & 0 deletions apps/cli/src/lib/mcp-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,45 @@ export function isAppInstalled(appId: string): boolean {
return existsSync(configDir);
}

/**
* Check if an MCP server entry looks like a remote Jack MCP connection.
*/
function isRemoteJackMcpEntry(entry: unknown): boolean {
if (!entry || typeof entry !== "object") return false;
const obj = entry as Record<string, unknown>;

if (typeof obj.url === "string" && obj.url.includes("getjack.org")) {
return true;
}

if (typeof obj.command === "string" && obj.command.includes("getjack.org")) {
return true;
}

return false;
}

/**
* Scan mcpServers for remote Jack MCP entries and remove them.
* Remote entries conflict with the local stdio-based MCP server.
*/
function removeRemoteJackMcpEntries(mcpServers: Record<string, unknown>): void {
const keysToRemove: string[] = [];

for (const [name, entry] of Object.entries(mcpServers)) {
if (name === "jack-cloud" || isRemoteJackMcpEntry(entry)) {
keysToRemove.push(name);
}
}

for (const key of keysToRemove) {
console.error(
`\n⚠ Removing remote Jack MCP server ("${key}") — it conflicts with the local server.\n Local MCP covers all remote capabilities when you have filesystem access.\n Remote MCP is only needed in terminal-less environments (claude.ai web).\n`,
);
delete mcpServers[key];
}
}

/**
* Install MCP config to a single app
* Reads existing config, merges jack server, writes back
Expand Down Expand Up @@ -163,6 +202,9 @@ export async function installMcpConfigToApp(appId: string): Promise<boolean> {
// Get or create mcpServers object
const mcpServers = (existingConfig[appConfig.key] as Record<string, unknown>) || {};

// Detect and remove remote Jack MCP server entries that conflict with local
removeRemoteJackMcpEntries(mcpServers);

// Add/update jack MCP server
mcpServers.jack = getJackMcpConfig();

Expand Down
4 changes: 3 additions & 1 deletion apps/cli/src/mcp/resources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,13 @@ This project is managed by jack.

## MCP Tools Available

If connected, prefer \`mcp__jack__*\` tools over CLI:
For cloud operations (deploy, databases, logs, services), prefer \`mcp__jack__*\` tools over CLI:
- \`mcp__jack__deploy_project\` - Deploy changes
- \`mcp__jack__execute_sql\` - Query databases
- \`mcp__jack__get_project_status\` - Check status

For file editing, always use built-in Read/Edit/Write tools — never remote MCP tools.

## Documentation

Full docs: https://docs.getjack.org/llms-full.txt
Expand Down
32 changes: 22 additions & 10 deletions apps/mcp-worker/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,36 @@ src/
├── deploy-template.ts # Deploy prebuilt template via control plane
├── endpoint-test.ts # HTTP request to deployed endpoints
├── projects.ts # list_projects, get_project_status
├── source.ts # list/read project files, update_file staging
├── source.ts # browse/read deployed files, stage_file staging
├── logs.ts # get_logs
├── database.ts # create_database, list_databases, execute_sql
└── rollback.ts # rollback_project
```

## Local vs Remote MCP — Mutually Exclusive

The local MCP (`jack mcp serve`, stdio) and remote MCP (`mcp.getjack.org`, HTTP) serve different environments and should **never be connected simultaneously**:

| Environment | MCP Server | Why |
|-------------|-----------|-----|
| Claude Code (has terminal + local FS) | Local only (`jack`) | Agent has Read/Edit/Write for files, local MCP for cloud ops |
| claude.ai web (no terminal) | Remote only (`jack-cloud`) | No local FS, must use stage_file + deploy for file changes |
| Claude Desktop | One or the other | Depends on whether shell access is available |

If both are connected, agents use remote file tools (`stage_file`, `read_deployed_file`) instead of local filesystem tools, causing partial deploys and failures. The `jack mcp install` command detects and removes remote MCP configs to prevent this.

## Tools (14 total)

| Tool | Type | Description |
|------|------|-------------|
| `deploy` | write | Unified deploy: `files`, `template`, `changes`, or `staged` mode |
| `update_file` | write | Stage a file change for later deployment via `deploy(staged=true)` |
| `list_staged_changes` | read | List files staged via `update_file` pending deployment |
| `stage_file` | write | Stage a file change for later deployment via `deploy(staged=true)` |
| `list_staged_files` | read | List files staged via `stage_file` pending deployment |
| `list_projects` | read | List all user's projects with URLs |
| `get_project_status` | read | Deployment status, URL, resources for a project |
| `test_endpoint` | read | HTTP request to a deployed endpoint, returns status + body |
| `list_project_files` | read | File tree of deployed project's source |
| `read_project_file` | read | Read single source file from deployed project |
| `browse_deployed_source` | read | File tree of deployed project's source |
| `read_deployed_file` | read | Read single source file from deployed project |
| `get_logs` | read | Start log session and collect entries |
| `create_database` | write | Create D1 database for a project (idempotent) |
| `list_databases` | read | List D1 databases for a project |
Expand All @@ -64,8 +76,8 @@ This is the core workflow the tools enable:
→ Returns project_id + live URL

2. User: "add a /forecast endpoint"
→ LLM calls list_project_files(project_id) → sees current files
→ LLM calls read_project_file(project_id, "src/index.ts") → gets source
→ LLM calls browse_deployed_source(project_id) → sees current files
→ LLM calls read_deployed_file(project_id, "src/index.ts") → gets source
→ LLM modifies the code
→ LLM calls deploy(changes: {"src/index.ts": "..."}, project_id) → redeploys

Expand All @@ -75,8 +87,8 @@ This is the core workflow the tools enable:
→ LLM calls test_endpoint(project_id, "/api/health") → verifies fix

4. Large file update (>15KB):
→ LLM calls update_file(project_id, "src/index.ts", content) → stages file
→ LLM calls update_file(project_id, "src/styles.css", content) → stages another
→ LLM calls stage_file(project_id, "src/index.ts", content) → stages file
→ LLM calls stage_file(project_id, "src/styles.css", content) → stages another
→ LLM calls deploy(project_id, staged=true) → deploys all staged changes
```

Expand Down Expand Up @@ -106,7 +118,7 @@ The remote MCP (this worker) has 10 tools. The local MCP (`apps/cli/src/mcp/`) h
| Category | Local MCP | Remote MCP | Notes |
|----------|-----------|------------|-------|
| Deploy/Projects | create_project, deploy_project, get_project_status, list_projects, rollback_project | deploy, list_projects, get_project_status, rollback_project | Remote merged create+deploy into `deploy` |
| Source | — | list_project_files, read_project_file | Remote-only (new) |
| Source | — | browse_deployed_source, read_deployed_file | Remote-only (new) |
| Logs | start_log_session, tail_logs | get_logs | Remote merged into one |
| Database | create_database, list_databases, execute_sql | create_database, list_databases, execute_sql | Aligned |
| Vectorize | create/list/delete/get_vectorize_index | — | Not yet in remote |
Expand Down
36 changes: 23 additions & 13 deletions apps/mcp-worker/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type { Bindings } from "./types.ts";

export function createMcpServer(token: string, env: Bindings): McpServer {
const server = new McpServer({
name: "jack",
name: "jack-cloud",
version: "1.0.0",
});

Expand All @@ -26,11 +26,11 @@ export function createMcpServer(token: string, env: Bindings): McpServer {
- files: Full file set for a brand-new project. Pass all source files as { "path": "content" }. Only use this for the FIRST deploy of a new custom project.
- template: Deploy a prebuilt template (hello, api, miniapp, nextjs, saas). Always creates a new project.
- changes: Partial update to an existing project. Pass only changed/added files as { "path": "new content" } or { "path": null } to delete. Requires project_id.
- staged: Deploy files previously staged via update_file. Set staged=true with project_id. Use this when files are too large to pass inline in a single changes call.
- staged: Deploy files previously staged via stage_file. Set staged=true with project_id. Use this when files are too large to pass inline in a single changes call.

IMPORTANT: To update an existing project, ALWAYS use changes mode with project_id. Do NOT use files mode for existing projects — it replaces all files and may create a duplicate project. If the user mentions an existing app, call list_projects first to find its project_id, then use changes.

For large files (>15KB): Use update_file to stage files one at a time, then deploy(staged=true, project_id). This avoids output token limits that can truncate large inline content.`,
For large files (>15KB): Use stage_file to stage files one at a time, then deploy(staged=true, project_id). This avoids output token limits that can truncate large inline content.`,
{
files: z
.record(z.string(), z.string())
Expand All @@ -52,7 +52,7 @@ For large files (>15KB): Use update_file to stage files one at a time, then depl
.boolean()
.optional()
.describe(
"Deploy files previously staged via update_file calls. Requires project_id. Use when files are too large for inline changes.",
"Deploy files previously staged via stage_file calls. Requires project_id. Use when files are too large for inline changes.",
),
project_id: z
.string()
Expand All @@ -73,8 +73,10 @@ For large files (>15KB): Use update_file to stage files one at a time, then depl
);

server.tool(
"update_file",
`Stage a file change for later deployment. Use this to build up changes across multiple calls, then deploy them all at once with deploy(staged=true).
"stage_file",
`Stage a file for cloud deployment via deploy(staged=true). Only use this in environments WITHOUT local filesystem access (e.g. claude.ai web, Claude Desktop without terminal).

If you have built-in Read/Edit/Write tools available, do NOT use this tool — edit files locally and deploy with deploy_project or jack ship instead.

Best for:
- Large files that exceed output token limits when passed inline via changes
Expand All @@ -100,8 +102,8 @@ Pass content=null to mark a file for deletion.`,
);

server.tool(
"list_staged_changes",
"List files currently staged via update_file that haven't been deployed yet. Use to review pending changes before calling deploy(staged=true).",
"list_staged_files",
"List files currently staged via stage_file that haven't been deployed yet. Use to review pending changes before calling deploy(staged=true).",
{
project_id: z.string().describe("The project ID"),
},
Expand Down Expand Up @@ -188,8 +190,12 @@ Pass content=null to mark a file for deletion.`,
);

server.tool(
"list_project_files",
"List all source files in a deployed project. Use before read_project_file to see what files exist, or before deploying with changes to understand current project structure.",
"browse_deployed_source",
`List all source files in the DEPLOYED version of a project on Jack Cloud. Shows the file tree as it exists in production, not local files.

If you have local filesystem access (e.g. Claude Code with Glob/LS tools), read the local project directory instead — it's faster and more accurate.

Use before read_deployed_file to see what files exist, or before deploying with changes to understand current project structure.`,
{
project_id: z.string().describe("The project ID"),
},
Expand All @@ -199,11 +205,15 @@ Pass content=null to mark a file for deletion.`,
);

server.tool(
"read_project_file",
"Read the contents of a single source file from a deployed project. Use after list_project_files to inspect specific files before making changes with deploy(changes).",
"read_deployed_file",
`Read the contents of a single source file from the DEPLOYED version on Jack Cloud. Returns the file as it exists in production.

If you have local filesystem access (e.g. Claude Code with the Read tool), read the local file instead — it's faster and always up-to-date with your working copy.

Use after browse_deployed_source to inspect specific files before making changes with deploy(changes).`,
{
project_id: z.string().describe("The project ID"),
path: z.string().describe("File path from list_project_files (e.g. 'src/index.ts')"),
path: z.string().describe("File path from browse_deployed_source (e.g. 'src/index.ts')"),
},
async ({ project_id, path }) => {
return readProjectFile(client, project_id, path);
Expand Down
2 changes: 1 addition & 1 deletion apps/mcp-worker/src/staging.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* File staging for multi-call deploys.
*
* Uses KV to accumulate file changes across multiple update_file calls,
* Uses KV to accumulate file changes across multiple stage_file calls,
* then deploy reads and clears the staged changes. This works around
* LLM output token limits that prevent sending large files in a single
* tool call.
Expand Down
6 changes: 3 additions & 3 deletions apps/mcp-worker/src/tools/deploy-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export async function deploy(
const { files, template, changes, staged, project_id, project_name, compatibility_flags } =
params;

// Staged mode: deploy from accumulated update_file calls
// Staged mode: deploy from accumulated stage_file calls
if (staged) {
if (files || template || changes) {
return err(
Expand All @@ -202,7 +202,7 @@ export async function deploy(
return err(
"VALIDATION_ERROR",
"No staged changes found for this project.",
"Use update_file to stage file changes before deploying with staged=true.",
"Use stage_file to stage file changes before deploying with staged=true.",
);
}

Expand Down Expand Up @@ -252,7 +252,7 @@ export async function deploy(
return err(
"VALIDATION_ERROR",
"Exactly one of files, template, changes, or staged=true must be provided.",
"Pass files for a full deploy, template for a prebuilt app, changes for a partial update, or staged=true to deploy files from update_file calls.",
"Pass files for a full deploy, template for a prebuilt app, changes for a partial update, or staged=true to deploy files from stage_file calls.",
);
}
if (modeCount > 1) {
Expand Down
4 changes: 2 additions & 2 deletions apps/mcp-worker/src/tools/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function readProjectFile(
return err(
"NOT_FOUND",
message,
"Check the file path. Use list_project_files to see available files.",
"Check the file path. Use browse_deployed_source to see available files.",
);
}
}
Expand Down Expand Up @@ -93,7 +93,7 @@ export async function listStagedChanges(
return ok({
staged_files: 0,
files: [],
note: "No staged changes. Use update_file to stage file changes before deploying.",
note: "No staged changes. Use stage_file to stage file changes before deploying.",
});
}

Expand Down
Loading