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 intoankitpokhrel:mainfrom
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
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!