Conversation
Signed-off-by: Kim Christensen <[email protected]>
License lives at repo root; subpackages have no own LICENSE files, causing go-licenses to fail. Introduced transitively via github.com/modelcontextprotocol/go-sdk. Signed-off-by: Kim Christensen <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds a new porter mcp CLI subcommand that runs a Model Context Protocol (MCP) server over stdio, exposing Porter read-only (and optionally write) operations as MCP tools for AI clients.
Changes:
- Introduces a new
pkg/mcppackage implementing the MCP server and tool handlers (runs/logs, outputs, credentials, parameters, bundle ops, failure analysis). - Wires the new
porter mcpCobra command into the CLI and adds documentation pages describing usage and client configuration. - Adds the MCP Go SDK dependency and updates license-check workflow ignores.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/mcp/server.go | Core MCP server wrapper, stdio transport, output capture/masking helpers. |
| pkg/mcp/tools_bundle.go | Registers bundle + installation tools; adds opt-in write tools with output capture. |
| pkg/mcp/tools_runs.go | Adds run listing and log retrieval tools. |
| pkg/mcp/tools_outputs.go | Adds output listing/get tools, including masking/sensitive blocking. |
| pkg/mcp/tools_creds.go | Adds credential set list/show tools. |
| pkg/mcp/tools_params.go | Adds parameter set list/show tools. |
| pkg/mcp/tools_analyze.go | Adds analyze_failure tool aggregating run/log/output context. |
| cmd/porter/mcp.go | Implements the porter mcp subcommand and --allow-write flag. |
| cmd/porter/main.go | Registers the new mcp subcommand on the root command. |
| docs/content/docs/references/cli/porter.md | Adds porter mcp to the CLI reference index. |
| docs/content/docs/references/cli/mcp.md | Adds CLI reference page for porter mcp. |
| docs/content/docs/how-to-guides/work-with-ai-agents.md | Adds a how-to guide for configuring MCP clients with Porter. |
| docs/content/docs/how-to-guides/_index.md | Adds “Working with AI Agents” to the how-to guide cards. |
| go.mod | Adds github.com/modelcontextprotocol/go-sdk v1.4.1 dependency. |
| go.sum | Updates checksums for new dependencies. |
| .github/workflows/check-licenses.yaml | Updates ignored modules list for license checking. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (s *MCPServer) listRuns(_ context.Context, req *sdkmcp.CallToolRequest) (*sdkmcp.CallToolResult, error) { | ||
| var args struct { | ||
| Installation string `json:"installation"` | ||
| Namespace string `json:"namespace"` | ||
| } | ||
| if err := unmarshalArgs(req, &args); err != nil { | ||
| return toolErr(err), nil | ||
| } | ||
|
|
||
| opts := porter.RunListOptions{} | ||
| opts.Name = args.Installation | ||
| opts.Namespace = args.Namespace | ||
|
|
There was a problem hiding this comment.
list_runs relies on the JSON schema to require "installation", but the handler doesn't enforce it. If arguments are omitted/empty, opts.Name stays empty and the call can return unexpected results. Add a server-side check that installation is non-empty and return a tool error when missing.
| func (s *MCPServer) listOutputs(_ context.Context, req *sdkmcp.CallToolRequest) (*sdkmcp.CallToolResult, error) { | ||
| var args struct { | ||
| Installation string `json:"installation"` | ||
| Namespace string `json:"namespace"` | ||
| RunID string `json:"run_id"` | ||
| } | ||
| if err := unmarshalArgs(req, &args); err != nil { | ||
| return toolErr(err), nil | ||
| } | ||
|
|
||
| opts := &porter.OutputListOptions{} | ||
| opts.Name = args.Installation | ||
| opts.Namespace = args.Namespace | ||
| opts.RunID = args.RunID | ||
|
|
There was a problem hiding this comment.
list_outputs requires "installation" in the schema, but the handler doesn't validate it. When installation is empty and run_id is also empty, Porter falls back to GetLastOutputs/GetLastRun using an empty installation name, which is likely an error or confusing no-op. Add an explicit check that installation is provided (unless you intend to support run_id-only here).
| func (s *MCPServer) getOutput(_ context.Context, req *sdkmcp.CallToolRequest) (*sdkmcp.CallToolResult, error) { | ||
| var args struct { | ||
| Installation string `json:"installation"` | ||
| OutputName string `json:"output_name"` | ||
| Namespace string `json:"namespace"` | ||
| } | ||
| if err := unmarshalArgs(req, &args); err != nil { | ||
| return toolErr(err), nil | ||
| } | ||
|
|
||
| // Check sensitivity via list before returning the raw value. | ||
| opts := &porter.OutputListOptions{} | ||
| opts.Name = args.Installation | ||
| opts.Namespace = args.Namespace | ||
| outputs, err := s.porter.ListBundleOutputs(s.ctx, opts) | ||
| if err != nil { | ||
| return toolErr(err), nil | ||
| } |
There was a problem hiding this comment.
get_output checks sensitivity by calling ListBundleOutputs with only installation/namespace (no run_id). That means it always checks the latest run, and also doesn't handle the case where "installation" is missing despite being required by the schema. Consider validating installation is non-empty, and (optionally) accept/propagate a run_id so sensitivity is checked against the same run whose value is being read.
| // Fetch logs. | ||
| logsOpts := &porter.LogsShowOptions{} | ||
| logsOpts.RunID = targetRun.ID | ||
| logs, _, logsErr := s.porter.GetInstallationLogs(s.ctx, logsOpts) |
There was a problem hiding this comment.
analyze_failure fetches logs via GetInstallationLogs but ignores the returned ok flag. When ok is false (no logs found) and err is nil, this currently reports no error and returns an empty logs string, unlike get_logs which returns an explicit error. Handle !ok by adding a "no logs found" error (or reuse errNoLogs) so the result is accurate.
| logs, _, logsErr := s.porter.GetInstallationLogs(s.ctx, logsOpts) | |
| logs, ok, logsErr := s.porter.GetInstallationLogs(s.ctx, logsOpts) | |
| if !ok && logsErr == nil { | |
| logsErr = errors.New("no logs found") | |
| } |
| results, err := s.porter.Installations.ListResults(s.ctx, args.RunID) | ||
| if err == nil && len(results) > 0 { | ||
| targetRun.Status = results[len(results)-1].Status | ||
| } |
There was a problem hiding this comment.
When run_id is provided, analyze_failure ignores the required "installation"/"namespace" inputs and analyzes the run solely by ID. This is inconsistent with the tool schema/description and can allow fetching logs/outputs for a run that doesn't belong to the specified installation. Either (a) make installation optional when run_id is supplied, or (b) verify the fetched run's Namespace/Installation match args before returning results.
| } | |
| } | |
| // Ensure the run belongs to the requested installation/namespace (when provided). | |
| if args.Installation != "" && args.Installation != targetRun.Name { | |
| return toolErr(fmt.Errorf("run %s does not belong to installation %q", args.RunID, args.Installation)), nil | |
| } | |
| if args.Namespace != "" && args.Namespace != targetRun.Namespace { | |
| return toolErr(fmt.Errorf("run %s does not belong to namespace %q", args.RunID, args.Namespace)), nil | |
| } |
| Configure your MCP client (e.g. Claude Desktop) with: | ||
| {"command": "porter", "args": ["mcp"]} | ||
|
|
||
|
|
There was a problem hiding this comment.
The JSON configuration example under "Configure your MCP client" isn't in a fenced code block, so it may render as normal text instead of a copy/paste snippet. Consider wrapping it in a ```json fenced block (or indenting 4 spaces) for consistent rendering with other CLI reference pages.
| run: | | ||
| go-licenses check ./... \ | ||
| --ignore github.com/xi2/xz,golang.org/x/sys/unix # https://github.com/xi2/xz/blob/master/LICENSE | ||
| --ignore github.com/xi2/xz,golang.org/x/sys/unix,github.com/segmentio/asm # https://github.com/xi2/xz/blob/master/LICENSE |
There was a problem hiding this comment.
The inline comment on the --ignore list only references the xi2/xz license link, but the ignore list now also includes github.com/segmentio/asm. Update the comment to document why segmentio/asm is ignored (or split comments per entry) so future license checks remain auditable.
| --ignore github.com/xi2/xz,golang.org/x/sys/unix,github.com/segmentio/asm # https://github.com/xi2/xz/blob/master/LICENSE | |
| --ignore github.com/xi2/xz,golang.org/x/sys/unix,github.com/segmentio/asm # github.com/xi2/xz license: https://github.com/xi2/xz/blob/master/LICENSE; github.com/segmentio/asm license (MIT): https://github.com/segmentio/asm/blob/master/LICENSE |
| opts.Name = args.Installation | ||
| opts.RunID = args.RunID | ||
| opts.Namespace = args.Namespace | ||
|
|
There was a problem hiding this comment.
get_logs accepts both installation and run_id (or neither) without validation. This diverges from porter.LogsShowOptions.Validate (pkg/porter/logs.go:24-39), and can yield confusing behavior (RunID silently wins) or an empty installation lookup. Consider enforcing: exactly one of installation/run_id must be set, otherwise return a tool error with a clear message.
| if err := opts.Validate(); err != nil { | |
| return toolErr(err), nil | |
| } |
What does this change
Adds a
porter mcpsubcommand that starts an MCP (Model Context Protocol) server over stdio, allowing AI coding assistants (Claude Code, Cursor, VS Code Copilot, etc.) to interact with Porter using natural language.Read-only tools (always available):
explain_bundle— show bundle parameters, credentials, outputs, and actionslist_installations/show_installation— browse installationslist_runs/get_logs— inspect execution history and logslist_outputs/get_output— read run outputs (sensitive values masked)list_credentials/show_credential— browse credential setslist_parameters/show_parameter— browse parameter setsanalyze_failure— one-shot failure triage: fetches the last (or a specific) failed run's logs and outputs in a single callWrite tools (opt-in via
--allow-write):install_bundle,upgrade_bundle,uninstall_bundle,invoke_bundleExample usage with Claude Code:
Configure in Claude Code:
claude mcp add porter -- porter mcpWhat issue does it fix
Closes # (no existing issue)
This capability was identified as a natural fit for Porter: LLM agents can already reason about CNAB bundles conceptually, and exposing Porter's CLI operations as MCP tools lets them act on real installations with full context (logs, outputs, failure analysis) rather than just generating shell commands.
Notes for the reviewer
github.com/modelcontextprotocol/go-sdk(the official Go SDK, v1.4.1).exec.CommandContext. The MCP SDK cancels per-request contexts after each tool call, which would kill plugin subprocesses between calls. This is fixed by storing the server-lifetime context inMCPServer.ctxand using it for all Porter API calls.porter.Outdefaults toos.Stdout, which would corrupt the stdio JSON-RPC stream. It is redirected toos.StderrinNewMCPServer, with acaptureOutputhelper that temporarily swaps to a buffer when write-tool output should be returned to the client.Checklist