feat: embed MCP server for IDE-integrated Jira access (jira mcp serve)#985
feat: embed MCP server for IDE-integrated Jira access (jira mcp serve)#985Charzander wants to merge 27 commits into
Conversation
Captures the v1 design for an MCP server integrated into jira-cli: thin tool layer over pkg/jira, exposed as `jira mcp serve` over stdio, five tools (search/get/create/comment/transition), reusing existing config and auth. Intended for upstream PR. Made-with: Cursor
Bite-sized, TDD-style plan that walks through adding the MCP server package, the five tools (search/get/create/comment/transition), the `jira mcp serve` cobra command, and README docs. Each task is self-contained with full test code, full implementation code, exact commands, and a commit step. Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
…turns displayName, not name) Made-with: Cursor
… v1 (unreliable on Cloud/classic) Made-with: Cursor
Drop create_issue.Parent (epic/sub-task linking needs project-type resolution
we don't do yet) and transition_issue.Assignee (pkg/jira only supports v2-style
{name} bodies, which Cloud ignores for account-id users).
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
Made-with: Cursor
pkg/jira's debug dump writes to stdout, which would corrupt the JSON-RPC framing used by the stdio MCP transport. Force-disable debug in the MCP path with a stderr notice so users know their config flag is being ignored for this session. Also add a test that panic inside a tool handler surfaces as a tool error (IsError=true) without killing the transport. Made-with: Cursor
Every existing tool test pins viper.Set("installation", Cloud) and only
exercises the v3 branches of api.ProxySearch / ProxyGetIssue / ProxyCreate.
Add three tests that route through the v2 branches instead:
- TestSearchIssues_Local hits /rest/api/2/search with startAt in the query.
- TestGetIssue_Local serves a v2 body with a plain-string description and
verifies bodyToMarkdown(string) works end-to-end.
- TestCreateIssue_Local verifies CreateRequest.ForInstallationType serializes
{"name": "alice"} rather than {"accountId": "alice"} for the assignee.
Made-with: Cursor
The specs/ and plans/ files under docs/superpowers/ are Cursor-workflow artifacts (TDD plans, design iterations) useful during development but not appropriate for the upstream repository. History preserves them for reference; the delivered tree does not. Made-with: Cursor
- Add Annotations{cmd:main: true} to the `jira mcp` parent so it appears
under MAIN COMMANDS in root --help, matching the convention used by
issue, epic, sprint, board, project, open, and release.
- Wrap session.Close() in defer funcs in server_test.go (errcheck).
- Extract the 50/100 magic numbers in search_issues.go's limit clamp to
defaultSearchLimit / maxSearchLimit named constants (mnd).
golangci-lint run ./... now reports 0 issues (v2.6.2, GOTOOLCHAIN=go1.25.6).
Made-with: Cursor
The print fires from cobra.OnInitialize, which runs before any subcommand's RunE, so the defensive force-disable of debug in `jira mcp serve` happens too late to prevent the message from corrupting the stdio JSON-RPC stream if the user invokes `jira mcp serve --debug`. Stderr is the correct destination for debug/log output in every command anyway, so the change is a strict improvement for non-MCP users too. Also add a doc comment on tools.Deps.Installation clarifying that it's only consumed by CreateIssue; the other tools still dispatch v2/v3 via viper inside api.Proxy*, and tests exercising non-Cloud paths must set both fields. Made-with: Cursor
|
Did a self-review before flipping to ready. Two follow-ups pushed to the branch in
Not changed, but surfacing for your call:
|
Adds an embedded MCP server exposed as `jira mcp serve`, letting MCP-aware hosts (Cursor, Claude Desktop, etc.) read and modify Jira issues during a coding session while reusing the CLI's existing config, auth, and HTTP client. Single-user, IDE-focused v1 with five tools: search_issues, get_issue, create_issue, add_comment, transition_issue. Stdio transport only. New packages: - internal/mcp/ — server constructor + SDK wiring + panic recovery - internal/mcp/tools/ — five handlers, Deps DI struct, bodyToMarkdown helper - internal/cmd/mcp/ — Cobra surface (jira mcp parent + serve leaf) Wiring change: one line added to internal/cmd/root/root.go to register the new command. The "Using config file" debug print in cobra.OnInitialize is also routed to stderr so debug output never corrupts the stdio JSON-RPC stream when running `jira mcp serve --debug`. New direct dependency: github.com/modelcontextprotocol/go-sdk v1.5.0 (official Tier-1 SDK, Apache-2.0). Tracks upstream PR ankitpokhrel#985. Made-with: Cursor
|
Have you made any comparison of how does this compare with simply using the CLI commands (token usage wise)? I'm personally using this skill with my AI agents and its been doing the work nicely so far. I've heard (though I didn't extensively test myself) that MCP servers are generally much more token hungry than CLI tools, so I'd take this with a bit of care. |
Summary
Adds an embedded Model Context Protocol server to
jira-cli, exposed asjira mcp serve. It lets MCP-aware hosts (Cursor, Claude Desktop, etc.) read and modify Jira issues during a coding session while reusing the CLI's existing config, auth, and HTTP client.Single-user, IDE-focused v1. Five tools:
search_issues,get_issue,create_issue,add_comment,transition_issue. Stdio transport only.Why
When an LLM in your IDE needs Jira context (which ticket am I working on? what does it say? who's assigned?) or wants to record progress (file a bug, comment on the one I'm fixing, transition to In Progress), shelling out to
jiraand parsing--plainoutput is brittle and loses structure. MCP gives the LLM a typed interface, andjira-clialready has the config/auth/HTTP plumbing sorted — this PR is just a thin adapter layer overpkg/jira(via the existingapi.Proxy*helpers).What's new
Packages
internal/mcp/— server constructor wrappinggithub.com/modelcontextprotocol/go-sdk, registers tools via a small generic adapter, recovers from handler panics so a single bad call can't kill the session.internal/mcp/tools/— five tool handlers plus a sharedDeps{Client, Server, DefaultProject, Installation}struct andbodyToMarkdownhelper. Handlers depend only onpkg/jira/pkg/adf— nocobra/viper/survey/tuiimports, enforced by package doc-comment convention.internal/cmd/mcp/— Cobra surface:jira mcpparent andjira mcp serveleaf.Cobra wiring
One new child in
internal/cmd/root/root.go:The parent command sets
Annotations{"cmd:main": "true"}so it appears in the MAIN COMMANDS section ofjira --helpalongsideissue,epic,sprint, etc.Dependency
github.com/modelcontextprotocol/go-sdk v1.5.0(the official Tier-1 SDK, maintained with Google, Apache-2.0).golang.org/x/sys.Behavior highlights
browse_serverviper override when building issue URLs, matchinginternal/cmdutil.GenerateServerBrowseURL.pkg/jira's debug dump writes to stdout and would corrupt the JSON-RPC stream. Prints a stderr notice if the user had debug enabled.IsError: trueso the LLM can self-correct while the transport stays healthy.api.Proxy*functions, so both cloud and on-prem work the same way they do everywhere else.v1 scope decisions
--http :PORTlater without changing the public interface.create_issue.Parent:CreateRequest.ParentIssueKeyroutes through project-type-aware fields (EpicField,SubtaskField) we don't resolve in this layer, so exposing it would silently drop epic links on classic projects. Add back whenpkg/jiragrows first-class linker support.transition_issue.Assignee: upstream'sTransitionRequestFields.Assigneeis{name: ...}-only, which Cloud ignores for account-id users. Users can call a separateassignstep on Cloud.viper.GetString("project.key")whenprojectis omitted.Usage
After installing the binary, point your MCP host at it:
{ "mcpServers": { "jira": { "command": "jira", "args": ["mcp", "serve"], "env": { "JIRA_API_TOKEN": "..." } } } }The same env vars and config file the rest of
jira-clireads (JIRA_CONFIG_FILE,~/.config/.jira/.config.yml,.netrc, keychain) all continue to work unchanged.Test plan
Green locally:
go test ./...— 32 new tests (30 ininternal/mcp/tools, 2 ininternal/mcp) plus every pre-existing package test still passes.go vet ./...clean.gofmt -l ./gofumpt -l .clean.golangci-lint run ./...— 0 issues (v2.6.2 withGOTOOLCHAIN=go1.25.6).go run ./cmd/jira mcp --help/go run ./cmd/jira mcp serve --helpprint expected help text.Notable coverage:
httptest.NewServer.TestServer_ListsAllToolsruns an in-memory SDK round-trip that lists tools, calls one successfully, and calls one with missing required fields (asserting theIsError: truetool-result path).TestRegisterTool_RecoversFromPanicregisters a deliberately-panicking handler and asserts the transport survives and the LLM sees a tool error.TestSearchIssues_Local,TestGetIssue_Local,TestCreateIssue_Localexercise the v2/on-prem branches ofapi.ProxySearch/ProxyGetIssue/ProxyCreate, including verifying thatCreateRequest.ForInstallationTypeproduces{"name": "alice"}rather than{"accountId": "alice"}on Local.Ways to try it end-to-end:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"manual","version":"0"}}}' | jira mcp serve.Open items I'd appreciate maintainer input on
internal/mcptree and its import frominternal/cmd/rootbehind a build tag (//go:build mcp) if you'd prefer users opt in at compile time.pkg/jira/*_test.go(httptest.NewServer+testify). Command wiring matches existinginternal/cmd/<name>/<name>.go+ subfolder pattern. Doc style in the README section mirrors the existing sections.Thanks for considering!