Skip to content

feat: embed MCP server for IDE-integrated Jira access (jira mcp serve)#985

Open
Charzander wants to merge 27 commits intoankitpokhrel:mainfrom
Charzander:feat/mcp-server
Open

feat: embed MCP server for IDE-integrated Jira access (jira mcp serve)#985
Charzander wants to merge 27 commits intoankitpokhrel:mainfrom
Charzander:feat/mcp-server

Conversation

@Charzander
Copy link
Copy Markdown

@Charzander Charzander commented Apr 20, 2026

Summary

Adds an embedded Model Context Protocol server to jira-cli, exposed as jira 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 jira and parsing --plain output is brittle and loses structure. MCP gives the LLM a typed interface, and jira-cli already has the config/auth/HTTP plumbing sorted — this PR is just a thin adapter layer over pkg/jira (via the existing api.Proxy* helpers).

What's new

Packages

  • internal/mcp/ — server constructor wrapping github.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 shared Deps{Client, Server, DefaultProject, Installation} struct and bodyToMarkdown helper. Handlers depend only on pkg/jira / pkg/adf — no cobra/viper/survey/tui imports, enforced by package doc-comment convention.
  • internal/cmd/mcp/ — Cobra surface: jira mcp parent and jira mcp serve leaf.

Cobra wiring

One new child in internal/cmd/root/root.go:

mcp.NewCmdMCP(),

The parent command sets Annotations{"cmd:main": "true"} so it appears in the MAIN COMMANDS section of jira --help alongside issue, epic, sprint, etc.

Dependency

  • github.com/modelcontextprotocol/go-sdk v1.5.0 (the official Tier-1 SDK, maintained with Google, Apache-2.0).
  • Transitive bump of golang.org/x/sys.

Behavior highlights

  • Honors browse_server viper override when building issue URLs, matching internal/cmdutil.GenerateServerBrowseURL.
  • Force-disables debug mode on the MCP path, because 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.
  • All tool errors (bad input, Jira API failures, panics) come back as MCP tool results with IsError: true so the LLM can self-correct while the transport stays healthy.
  • Installation-type (Cloud vs. Local) v2/v3 dispatch reuses the existing api.Proxy* functions, so both cloud and on-prem work the same way they do everywhere else.

v1 scope decisions

  • Stdio only. HTTP/SSE deferred; design allows adding --http :PORT later without changing the public interface.
  • No MCP resources or prompts yet. Tools cover the main use cases.
  • Simpler-than-CLI tool surface. Five tools, not twenty — keeps the LLM's tool-list context lean. Edit, assign, link, epic/sprint membership, deletion, worklog, and releases are deferred.
  • Dropped create_issue.Parent: CreateRequest.ParentIssueKey routes 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 when pkg/jira grows first-class linker support.
  • Dropped transition_issue.Assignee: upstream's TransitionRequestFields.Assignee is {name: ...}-only, which Cloud ignores for account-id users. Users can call a separate assign step on Cloud.
  • Project defaulting, not required. Tools fall back to viper.GetString("project.key") when project is 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-cli reads (JIRA_CONFIG_FILE, ~/.config/.jira/.config.yml, .netrc, keychain) all continue to work unchanged.

Test plan

Green locally:

  • go test ./... — 32 new tests (30 in internal/mcp/tools, 2 in internal/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 with GOTOOLCHAIN=go1.25.6).
  • go run ./cmd/jira mcp --help / go run ./cmd/jira mcp serve --help print expected help text.

Notable coverage:

  • All five tools have input-validation, happy-path, and Jira-error tests using httptest.NewServer.
  • TestServer_ListsAllTools runs an in-memory SDK round-trip that lists tools, calls one successfully, and calls one with missing required fields (asserting the IsError: true tool-result path).
  • TestRegisterTool_RecoversFromPanic registers a deliberately-panicking handler and asserts the transport survives and the LLM sees a tool error.
  • TestSearchIssues_Local, TestGetIssue_Local, TestCreateIssue_Local exercise the v2/on-prem branches of api.ProxySearch / ProxyGetIssue / ProxyCreate, including verifying that CreateRequest.ForInstallationType produces {"name": "alice"} rather than {"accountId": "alice"} on Local.

Ways to try it end-to-end:

  • Add the JSON snippet above to Cursor / Claude Desktop and ask the LLM to list your issues.
  • Manual JSON-RPC: 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

  1. Does the dependency footprint feel OK? The MCP Go SDK is official (Tier-1, Apache-2.0) but it's a new top-level dep. I can put the whole internal/mcp tree and its import from internal/cmd/root behind a build tag (//go:build mcp) if you'd prefer users opt in at compile time.
  2. Are there repo conventions I've missed? The test style matches pkg/jira/*_test.go (httptest.NewServer + testify). Command wiring matches existing internal/cmd/<name>/<name>.go + subfolder pattern. Doc style in the README section mirrors the existing sections.

Thanks for considering!

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
…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
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
@Charzander
Copy link
Copy Markdown
Author

Did a self-review before flipping to ready. Two follow-ups pushed to the branch in 7ddb4c5:

  1. Stdout leak on --debug is now fully closed. Previously, internal/cmd/root/root.go's cobra.OnInitialize would print "Using config file: ..." to stdout when --debug was set, which happens before any subcommand's RunE, so the defensive debug-force-disable in serve.go couldn't catch it. Routed that print to stderr — strict improvement for every command, since debug output belongs there anyway.

  2. Deps.Installation has a doc comment clarifying it's only consumed by CreateIssue (for ForInstallationType) — the other tools still dispatch v2/v3 via viper inside api.Proxy*. Notes that tests exercising non-Cloud paths need to set both the struct field and the viper key. Avoids a foot-gun for future contributors who might assume the field drives all dispatch.

Not changed, but surfacing for your call:

  • add_comment skips the api.Proxy* pattern. It calls client.AddIssueComment directly because pkg/jira only has one version (no AddIssueCommentV3), so there's nothing for a ProxyAddComment to dispatch between. Happy to add a thin api.ProxyAddComment wrapper for consistency if you'd prefer that house rule — or to soften the PR description to "reuses api.Proxy* where proxies exist." Let me know.

  • Internal flag on add_comment silently no-ops on non-Jira-Service-Desk projects (it's the sd.public.comment property under the hood). Could strengthen the schema description from "mark as an internal (service-desk) comment" to "(no-op on non-Service-Desk projects)" so an LLM doesn't quietly get a public comment. Will add if you'd like.

  • JQL composition in search_issues uses %q for quoting. Works for today's project/status/assignee values, but Jira's JQL quoting rules aren't identical to Go's — a status like "Won't Fix" could produce a Jira parse error the user'd recover from via the tool error, but it's not ideal. Happy to add proper JQL escaping if you'd like.

@Charzander Charzander marked this pull request as ready for review April 20, 2026 15:58
Charzander added a commit to Charzander/jira-cli that referenced this pull request Apr 20, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant