-
Notifications
You must be signed in to change notification settings - Fork 962
feat(mcp): add 'mcp list' / 'mcp call' general MCP client#1453
feat(mcp): add 'mcp list' / 'mcp call' general MCP client #1453shophypelive-cloud wants to merge 1 commit into
Conversation
Add a top-level `mcp` command group that talks to ANY MCP server over JSON-RPC 2.0 (Streamable HTTP), complementing the Lark-managed MCP path that shortcuts use internally (shortcuts/common/mcp_client.go). - `lark-cli mcp list --url <server>` -> JSON-RPC tools/list - `lark-cli mcp call --url <server> --tool <name> --args <json>` -> tools/call - repeatable `--header "Key: Value"` for arbitrary server auth; no Lark credentials are sent to the external server - user-supplied --url is SSRF-guarded via internal/validate (https-only, rejects localhost/private/link-local/CGNAT, DNS-rebind re-check at dial) - handles Streamable-HTTP SSE response framing - typed *output.ExitError throughout; raw JSON-RPC result returned under `data` so it composes with --jq Lives in cmd/ (not shortcuts/) so it may use net/http directly per the forbidigo `shortcuts-no-raw-http` policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLAassistant
commented
Jun 13, 2026
CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.
📝 WalkthroughWalkthroughThis PR introduces a new ChangesMCP JSON-RPC CLI Client
Sequence DiagramsequenceDiagram
participant User
participant CLI as mcp list/call
participant Client as jsonRPC
participant Server as HTTP Server
User->>CLI: Invoke with --url, --header, args
CLI->>Client: parseHeaders, httpClientFor
Client->>Server: POST JSON-RPC 2.0 request
Server->>Client: Response (text/event-stream or JSON)
Client->>Client: decodeJSONRPC (extract SSE if present)
Client->>Client: Unmarshal result or error
alt JSON-RPC error or HTTP error
Client->>User: ExitAPI error
else Success
Client->>User: OK with result
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
@LuuOW
LuuOW
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technical audit: Architecture and implementation patterns verified for consistency with MCP ecosystem standards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/mcp/call.go`:
- Around line 52-54: Replace the legacy output.ErrValidation return in the
json.Unmarshal failure path (the block that checks if
json.Unmarshal([]byte(raw), &arguments) returns an error) with a typed
validation error: call errs.NewValidationError(errs.SubtypeInvalidArgument,
"invalid --args (must be a JSON object): %v", err), chain .WithParam("--args")
and preserve the original error as the cause (e.g., .WithCause(err)), and return
that instead of output.ErrValidation.
In `@cmd/mcp/client_test.go`:
- Around line 17-24: Update the error-path tests to assert the typed problem
metadata and preserved causes instead of only inspecting *output.ExitError
fields: where tests currently call asExit (the helper shown) and then assert
ExitError.Code/Detail, change them to use errs.ProblemOf on the returned error
to assert category/subtype/params, and also use errors.As to check for a
*errs.ValidationError when you need to assert Param values; additionally assert
that the original cause is preserved (use errors.Unwrap or errors.As on the err
returned by the operation to verify the underlying cause). Apply these changes
at the other affected test sites (the regions noted at lines 96-103, 109-113,
119-121, 136-141) so each test validates typed metadata via errs.ProblemOf and
cause chaining rather than only the ExitError shape.
- Around line 45-161: Add unit tests in cmd/mcp/client_test.go that directly
exercise the URL validation in the httpClientFor function: create tests that
call httpClientFor with invalid/unsafe URLs (e.g., "file://", "data:", malformed
URLs) and assert it returns an error/ExitValidation, and add a test that calls
httpClientFor with an http:// URL to assert HTTPS-only enforcement (expect
error) and another that toggles any existing "allow HTTP" option/flag (or
context) to verify http:// is accepted when explicitly allowed; place these new
tests alongside the existing TestJSONRPC_* tests and use the httpClientFor
identifier so the test fails if the validation behavior changes.
- Around line 154-155: The test currently ignores the error return from
parseHeaders (used to build h) which can mask setup failures and cause
misleading jsonRPC errors; update the test to capture the error (e.g., h, err :=
parseHeaders(...)) and immediately fail the test if err != nil (use t.Fatalf or
t.Fatal/require for the test framework in use) before calling jsonRPC so that
parseHeaders failures are reported directly.
In `@cmd/mcp/client.go`:
- Line 33: The command code returns legacy output.ErrValidation for bad header
parsing; replace that with a typed errs.* error, e.g.
errs.InvalidUserInput("invalid --header %q (expected \"Key: Value\")",
item).WithParam("--header") and, if this is wrapping a lower-level error, chain
the cause via .WithCause(err); update every other legacy output.* site in
cmd/mcp/client.go (the return at the header parse, and the other occurrences
flagged in the review) to use the appropriate errs.* constructor, include
WithParam("--flag") for user-argument errors and .WithCause(...) when there is
an underlying error so typed metadata and cause propagation are preserved.
- Around line 112-121: The SSE parsing loop in cmd/mcp/client.go currently
overwrites the variable last for each "data:" line, dropping earlier lines;
change the loop that inspects bytes.Split(trimmed, []byte("\n")) to accumulate
each bytes.TrimSpace(rest) into a slice or bytes.Buffer and then set trimmed to
the concatenation of those lines joined with a single '\n' (SSE semantics)
instead of only the last line; keep the existing use of bytes.CutPrefix and
trimming but replace the single-variable last with an append-and-join approach
so multiline `data:` payloads are preserved.
- Around line 101-105: The code currently returns payload["result"] even when
the "result" key is missing; change the flow to treat a response missing both
"error" and "result" as malformed. Specifically, after checking if errObj, ok :=
payload["error"]; ok { return nil, mcpErrorFrom(errObj) }, add an explicit check
for the presence of "result" (e.g. if res, ok := payload["result"]; ok { return
res, nil }) and otherwise return nil and an error (e.g. fmt.Errorf or a suitable
MCP error) indicating "malformed JSON-RPC response: missing result and error".
Keep references to payload, "error", "result", and mcpErrorFrom when
implementing the fix.
In `@cmd/mcp/list.go`:
- Around line 39-50: runList currently returns lower-layer errors directly but
must convert legacy output.Err*/output.Errorf errors into the new typed errs.*
before returning while leaving already-typed errs.* untouched; update runList
(and its error returns from parseHeaders, httpClientFor, jsonRPC) to detect
legacy output errors and wrap/translate them into the appropriate errs.* variant
(preserving message and metadata) and pass through any errs.* errors unchanged;
use a small converter helper (e.g., translateLegacyError(err) used inside
runList) to centralize the mapping logic so future calls
(parseHeaders/httpClientFor/jsonRPC) get normalized typed errors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
i️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 657f7227-060e-455e-ae82-834bde4c512c
📒 Files selected for processing (6)
cmd/build.gocmd/mcp/call.gocmd/mcp/client.gocmd/mcp/client_test.gocmd/mcp/list.gocmd/mcp/mcp.go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use typed validation error for --args parsing failures.
Line 53 returns legacy output.ErrValidation, but --args is a user flag validation failure and should return a typed invalid-argument error with param="--args" and preserved cause.
Suggested fix
- if err := json.Unmarshal([]byte(raw), &arguments); err != nil { - return output.ErrValidation("invalid --args (must be a JSON object): %v", err) - } + if err := json.Unmarshal([]byte(raw), &arguments); err != nil { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --args (must be a JSON object)"). + WithParam("--args"). + WithCause(err) + }
As per coding guidelines, cmd/**/*.go must use errs.NewValidationError(errs.SubtypeInvalidArgument, ...) with .WithParam("--flag") for user argument validation failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/call.go` around lines 52 - 54, Replace the legacy
output.ErrValidation return in the json.Unmarshal failure path (the block that
checks if json.Unmarshal([]byte(raw), &arguments) returns an error) with a typed
validation error: call errs.NewValidationError(errs.SubtypeInvalidArgument,
"invalid --args (must be a JSON object): %v", err), chain .WithParam("--args")
and preserve the original error as the cause (e.g., .WithCause(err)), and return
that instead of output.ErrValidation.
Source: Coding guidelines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update error-path tests to assert typed metadata (errs.ProblemOf) and preserved causes.
Current assertions focus on *output.ExitError code/detail shape, which won’t catch regressions in typed category/subtype/param or cause chaining required by the test contract.
As per coding guidelines, *_test.go error-path tests must assert typed metadata using errs.ProblemOf and cause preservation; based on learnings, use errors.As(*errs.ValidationError) when asserting Param because ProblemOf does not expose it.
Also applies to: 96-103, 109-113, 119-121, 136-141
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client_test.go` around lines 17 - 24, Update the error-path tests to
assert the typed problem metadata and preserved causes instead of only
inspecting *output.ExitError fields: where tests currently call asExit (the
helper shown) and then assert ExitError.Code/Detail, change them to use
errs.ProblemOf on the returned error to assert category/subtype/params, and also
use errors.As to check for a *errs.ValidationError when you need to assert Param
values; additionally assert that the original cause is preserved (use
errors.Unwrap or errors.As on the err returned by the operation to verify the
underlying cause). Apply these changes at the other affected test sites (the
regions noted at lines 96-103, 109-113, 119-121, 136-141) so each test validates
typed metadata via errs.ProblemOf and cause chaining rather than only the
ExitError shape.
Sources: Coding guidelines, Learnings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add tests for httpClientFor URL validation and HTTPS-only enforcement.
cmd/mcp/client.go adds security-critical URL validation/client wrapping, but this file currently exercises only jsonRPC/parseHeaders. Please add direct tests for rejecting invalid/unsafe URLs and enforcing HTTPS-only behavior.
As per coding guidelines, every behavior change should be covered by tests alongside the change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client_test.go` around lines 45 - 161, Add unit tests in
cmd/mcp/client_test.go that directly exercise the URL validation in the
httpClientFor function: create tests that call httpClientFor with invalid/unsafe
URLs (e.g., "file://", "data:", malformed URLs) and assert it returns an
error/ExitValidation, and add a test that calls httpClientFor with an http://
URL to assert HTTPS-only enforcement (expect error) and another that toggles any
existing "allow HTTP" option/flag (or context) to verify http:// is accepted
when explicitly allowed; place these new tests alongside the existing
TestJSONRPC_* tests and use the httpClientFor identifier so the test fails if
the validation behavior changes.
Source: Coding guidelines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do not ignore parseHeaders error in test setup.
Line 154 drops the returned error; if parsing fails, this test can produce misleading failures later in jsonRPC.
Suggested fix
- h, _ := parseHeaders([]string{"Authorization: Bearer xyz"}) + h, err := parseHeaders([]string{"Authorization: Bearer xyz"}) + if err != nil { + t.Fatalf("parseHeaders: %v", err) + } - if _, err := jsonRPC(context.Background(), srv.Client(), srv.URL, "tools/list", map[string]any{}, h); err != nil { + if _, err := jsonRPC(context.Background(), srv.Client(), srv.URL, "tools/list", map[string]any{}, h); err != nil { t.Fatalf("jsonRPC: %v", err) }
📝 Committable suggestion
!!️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client_test.go` around lines 154 - 155, The test currently ignores
the error return from parseHeaders (used to build h) which can mask setup
failures and cause misleading jsonRPC errors; update the test to capture the
error (e.g., h, err := parseHeaders(...)) and immediately fail the test if err
!= nil (use t.Fatalf or t.Fatal/require for the test framework in use) before
calling jsonRPC so that parseHeaders failures are reported directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace legacy output.Err*/output.Errorf with typed errs.* in command-facing paths.
Line 33, Line 37, Line 49, Line 53, and the downstream error sites still emit legacy output.* errors, which downgrades classification and loses typed metadata/cause propagation expected in cmd/**/*.go.
Suggested direction
- return nil, output.ErrValidation("invalid --header %q (expected \"Key: Value\")", item) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid --header format").WithParam("--header") - return nil, output.ErrValidation("invalid --url: %v", err) + return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "invalid MCP server URL").WithParam("--url").WithCause(err) - return nil, output.ErrNetwork("MCP transport failed: %v", err) + return nil, errs.NewNetworkError(errs.SubtypeNetworkTransport, "MCP transport failed").WithCause(err)
As per coding guidelines, command-facing failures in **/*.go/cmd/**/*.go must use typed errs.*, preserve causes via .WithCause(err), and use WithParam("--flag") for user argument failures.
Also applies to: 37-37, 49-49, 53-53, 68-69, 73-74, 85-86, 91-95, 124-125, 136-142
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client.go` at line 33, The command code returns legacy
output.ErrValidation for bad header parsing; replace that with a typed errs.*
error, e.g. errs.InvalidUserInput("invalid --header %q (expected \"Key:
Value\")", item).WithParam("--header") and, if this is wrapping a lower-level
error, chain the cause via .WithCause(err); update every other legacy output.*
site in cmd/mcp/client.go (the return at the header parse, and the other
occurrences flagged in the review) to use the appropriate errs.* constructor,
include WithParam("--flag") for user-argument errors and .WithCause(...) when
there is an underlying error so typed metadata and cause propagation are
preserved.
Source: Coding guidelines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Treat missing result+error as malformed JSON-RPC response, not success.
At Line 104, payload["result"] is returned even when the key is absent, which can surface malformed responses as ok with data: null.
Suggested fix
if errObj, ok := payload["error"]; ok {
return nil, mcpErrorFrom(errObj)
}
- return payload["result"], nil
+ result, ok := payload["result"]
+ if !ok {
+ return nil, output.Errorf(output.ExitAPI, "mcp_error", "MCP response missing both result and error")
+ }
+ return result, nil📝 Committable suggestion
!!️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client.go` around lines 101 - 105, The code currently returns
payload["result"] even when the "result" key is missing; change the flow to
treat a response missing both "error" and "result" as malformed. Specifically,
after checking if errObj, ok := payload["error"]; ok { return nil,
mcpErrorFrom(errObj) }, add an explicit check for the presence of "result" (e.g.
if res, ok := payload["result"]; ok { return res, nil }) and otherwise return
nil and an error (e.g. fmt.Errorf or a suitable MCP error) indicating "malformed
JSON-RPC response: missing result and error". Keep references to payload,
"error", "result", and mcpErrorFrom when implementing the fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SSE parsing drops multiline data: payloads by keeping only the last line.
Line 117 overwrites last for each data: line, but SSE events can contain multiple data: lines that must be concatenated. Valid framed JSON can fail to parse.
Suggested fix
- var last []byte + var dataLines [][]byte for _, line := range bytes.Split(trimmed, []byte("\n")) { line = bytes.TrimSpace(line) if rest, ok := bytes.CutPrefix(line, []byte("data:")); ok { - last = bytes.TrimSpace(rest) + dataLines = append(dataLines, bytes.TrimSpace(rest)) } } - trimmed = last + trimmed = bytes.Join(dataLines, []byte("\n"))
📝 Committable suggestion
!!️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/client.go` around lines 112 - 121, The SSE parsing loop in
cmd/mcp/client.go currently overwrites the variable last for each "data:" line,
dropping earlier lines; change the loop that inspects bytes.Split(trimmed,
[]byte("\n")) to accumulate each bytes.TrimSpace(rest) into a slice or
bytes.Buffer and then set trimmed to the concatenation of those lines joined
with a single '\n' (SSE semantics) instead of only the last line; keep the
existing use of bytes.CutPrefix and trimming but replace the single-variable
last with an append-and-join approach so multiline `data:` payloads are
preserved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Migrate MCP helper error returns to typed errs.* before passthrough.
runList correctly passes lower-layer errors through, but the current lower layer (cmd/mcp/client.go) returns legacy output.Err*/output.Errorf, so command-facing failures are not emitted as typed errors. This breaks the migrated cmd/**/*.go error contract for structured category/subtype/param metadata.
As per coding guidelines, cmd/**/*.go must use typed errs.* errors and preserve typed lower-layer errors unchanged; cmd/mcp/client.go currently emits legacy output.Err* values.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/mcp/list.go` around lines 39 - 50, runList currently returns lower-layer
errors directly but must convert legacy output.Err*/output.Errorf errors into
the new typed errs.* before returning while leaving already-typed errs.*
untouched; update runList (and its error returns from parseHeaders,
httpClientFor, jsonRPC) to detect legacy output errors and wrap/translate them
into the appropriate errs.* variant (preserving message and metadata) and pass
through any errs.* errors unchanged; use a small converter helper (e.g.,
translateLegacyError(err) used inside runList) to centralize the mapping logic
so future calls (parseHeaders/httpClientFor/jsonRPC) get normalized typed
errors.
Source: Coding guidelines
Uh oh!
There was an error while loading. Please reload this page.
Summary
Add a general MCP client to lark-cli —
mcp listandmcp calltalk to any Model Context Protocol server over JSON-RPC 2.0 (Streamable HTTP). This complements the existing Lark-managed MCP path used internally by shortcuts (shortcuts/common/mcp_client.go), which is hardwired to Lark's endpoint and proprietaryX-Lark-MCP-*headers and cannot reach a third-party server.Changes
cmd/mcpcommand group:lark-cli mcp list --url <server>→ JSON-RPCtools/listlark-cli mcp call --url <server> --tool <name> --args '<json>'→tools/call--url(required),--tool(required forcall),--args(JSON object, default{}), repeatable--header "Key: Value"for arbitrary server auth. No Lark credentials are sent to the external server.--urlis SSRF-guarded viainternal/validate— https-only, rejectslocalhost/private/link-local/CGNAT hosts, and re-checks the resolved IP at dial time to defeat DNS rebinding.resultunderdataso it composes with--jq.*output.ExitErrorthroughout (ErrValidation/ErrNetwork/Errorf(ExitAPI, ...)); no barefmt.Errorf.cmd/(notshortcuts/) so it may usenet/httpdirectly, per theshortcuts-no-raw-httpforbidigo policy. Registered incmd/build.go.No new dependencies (reuses
github.com/google/uuid,cobra, andinternal/*).Test Plan
go test -race ./cmd/mcp/...):httptest-based table tests coveringtools/list,tools/call, SSE framing, JSON-RPC error payload, HTTP ≥400, non-JSON body, and--headerparse + on-the-wire delivery.mcp --help,mcp list/call --help, SSRF rejection ofhttp://localhost:...,--argsJSON validation — all return the correct JSON error envelope.gofmt -lclean ·go vetclean ·golangci-lint@v2.1.6 run --new-from-rev=origin/main→0 issues·go mod tidyno-op.Related Issues
Summary by CodeRabbit
mcpcommand group for interacting with MCP serversmcp listsubcommand lists available tools from a target MCP servermcp callsubcommand invokes specific tools on a target MCP server