Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c1eccf8
docs: add design spec for jira-cli MCP server
Charzander Apr 17, 2026
d77cdbe
docs: add implementation plan for jira-cli MCP server
Charzander Apr 17, 2026
bd8de50
feat(mcp): add internal/mcp package skeleton
Charzander Apr 17, 2026
eb3fc80
docs(mcp): defer SDK dependency to Task 2 (first SDK importer)
Charzander Apr 17, 2026
c1a44d5
docs(mcp): defer SDK fetch to Task 8 (first SDK importer)
Charzander Apr 17, 2026
fb8f581
feat(mcp): add tools.Deps with project/url/installation helpers
Charzander Apr 17, 2026
9a39e5b
docs(mcp): honor browse_server viper override in serve plan
Charzander Apr 17, 2026
59bdbfd
feat(mcp): add search_issues tool
Charzander Apr 17, 2026
e53ccb4
docs(mcp): clarify JQL passthrough vs. composed-JQL project scoping
Charzander Apr 17, 2026
0537782
docs(mcp): tighten search_issues schema and rename Total to Returned
Charzander Apr 17, 2026
d7fc5ad
feat(mcp): add bodyToMarkdown helper for ADF/string description bodies
Charzander Apr 17, 2026
e5a58f8
feat(mcp): add get_issue tool
Charzander Apr 17, 2026
b0cde56
feat(mcp): add create_issue tool
Charzander Apr 17, 2026
95e405e
feat(mcp): add add_comment tool
Charzander Apr 17, 2026
b14e3dd
feat(mcp): add transition_issue tool
Charzander Apr 17, 2026
296a0c3
docs(mcp): fix get_issue comment Author to use DisplayName (v3 API re…
Charzander Apr 17, 2026
a1495e3
docs(mcp): drop create_issue.Parent and transition_issue.Assignee for…
Charzander Apr 17, 2026
e499e2a
feat(mcp): narrow create_issue and transition_issue schemas for v1
Charzander Apr 17, 2026
19f03a6
feat(mcp): add Server constructor and in-memory round-trip test
Charzander Apr 17, 2026
a665efc
feat(mcp): add 'jira mcp serve' cobra command (stdio transport)
Charzander Apr 17, 2026
262927b
docs: document the jira-cli MCP server
Charzander Apr 17, 2026
bde6cf5
docs(mcp): drop unused context import from serve.go code block
Charzander Apr 17, 2026
eec04f5
fix(mcp): prevent --debug from corrupting stdio and cover panic recovery
Charzander Apr 17, 2026
9ddac87
test(mcp): add v2/Local installation path coverage
Charzander Apr 17, 2026
50c4285
chore: remove workflow-internal design docs from tree
Charzander Apr 20, 2026
ae4b6e9
chore(mcp): apply golangci-lint fixes and add cmd:main annotation
Charzander Apr 20, 2026
7ddb4c5
fix(mcp): route root's debug "Using config file" line to stderr
Charzander Apr 20, 2026
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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,36 @@ jira board list
```
</details>

## MCP server

`jira-cli` ships an embedded [Model Context Protocol](https://modelcontextprotocol.io) server so MCP-aware hosts (Cursor, Claude Desktop, etc.) can read and modify Jira issues during a coding session. The server reuses the same config, auth, and Jira API client as the rest of the CLI.

Start it from your MCP host configuration:

```json
{
"mcpServers": {
"jira": {
"command": "jira",
"args": ["mcp", "serve"],
"env": { "JIRA_API_TOKEN": "..." }
}
}
}
```

The server speaks stdio and exposes the following tools:

| Tool | Purpose |
| --- | --- |
| `search_issues` | Search by raw JQL or simple `status`/`assignee` filters. |
| `get_issue` | Full issue details including description and recent comments. |
| `create_issue` | Create a new issue in a project. |
| `add_comment` | Add a comment to an issue. |
| `transition_issue` | Move an issue to a new status by name. |

Every tool that returns an issue also returns its browser URL so the LLM can cite or link to it directly.

## Scripts
Often times, you may want to use the output of the command to do something cool. However, the default interactive UI might not allow you to do that.
The tool comes with the `--plain` flag that displays results in a simple layout that can then be manipulated from the shell script.
Expand Down
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/mitchellh/go-homedir v1.1.0
github.com/modelcontextprotocol/go-sdk v1.5.0
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/rivo/tview v0.0.0-20240406141410-79d4cc321256
github.com/russross/blackfriday/v2 v2.1.0
Expand Down Expand Up @@ -47,6 +48,7 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gdamore/encoding v1.0.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/jsonschema-go v0.4.2 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 // indirect
Expand All @@ -64,18 +66,22 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
24 changes: 20 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAY
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
Expand Down Expand Up @@ -109,6 +113,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU=
github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
Expand Down Expand Up @@ -136,6 +142,10 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
github.com/segmentio/encoding v0.5.4/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
Expand Down Expand Up @@ -165,6 +175,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
Expand All @@ -187,6 +199,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -200,8 +214,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand All @@ -220,6 +234,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
Expand Down
25 changes: 25 additions & 0 deletions internal/cmd/mcp/mcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mcp

import (
"github.com/spf13/cobra"

"github.com/ankitpokhrel/jira-cli/internal/cmd/mcp/serve"
)

const helpText = `Run jira-cli as a Model Context Protocol (MCP) server, exposing
Jira operations to MCP-aware hosts (e.g. Cursor, Claude Desktop).`

// NewCmdMCP is the parent command for MCP-related subcommands.
func NewCmdMCP() *cobra.Command {
cmd := &cobra.Command{
Use: "mcp",
Short: "Run jira-cli as an MCP server",
Long: helpText,
Annotations: map[string]string{"cmd:main": "true"},
RunE: func(cmd *cobra.Command, _ []string) error {
return cmd.Help()
},
}
cmd.AddCommand(serve.NewCmdServe())
return cmd
}
85 changes: 85 additions & 0 deletions internal/cmd/mcp/serve/serve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package serve

import (
"fmt"
"os"
"os/signal"
"syscall"

mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/ankitpokhrel/jira-cli/api"
jiramcp "github.com/ankitpokhrel/jira-cli/internal/mcp"
"github.com/ankitpokhrel/jira-cli/internal/mcp/tools"
)

const helpText = `Start an MCP server over stdio.

Configure your MCP host (Cursor, Claude Desktop, etc.) like this:

{
"mcpServers": {
"jira": {
"command": "jira",
"args": ["mcp", "serve"],
"env": { "JIRA_API_TOKEN": "..." }
}
}
}

The server inherits the same configuration as every other jira-cli command:
JIRA_CONFIG_FILE, ~/.config/.jira/.config.yml, .netrc, and keychain all work
unchanged. The server reads from stdin and writes JSON-RPC frames to stdout;
all logs go to stderr.`

// NewCmdServe is the `jira mcp serve` command.
func NewCmdServe() *cobra.Command {
return &cobra.Command{
Use: "serve",
Short: "Start an MCP server over stdio",
Long: helpText,
RunE: run,
}
}

func run(cmd *cobra.Command, _ []string) error {
server := viper.GetString("server")
if server == "" {
return fmt.Errorf("no Jira server configured. Run 'jira init' to set up the tool")
}

// Honor browse_server override the same way internal/cmdutil.GenerateServerBrowseURL does,
// so MCP-emitted issue URLs match what the rest of the CLI produces for users whose web
// client and API endpoints differ.
browseServer := server
if v := viper.GetString("browse_server"); v != "" {
browseServer = v
}

// Stdout is reserved for JSON-RPC frames; pkg/jira's debug dump and
// root's "Using config file: ..." both write to stdout when debug is
// enabled, which would corrupt the MCP transport. Force-disable here
// and surface a stderr notice if the user had it on in config.
if viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "jira-cli MCP server: ignoring debug=true (would corrupt stdio transport)")
}
deps := &tools.Deps{
Client: api.DefaultClient(false),
Server: browseServer,
DefaultProject: viper.GetString("project.key"),
Installation: viper.GetString("installation"),
}

srv := jiramcp.NewServer(deps)

ctx, stop := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
defer stop()

fmt.Fprintln(os.Stderr, "jira-cli MCP server: listening on stdio")
if err := srv.Run(ctx, &mcpsdk.StdioTransport{}); err != nil {
return fmt.Errorf("mcp server: %w", err)
}
return nil
}
4 changes: 3 additions & 1 deletion internal/cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
initCmd "github.com/ankitpokhrel/jira-cli/internal/cmd/init"
"github.com/ankitpokhrel/jira-cli/internal/cmd/issue"
"github.com/ankitpokhrel/jira-cli/internal/cmd/man"
"github.com/ankitpokhrel/jira-cli/internal/cmd/mcp"
"github.com/ankitpokhrel/jira-cli/internal/cmd/me"
"github.com/ankitpokhrel/jira-cli/internal/cmd/open"
"github.com/ankitpokhrel/jira-cli/internal/cmd/project"
Expand Down Expand Up @@ -64,7 +65,7 @@ func init() {
viper.SetEnvPrefix("jira")

if err := viper.ReadInConfig(); err == nil && debug {
fmt.Printf("Using config file: %s\n", viper.ConfigFileUsed())
fmt.Fprintln(os.Stderr, "Using config file:", viper.ConfigFileUsed())
}
})
}
Expand Down Expand Up @@ -140,6 +141,7 @@ func addChildCommands(cmd *cobra.Command) {
version.NewCmdVersion(),
release.NewCmdRelease(),
man.NewCmdMan(),
mcp.NewCmdMCP(),
)
}

Expand Down
4 changes: 4 additions & 0 deletions internal/mcp/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package mcp implements a Model Context Protocol server that exposes
// a subset of jira-cli's capabilities to MCP-aware hosts (e.g. Cursor,
// Claude Desktop). Wiring lives in internal/cmd/mcp.
package mcp
94 changes: 94 additions & 0 deletions internal/mcp/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package mcp

import (
"context"
"fmt"
"os"

mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"

"github.com/ankitpokhrel/jira-cli/internal/mcp/tools"
)

const (
// ServerName is the implementation name advertised over MCP.
ServerName = "jira-cli"
// ServerVersion is the MCP server version advertised to clients.
// Bumped independently of the jira-cli release version when the MCP
// surface changes in a backward-incompatible way.
ServerVersion = "0.1.0"
)

// NewServer constructs a configured *mcp.Server with all jira-cli tools
// registered. The caller is responsible for invoking server.Run with a
// transport.
func NewServer(d *tools.Deps) *mcpsdk.Server {
srv := mcpsdk.NewServer(&mcpsdk.Implementation{
Name: ServerName,
Version: ServerVersion,
}, nil)

registerTool(srv, "search_issues",
"Search Jira issues by JQL or simple filters. Defaults to the configured project.",
d, tools.SearchIssues)

registerTool(srv, "get_issue",
"Get full details of a Jira issue including description and recent comments.",
d, tools.GetIssue)

registerTool(srv, "create_issue",
"Create a new Jira issue in the given project.",
d, tools.CreateIssue)

registerTool(srv, "add_comment",
"Add a comment to a Jira issue.",
d, tools.AddComment)

registerTool(srv, "transition_issue",
"Transition a Jira issue to a new status by name (e.g. \"In Progress\", \"Done\").",
d, tools.TransitionIssue)

return srv
}

// registerTool adapts a tools.* handler (which takes Deps + Input and returns
// Output + error) onto the SDK's expected handler signature. It also recovers
// from panics in the handler body so a single bad call cannot kill the server
// mid-session, and converts both errors and panics into MCP tool errors that
// the LLM can read.
func registerTool[In, Out any](
srv *mcpsdk.Server,
name, description string,
d *tools.Deps,
fn func(context.Context, *tools.Deps, In) (Out, error),
) {
mcpsdk.AddTool(srv,
&mcpsdk.Tool{Name: name, Description: description},
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in In) (result *mcpsdk.CallToolResult, out Out, err error) {
defer func() {
if r := recover(); r != nil {
var zero Out
fmt.Fprintf(os.Stderr, "mcp: panic in tool %q: %v\n", name, r)
result = &mcpsdk.CallToolResult{
IsError: true,
Content: []mcpsdk.Content{&mcpsdk.TextContent{
Text: fmt.Sprintf("internal error in tool %q: %v", name, r),
}},
}
out = zero
err = nil
}
}()

out, callErr := fn(ctx, d, in)
if callErr != nil {
var zero Out
return &mcpsdk.CallToolResult{
IsError: true,
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: callErr.Error()}},
}, zero, nil
}
return nil, out, nil
},
)
}
Loading
Loading