jj-mcp-server
Version:
Model Context Protocol (MCP) server for the Jujutsu (jj) version control system
1,075 lines • 61.1 kB
JavaScript
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
const JJ_COMMAND = "jj";
const server = new McpServer({
name: "jj-mcp-server",
version: "1.0.0",
capabilities: {
tools: {},
},
});
// Helper to run jj commands and return stdout or error text
async function runJJCommand(args, cwd) {
try {
const { stdout } = await execFileAsync(JJ_COMMAND, args, { cwd });
return stdout.trim();
}
catch (error) {
if (error.stderr) {
return `Error: ${error.stderr.trim()}`;
}
return `Error: ${error.message}`;
}
}
// Tool: status
server.tool("status", "Show the high-level status of the Jujutsu (jj) repository, including the working copy commit, parent commits, and a summary of changes. Useful for getting an overview of the current state of your repository. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["status"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: rebase
server.tool("rebase", "Rebase one or more revisions onto a different parent revision in a Jujutsu (jj) repository. Commonly used to move changes to a new base or to clean up commit history. Parameters: source (Revisions to rebase, e.g., '@-'), destination (Destination revision, e.g., 'main'), repoPath (Optional repo path), cwd (Optional working directory to run the command in).", {
// Revisions to rebase, e.g. "@-"
source: z.string(),
// Destination revision, e.g. "main"
destination: z.string(),
// Optional repo path
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ source, destination, repoPath, cwd }) => {
const args = ["rebase", "--source", source, "--destination", destination];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: commit
server.tool("commit", "Update the current change with the specified message in a Jujutsu (jj) repository and then create and move to a new, empty change. This set of actions is jj's closest analog to committing in a Git repository. Parameters: message (Commit message), repoPath (Optional repo path), cwd (Optional working directory to run the command in).", {
// Commit message
message: z.string(),
// Optional repo path
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ message, repoPath, cwd }) => {
const args = ["commit", "-m", message];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: new
server.tool("new", "Create a new, empty change with optional parent revisions in a Jujutsu (jj) repository. Useful for starting a new line of development or feature branch. Parameters: parents (Optional parent revisions, comma separated), repoPath (Optional repo path), cwd (Optional working directory to run the command in).", {
// Optional parent revisions, comma separated
parents: z.string().optional(),
// Optional repo path
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ parents, repoPath, cwd }) => {
const args = ["new"];
if (parents) {
args.push("--parents", parents);
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: abandon
server.tool("abandon", "Abandon one or more revisions, rebasing descendants onto their parents in a Jujutsu (jj) repository. Useful for discarding changes or cleaning up history. Parameters: revisions (Revisions to abandon, e.g., '@'), repoPath (Optional repo path), cwd (Optional working directory to run the command in).", {
// Revisions to abandon, e.g. "@"
revisions: z.string(),
// Optional repo path
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ revisions, repoPath, cwd }) => {
const args = ["abandon", revisions];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
/* Tool: log */
server.tool("log", "Show the commit history of a Jujutsu (jj) repository. Useful for reviewing past changes and understanding project evolution. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), limit (Optional number of commits to show).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional number of commits to show
limit: z.number().int().positive().optional(),
}, async ({ repoPath, cwd, limit }) => {
const args = ["log"];
if (repoPath) {
args.push("--repository", repoPath);
}
if (limit !== undefined) {
args.push("-n", limit.toString());
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: create
server.tool("bookmark-create", "Create a new bookmark in a Jujutsu (jj) repository. Useful for marking important points in history or creating named branches. Parameters: name (Name of the bookmark to create), revision (Optional revision to point the bookmark at), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Name of the bookmark to create
name: z.string(),
// Optional revision to point the bookmark at
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ name, revision, repoPath, cwd }) => {
const args = ["bookmark", "create", name];
if (revision) {
args.push("-r", revision);
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: delete
server.tool("bookmark-delete", "Delete an existing bookmark in a Jujutsu (jj) repository and propagate the deletion to remotes on the next push. Parameters: names (Names of bookmarks to delete), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Names of bookmarks to delete
names: z.array(z.string()),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ names, repoPath, cwd }) => {
const args = ["bookmark", "delete", ...names];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: forget
server.tool("bookmark-forget", "Forget a bookmark in a Jujutsu (jj) repository without marking it as a deletion to be pushed. Parameters: names (Names of bookmarks to forget), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Names of bookmarks to forget
names: z.array(z.string()),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ names, repoPath, cwd }) => {
const args = ["bookmark", "forget", ...names];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: list
server.tool("bookmark-list", "List bookmarks and their targets in a Jujutsu (jj) repository. Useful for reviewing all named points in history. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), template (Optional template for output formatting).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional template for output formatting
template: z.string().optional(),
}, async ({ repoPath, cwd, template }) => {
const args = ["bookmark", "list"];
if (repoPath) {
args.push("--repository", repoPath);
}
if (template) {
args.push("-T", template);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: move
server.tool("bookmark-move", "Move existing bookmarks to target revision in a Jujutsu (jj) repository. Useful for updating bookmark positions. Parameters: names (Names of bookmarks to move), revision (Target revision to move bookmarks to), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Names of bookmarks to move
names: z.array(z.string()),
// Target revision to move bookmarks to
revision: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ names, revision, repoPath, cwd }) => {
const args = ["bookmark", "move", ...names, "-t", revision];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: rename
server.tool("bookmark-rename", "Rename a bookmark in a Jujutsu (jj) repository. Useful for updating bookmark names. Parameters: oldName (Current name of the bookmark), newName (New name for the bookmark), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Current name of the bookmark
oldName: z.string(),
// New name for the bookmark
newName: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ oldName, newName, repoPath, cwd }) => {
const args = ["bookmark", "rename", oldName, newName];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: set
server.tool("bookmark-set", "Create or update a bookmark to point to a certain commit in a Jujutsu (jj) repository. Parameters: name (Name of the bookmark to set), revision (Revision to point the bookmark at), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Name of the bookmark to set
name: z.string(),
// Revision to point the bookmark at
revision: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ name, revision, repoPath, cwd }) => {
const args = ["bookmark", "set", name, "-r", revision];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: track
server.tool("bookmark-track", "Start tracking a remote bookmark in a Jujutsu (jj) repository. Parameters: remoteBookmark (Remote bookmark to track, format: bookmark@remote), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Remote bookmark to track (format: bookmark@remote)
remoteBookmark: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ remoteBookmark, repoPath, cwd }) => {
const args = ["bookmark", "track", remoteBookmark];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Bookmark: untrack
server.tool("bookmark-untrack", "Stop tracking a remote bookmark in a Jujutsu (jj) repository. Parameters: remoteBookmark (Remote bookmark to untrack, format: bookmark@remote), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Remote bookmark to untrack (format: bookmark@remote)
remoteBookmark: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ remoteBookmark, repoPath, cwd }) => {
const args = ["bookmark", "untrack", remoteBookmark];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: diff
server.tool("diff", "Compare file contents between two revisions in a Jujutsu (jj) repository. With the `--from` and/or `--to` options, shows the difference from/to the given revisions. If either is left out, it defaults to the working-copy commit. Parameters: from (Show changes from this revision), to (Show changes to this revision), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), context (Optional number of lines of context to show), stat (Optional flag to show a histogram of changes).", {
// Show changes from this revision
from: z.string().optional(),
// Show changes to this revision
to: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional number of lines of context to show
context: z.number().int().nonnegative().optional(),
// Optional flag to show a histogram of changes
stat: z.boolean().optional(),
}, async ({ from, to, repoPath, cwd, context, stat }) => {
const args = ["diff"];
if (from) {
args.push("-f", from);
}
if (to) {
args.push("-t", to);
}
if (repoPath) {
args.push("--repository", repoPath);
}
if (context !== undefined) {
args.push("--context", context.toString());
}
if (stat) {
args.push("--stat");
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-clone
server.tool("git-clone", "Create a new Jujutsu (jj) repository backed by a clone of a Git repo. The Git repo will be a bare git repo stored inside the .jj/ directory. Parameters: source (URL or path of the Git repo to clone), destination (Optional destination directory for the clone), remoteName (Optional name for the newly created remote, default: origin), colocate (Optional flag to colocate the jj repo with the git repo), depth (Optional depth for shallow clone).", {
// URL or path of the Git repo to clone
source: z.string(),
// Optional destination directory for the clone
destination: z.string().optional(),
// Optional name for the newly created remote (default: origin)
remoteName: z.string().optional(),
// Optional flag to colocate the jj repo with the git repo
colocate: z.boolean().optional(),
// Optional depth for shallow clone
depth: z.number().int().positive().optional(),
}, async ({ source, destination, remoteName, colocate, depth }) => {
const args = ["git", "clone", source];
if (destination) {
args.push(destination);
}
if (remoteName) {
args.push("--remote", remoteName);
}
if (colocate) {
args.push("--colocate");
}
if (depth) {
args.push("--depth", depth.toString());
}
const output = await runJJCommand(args);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-export
server.tool("git-export", "Update the underlying Git repo with changes made in the Jujutsu (jj) repository. Useful for syncing jj changes back to Git. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["git", "export"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-fetch
server.tool("git-fetch", "Fetch branches and bookmarks from a Git remote into a Jujutsu (jj) repository. Similar to git fetch but preserves the jj-specific state. Parameters: remote (Remote to fetch from, default: origin), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), branches (Optional branch/patterns to fetch, can specify multiple).", {
// Remote to fetch from (default: origin)
remote: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional branch/patterns to fetch (can specify multiple)
branches: z.array(z.string()).optional(),
}, async ({ remote, repoPath, cwd, branches }) => {
const args = ["git", "fetch"];
if (remote) {
args.push("--remote", remote);
}
if (repoPath) {
args.push("--repository", repoPath);
}
if (branches) {
args.push("--branch", ...branches);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-import
server.tool("git-import", "Update the Jujutsu (jj) repository with changes made in the underlying Git repo. Useful for syncing Git changes into jj. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["git", "import"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: init
server.tool("init", "Create a new Jujutsu (jj) repository backed by a Git repo. Can create a new Git repo or import an existing one. Parameters: destination (Optional destination directory), colocate (Optional flag to colocate the jj repo with the git repo), gitRepo (Optional path to existing git repo to use).", {
// Optional destination directory
destination: z.string().optional(),
// Optional flag to colocate the jj repo with the git repo
colocate: z.boolean().optional(),
// Optional path to existing git repo to use
gitRepo: z.string().optional(),
}, async ({ destination, colocate, gitRepo }) => {
const args = ["git", "init"];
if (destination) {
args.push(destination);
}
if (colocate) {
args.push("--colocate");
}
if (gitRepo) {
args.push("--git-repo", gitRepo);
}
const output = await runJJCommand(args);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-push
server.tool("git-push", "Push branches and bookmarks to a Git remote from a Jujutsu (jj) repository. Similar to git push but preserves the jj-specific state. Parameters: remote (Remote to push to, default: origin), bookmarks (Optional bookmarks to push, can specify multiple), all (Optional flag to push all bookmarks), tracked (Optional flag to push tracked bookmarks), deleted (Optional flag to push deleted bookmarks), allowNew (Optional flag to allow pushing new bookmarks), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Remote to push to (default: origin)
remote: z.string().optional(),
// Optional bookmarks to push (can specify multiple)
bookmarks: z.array(z.string()).optional(),
// Optional flag to push all bookmarks
all: z.boolean().optional(),
// Optional flag to push tracked bookmarks
tracked: z.boolean().optional(),
// Optional flag to push deleted bookmarks
deleted: z.boolean().optional(),
// Optional flag to allow pushing new bookmarks
allowNew: z.boolean().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ remote, bookmarks, all, tracked, deleted, allowNew, repoPath, cwd, }) => {
const args = ["git", "push"];
if (remote) {
args.push("--remote", remote);
}
if (bookmarks) {
args.push("--bookmark", ...bookmarks);
}
if (all) {
args.push("--all");
}
if (tracked) {
args.push("--tracked");
}
if (deleted) {
args.push("--deleted");
}
if (allowNew) {
args.push("--allow-new");
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-remote-add
server.tool("git-remote-add", "Add a Git remote to a Jujutsu (jj) repository. Parameters: name (Name of the remote to add), url (URL of the remote), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Name of the remote to add
name: z.string(),
// URL of the remote
url: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ name, url, repoPath, cwd }) => {
const args = ["git", "remote", "add", name, url];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-remote-list
server.tool("git-remote-list", "List Git remotes in a Jujutsu (jj) repository. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["git", "remote", "list"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-remote-remove
server.tool("git-remote-remove", "Remove a Git remote from a Jujutsu (jj) repository. Parameters: name (Name of the remote to remove), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Name of the remote to remove
name: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ name, repoPath, cwd }) => {
const args = ["git", "remote", "remove", name];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-remote-rename
server.tool("git-remote-rename", "Rename a Git remote in a Jujutsu (jj) repository. Parameters: oldName (Current name of the remote), newName (New name for the remote), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Current name of the remote
oldName: z.string(),
// New name for the remote
newName: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ oldName, newName, repoPath, cwd }) => {
const args = ["git", "remote", "rename", oldName, newName];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-remote-set-url
server.tool("git-remote-set-url", "Set the URL of a Git remote in a Jujutsu (jj) repository. Parameters: name (Name of the remote), url (New URL for the remote), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Name of the remote
name: z.string(),
// New URL for the remote
url: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ name, url, repoPath, cwd }) => {
const args = ["git", "remote", "set-url", name, url];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: git-root
server.tool("git-root", "Show the underlying Git directory of a repository using the Git backend in Jujutsu (jj). Useful for finding the .git directory location. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["git", "root"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: show
server.tool("show", "Show description and changes in a revision of a Jujutsu (jj) repository compared to its parent(s). Useful for reviewing the exact changes made in a specific commit. Parameters: revision (The revision to show, defaults to working copy), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), context (Optional number of lines of context to show).", {
// The revision to show (defaults to working copy)
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional number of lines of context to show
context: z.number().int().nonnegative().optional(),
}, async ({ revision, repoPath, cwd, context }) => {
const args = ["show"];
if (revision) {
args.push("-r", revision);
}
if (repoPath) {
args.push("--repository", repoPath);
}
if (context !== undefined) {
args.push("--context", context.toString());
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: revert
server.tool("revert", "Apply the reverse of one or more revisions in a Jujutsu (jj) repository. Creates new changes that undo the specified commits. Useful for safely undoing changes. Parameters: revisions (Revisions to revert), destination (Optional destination revision, where to apply the revert), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Revisions to revert
revisions: z.array(z.string()),
// Optional destination revision (where to apply the revert)
destination: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ revisions, destination, repoPath, cwd }) => {
const args = ["revert"];
if (destination) {
args.push("-d", destination);
}
args.push(...revisions);
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: restore
server.tool("restore", "Restore paths from another revision in a Jujutsu (jj) repository. Can undo changes to specific files by restoring them to their previous state. Parameters: source (Optional source revision to restore from, defaults to parent), destination (Optional destination revision to restore into, defaults to working copy), paths (Paths to restore), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional source revision to restore from (defaults to parent)
source: z.string().optional(),
// Optional destination revision to restore into (defaults to working copy)
destination: z.string().optional(),
// Paths to restore
paths: z.array(z.string()),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ source, destination, paths, repoPath, cwd }) => {
const args = ["restore"];
if (source) {
args.push("-f", source);
}
if (destination) {
args.push("-t", destination);
}
args.push(...paths);
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: interdiff
server.tool("interdiff", "Compare the changes between two commits in a Jujutsu (jj) repository. Shows only the difference between the two diffs, excluding changes from other commits. Parameters: from (First commit to compare), to (Second commit to compare), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), context (Optional number of lines of context to show).", {
// First commit to compare
from: z.string(),
// Second commit to compare
to: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional number of lines of context to show
context: z.number().int().nonnegative().optional(),
}, async ({ from, to, repoPath, cwd, context }) => {
const args = ["interdiff", "-f", from, "-t", to];
if (repoPath) {
args.push("--repository", repoPath);
}
if (context !== undefined) {
args.push("--context", context.toString());
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: evolog
server.tool("evolog", "Show how a change has evolved over time in a Jujutsu (jj) repository. Lists previous commits which a change has pointed to as it was updated, rebased, etc. Parameters: revision (Revision to show evolution for), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), limit (Optional limit on number of revisions to show), patch (Optional flag to show patch compared to previous version).", {
// Revision to show evolution for
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional limit on number of revisions to show
limit: z.number().int().positive().optional(),
// Optional flag to show patch compared to previous version
patch: z.boolean().optional(),
}, async ({ revision, repoPath, cwd, limit, patch }) => {
const args = ["evolog"];
if (revision) {
args.push("-r", revision);
}
if (repoPath) {
args.push("--repository", repoPath);
}
if (limit) {
args.push("-n", limit.toString());
}
if (patch) {
args.push("-p");
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: edit
server.tool("edit", "Sets the specified revision as the working-copy revision in a Jujutsu (jj) repository. Note: it is generally recommended to instead use `jj new` and `jj squash`. Parameters: revision (The commit to edit, e.g., 'my-branch' or a commit ID), repoPath (Optional path to the repository root or a working directory within the repository), cwd (Optional working directory to run the command in).", {
revision: z
.string()
.describe("The commit to edit, e.g., 'my-branch' or a commit ID."),
repoPath: z
.string()
.optional()
.describe("Optional path to the repository root or a working directory within the repository."),
cwd: z
.string()
.optional()
.describe("Optional working directory to run the command in."),
}, async ({ revision, repoPath, cwd }) => {
const args = ["edit", revision];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-annotate
server.tool("file-annotate", "Show the source change for each line of a file in a Jujutsu (jj) repository. Useful for understanding how each line of a file has evolved. Parameters: path (Path to the file to annotate), revision (Optional revision to annotate from, defaults to working copy), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Path to the file to annotate
path: z.string(),
// Optional revision to annotate from (defaults to working copy)
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ path, revision, repoPath, cwd }) => {
const args = ["file", "annotate"];
if (revision) {
args.push("-r", revision);
}
args.push(path);
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-chmod
server.tool("file-chmod", "Sets or removes the executable bit for files in a Jujutsu (jj) repository. Unlike POSIX chmod, this works on Windows and on arbitrary revisions. Parameters: mode (Mode to set, 'x' for executable, 'n' for non-executable), paths (Paths to modify), revision (Optional revision to modify, defaults to working copy), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Mode to set ('x' for executable, 'n' for non-executable)
mode: z.enum(["x", "n"]),
// Paths to modify
paths: z.array(z.string()),
// Optional revision to modify (defaults to working copy)
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ mode, paths, revision, repoPath, cwd }) => {
const args = ["file", "chmod"];
if (revision) {
args.push("-r", revision);
}
args.push(mode);
args.push(...paths);
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-list
server.tool("file-list", "List files in a revision of a Jujutsu (jj) repository. Useful for exploring the repository contents at a specific point in history. Parameters: revision (Optional revision to list, defaults to working copy), paths (Optional paths to filter by), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional revision to list (defaults to working copy)
revision: z.string().optional(),
// Optional paths to filter by
paths: z.array(z.string()).optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ revision, paths, repoPath, cwd }) => {
const args = ["file", "list"];
if (revision) {
args.push("-r", revision);
}
if (paths) {
args.push(...paths);
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-show
server.tool("file-show", "Print contents of files in a revision of a Jujutsu (jj) repository. Similar to Unix cat but works on repository files at any revision. Parameters: paths (Paths of files to show), revision (Optional revision to show from, defaults to working copy), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Paths of files to show
paths: z.array(z.string()),
// Optional revision to show from (defaults to working copy)
revision: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ paths, revision, repoPath, cwd }) => {
const args = ["file", "show"];
if (revision) {
args.push("-r", revision);
}
args.push(...paths);
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-track
server.tool("file-track", "Start tracking specified paths in the working copy of a Jujutsu (jj) repository. By default new files are automatically tracked; this is primarily useful when auto-tracking is disabled. Parameters: paths (Paths to track), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Paths to track
paths: z.array(z.string()),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ paths, repoPath, cwd }) => {
const args = ["file", "track", ...paths];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: file-untrack
server.tool("file-untrack", "Stop tracking specified paths in the working copy of a Jujutsu (jj) repository. The paths must already be ignored via .gitignore or other ignore rules. Parameters: paths (Paths to untrack), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Paths to untrack
paths: z.array(z.string()),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ paths, repoPath, cwd }) => {
const args = ["file", "untrack", ...paths];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: squash
server.tool("squash", "Move changes from one revision into another revision in a Jujutsu (jj) repository. Only supports whole-file changes (no --interactive flag support). Parameters: source (Source revision to squash from, defaults to working copy), destination (Destination revision to squash into), paths (Optional path patterns to squash, whole paths only, no partial file changes), keepEmptied (Optional flag to keep emptied source revision), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Source revision to squash from (defaults to working copy)
source: z.string().optional(),
// Destination revision to squash into
destination: z.string().optional(),
// Optional path patterns to squash (whole paths only, no partial file changes)
paths: z.array(z.string()).optional(),
// Optional flag to keep emptied source revision
keepEmptied: z.boolean().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ source, destination, paths, keepEmptied, repoPath, cwd }) => {
const args = ["squash"];
if (source) {
args.push("-f", source);
}
if (destination) {
args.push("-t", destination);
}
if (paths) {
args.push(...paths);
}
if (keepEmptied) {
args.push("-k");
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: workspace-root
server.tool("workspace-root", "Show the current workspace root directory in a Jujutsu (jj) repository. Useful for determining the base directory of the current workspace. Parameters: repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ repoPath, cwd }) => {
const args = ["workspace", "root"];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: operation-abandon
server.tool("operation-abandon", "Abandon operation history in a Jujutsu (jj) repository. Discards old operations and reparents descendants onto the root operation. Parameters: operation (Operation or range to abandon), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Operation or range to abandon
operation: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ operation, repoPath, cwd }) => {
const args = ["operation", "abandon", operation];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: operation-diff
server.tool("operation-diff", "Compare changes to the repository between two operations in a Jujutsu (jj) repository. Shows what changed between operation states. Parameters: from (Operation to show changes from), to (Operation to show changes to), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in), context (Optional number of lines of context to show).", {
// Operation to show changes from
from: z.string().optional(),
// Operation to show changes to
to: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
// Optional number of lines of context to show
context: z.number().int().nonnegative().optional(),
}, async ({ from, to, repoPath, cwd, context }) => {
const args = ["operation", "diff"];
if (from) {
args.push("-f", from);
}
if (to) {
args.push("-t", to);
}
if (repoPath) {
args.push("--repository", repoPath);
}
if (context !== undefined) {
args.push("--context", context.toString());
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: operation-log
server.tool("operation-log", "Show the operation log of a Jujutsu (jj) repository. Lists all operations performed on the repository in chronological order. Parameters: limit (Optional limit on number of operations to show), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Optional limit on number of operations to show
limit: z.number().int().positive().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ limit, repoPath, cwd }) => {
const args = ["operation", "log"];
if (limit) {
args.push("-n", limit.toString());
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: operation-restore
server.tool("operation-restore", "Create a new operation that restores the repo to an earlier state in a Jujutsu (jj) repository. Effectively undoes all operations after the specified one. Parameters: operation (Operation to restore to), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Operation to restore to
operation: z.string(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ operation, repoPath, cwd }) => {
const args = ["operation", "restore", operation];
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(args, cwd);
return {
content: [{ type: "text", text: output }],
};
});
// Tool: operation-show
server.tool("operation-show", "Show changes to the repository in an operation of a Jujutsu (jj) repository. Displays what changed in the specified operation. Parameters: operation (Operation to show, defaults to latest), repoPath (Optional path to repo root or working directory), cwd (Optional working directory to run the command in).", {
// Operation to show (defaults to latest)
operation: z.string().optional(),
// Optional path to repo root or working directory
repoPath: z.string().optional(),
// Optional working directory to run the command in
cwd: z.string().optional(),
}, async ({ operation, repoPath, cwd }) => {
const args = ["operation", "show"];
if (operation) {
args.push(operation);
}
if (repoPath) {
args.push("--repository", repoPath);
}
const output = await runJJCommand(