Skip to content

Add flow mcp command for Cadence MCP server#2306

Open
peterargue wants to merge 14 commits intomasterfrom
peter/mcp-server
Open

Add flow mcp command for Cadence MCP server#2306
peterargue wants to merge 14 commits intomasterfrom
peter/mcp-server

Conversation

@peterargue
Copy link
Contributor

Summary

Adds a flow mcp command that starts an MCP (Model Context Protocol) server over stdio for Cadence smart contract development. This enables AI coding tools like Claude Code, Cursor, and Claude Desktop to interact with the Cadence language server and Flow network directly.

Inspired by cadence-lang.org PR #285, but implemented natively in Go with no extra runtime dependencies — if you have flow installed, you have the MCP server.

Tools (9 total)

LSP tools (in-process cadence-tools/languageserver):

  • cadence_check — Check code for syntax/type errors
  • cadence_hover — Get type info at a position
  • cadence_definition — Find symbol definitions
  • cadence_symbols — List all symbols in code
  • cadence_completion — Get completions at a position

On-chain query tools:

  • get_contract_source — Fetch contract manifest from an address
  • get_contract_code — Fetch contract source code
  • cadence_execute_script — Execute read-only scripts

Code review:

  • cadence_code_review — Pattern-based review for common issues

Usage

claude mcp add cadence-mcp -- flow mcp

Or in Cursor / Claude Desktop settings:

{
  "mcpServers": {
    "cadence-mcp": {
      "command": "flow",
      "args": ["mcp"]
    }
  }
}

Design

  • LSP wrapper manages an in-process server.Server from cadence-tools/languageserver, using a single virtual document URI as a scratch buffer
  • Network config uses flow.json if present, otherwise falls back to default mainnet/testnet/emulator endpoints
  • Works without flow.json — useful for ad-hoc queries

Test plan

  • 21 tests passing (7 code review + 6 LSP wrapper + 5 tool handler + 3 integration)
  • Lint passes (0 issues)
  • License headers verified
  • Manual smoke test: MCP initialize responds correctly
  • Integration tests hit mainnet for contract queries and script execution

@github-actions
Copy link

github-actions bot commented Mar 26, 2026

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 5 package(s) with unknown licenses.
  • ⚠️ 1 packages with OpenSSF Scorecard issues.
See the Details below.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA 4cdf25c.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

License Issues

go.mod

PackageVersionLicenseIssue Type
github.com/bahlo/generic-list-go0.2.0NullUnknown License
github.com/buger/jsonparser1.1.2NullUnknown License
github.com/invopop/jsonschema0.13.0NullUnknown License
github.com/mailru/easyjson0.7.7NullUnknown License
github.com/mark3labs/mcp-go0.45.0NullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
gomod/github.com/bahlo/generic-list-go 0.2.0 UnknownUnknown
gomod/github.com/buger/jsonparser 1.1.2 UnknownUnknown
gomod/github.com/invopop/jsonschema 0.13.0 UnknownUnknown
gomod/github.com/mailru/easyjson 0.7.7 UnknownUnknown
gomod/github.com/mark3labs/mcp-go 0.45.0 UnknownUnknown
gomod/github.com/wk8/go-ordered-map/v2 2.1.8 🟢 3.8
Details
CheckScoreReason
Code-Review🟢 5Found 9/16 approved changesets -- score normalized to 5
Token-Permissions⚠️ -1No tokens found
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Dangerous-Workflow⚠️ -1no workflows found
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ -1no dependencies found
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing🟢 10project is fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
gomod/github.com/yosida95/uritemplate/v3 3.0.2 ⚠️ 2.6
Details
CheckScoreReason
Packaging⚠️ -1packaging workflow not detected
Binary-Artifacts🟢 10no binaries found in the repo
Code-Review🟢 4Found 9/19 approved changesets -- score normalized to 4
Dangerous-Workflow⚠️ -1no workflows found
Maintained⚠️ 00 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0
Pinned-Dependencies⚠️ -1no dependencies found
Token-Permissions⚠️ -1No tokens found
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Security-Policy⚠️ 0security policy file not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • go.mod

@github-actions
Copy link

⚠️ Security Dependency Review Failed ⚠️

This pull request introduces dependencies with security vulnerabilities of moderate severity or higher.

Vulnerable Dependencies:

📦 github.com/buger/jsonparser@1.1.1

What to do next?

  1. Review the vulnerability details in the Dependency Review Comment above, specifically the "Vulnerabilities" section
  2. Click on the links in the "Vulnerability" section to see the details of the vulnerability
  3. If multiple versions of the same package are vulnerable, please update to the common latest non-vulnerable version
  4. If you are unsure about the vulnerability, please contact the security engineer
  5. If the vulnerability cannot be avoided (can't upgrade, or need to keep), contact #security on slack to get it added to the allowlist

Security Engineering contact: #security on slack

@codecov-commenter
Copy link

codecov-commenter commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 40.20101% with 357 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/mcp/tools.go 24.90% 185 Missing and 17 partials ⚠️
internal/mcp/lsp.go 49.55% 103 Missing and 11 partials ⚠️
internal/mcp/mcp.go 18.75% 22 Missing and 4 partials ⚠️
internal/mcp/audit.go 79.71% 13 Missing and 1 partial ⚠️
cmd/flow/main.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! 👏

@@ -0,0 +1,203 @@
/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding auditing functionality is a good idea, it would be nice to have this usable outside of MCP.

Could we maybe move this into a new analyzer in the linter (https://github.com/onflow/cadence-tools/tree/master/lint), or a new tool in https://github.com/onflow/cadence-tools?

The rules should probably also not be regular expression-based, but instead be AST based. Some of these rules are also duplicates of existing linter analyzers (e.g. deprecated pre-1.0 code) and Cadence type checking diagnostics.

}
}

func TestIntegration_GetContractSource(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and for the other tests in this PR: Maybe parallelize the tests with t.Parallel()

}

// symbolKindName returns a human-readable name for a SymbolKind.
func symbolKindName(kind protocol.SymbolKind) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this to github.com/onflow/cadence-tools/languageserver/protocol and use stringer?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new flow mcp CLI subcommand that runs an MCP (Model Context Protocol) server over stdio, exposing Cadence LSP capabilities plus on-chain query and script execution tools for AI-assisted Cadence development.

Changes:

  • Add flow mcp Cobra command that starts an MCP server over stdio.
  • Implement MCP tool handlers for Cadence LSP actions, contract queries, script execution, and regex-based code review.
  • Add tests for LSP wrapper, tool handlers, code review rules, and network-backed integration flows; add mcp-go dependency.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
cmd/flow/main.go Wires the new mcp command into the root CLI.
internal/mcp/mcp.go Implements flow mcp command startup and Flow network gateway resolution.
internal/mcp/tools.go Defines MCP tool schemas and handler implementations (LSP + on-chain + review).
internal/mcp/lsp.go Adds an in-process Cadence language server wrapper and formatting helpers.
internal/mcp/audit.go Adds regex-based Cadence “code review” rule engine + formatting.
internal/mcp/*_test.go Adds unit/integration coverage for the new MCP functionality.
go.mod / go.sum Adds github.com/mark3labs/mcp-go and updates indirect deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +83
func (c *diagConn) captureDiagnostics(diags []protocol.Diagnostic) {
c.mu.Lock()
defer c.mu.Unlock()
c.diagnostics = append(c.diagnostics, diags...)
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

diagConn.captureDiagnostics appends diagnostics, which can duplicate results if the server publishes diagnostics multiple times for the same document update. Consider replacing (not appending) the stored diagnostics per publish event, and (optionally) filtering by scratchURI to avoid capturing diagnostics for unrelated documents.

Copilot uses AI. Check for mistakes.
Comment on lines +70 to +73
// Try to load flow.json for custom network configs
loader := &afero.Afero{Fs: afero.NewOsFs()}
state, _ := flowkit.Load(config.DefaultPaths(), loader)

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flowkit.Load errors are discarded here. If flow.json exists but is invalid/corrupt, MCP will silently fall back to defaults, which is hard to debug. Consider checking the returned error and printing a warning (except for config.ErrDoesNotExist).

Copilot uses AI. Check for mistakes.
Comment on lines +295 to +299
addr := flow.HexToAddress(address)
account, err := gw.GetAccount(ctx, addr)
if err != nil {
return mcplib.NewToolResultError(fmt.Sprintf("failed to get account: %v", err)), nil
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flow.HexToAddress does not return an error; invalid input can silently become flow.EmptyAddress. To avoid querying the wrong account, validate the parsed address (e.g., check addr == flow.EmptyAddress) and return a tool error for invalid address values.

Copilot uses AI. Check for mistakes.
Comment on lines +342 to +346
addr := flow.HexToAddress(address)
account, err := gw.GetAccount(ctx, addr)
if err != nil {
return mcplib.NewToolResultError(fmt.Sprintf("failed to get account: %v", err)), nil
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same address-parsing issue as above: invalid address input can silently map to flow.EmptyAddress. Add validation after flow.HexToAddress and return a clear error if the address is invalid.

Copilot uses AI. Check for mistakes.
Comment on lines +154 to +162
match := rule.pattern.FindStringSubmatch(line)
if match != nil {
findings = append(findings, Finding{
Rule: rule.id,
Severity: rule.severity,
Line: lineNum,
Message: rule.message(match),
})
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The review rules use FindStringSubmatch, so each rule can only emit at most one finding per line. This can miss multiple occurrences (e.g. multiple force-unwraps on one line). If you want complete findings, iterate matches with FindAllStringSubmatchIndex/FindAllStringSubmatch and emit one finding per match.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +176
// Check sends code to the LSP and returns any diagnostics.
func (w *LSPWrapper) Check(code, network string) ([]protocol.Diagnostic, error) {
w.mu.Lock()
defer w.mu.Unlock()

w.conn.reset()
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The network parameter is accepted by Check/Hover/Definition/Symbols/Completion but never used in this wrapper, so the MCP tool network argument is currently ignored for all LSP-backed tools. Either wire network into the cadence-tools flow integration/address resolution (if supported) or remove the network argument from the LSP wrapper/tool schemas to avoid misleading behavior.

Copilot uses AI. Check for mistakes.
net, err := resolveNetwork(state, network)
if err != nil {
return nil, err
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createGateway always uses gateway.NewGrpcGateway and ignores network.Key. Elsewhere in the CLI, networks with a configured key use gateway.NewSecureGrpcGateway (see internal/command/command.go:createGateway). For consistency and to support custom networks, mirror that logic here (or reuse the existing helper).

Suggested change
}
}
// Mirror CLI behavior: use a secure gateway when the network has a configured key.
if net.Key != "" {
return gateway.NewSecureGrpcGateway(*net, net.Key)
}

Copilot uses AI. Check for mistakes.
Comment on lines +288 to +293
network := req.GetString("network", "mainnet")

gw, err := createGateway(m.state, network)
if err != nil {
return mcplib.NewToolResultError(fmt.Sprintf("failed to create gateway: %v", err)), nil
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each tool invocation creates a new gRPC gateway (createGateway), which in a long-running MCP server can lead to repeated client/connection setup overhead. Consider caching/reusing gateways per network in mcpContext (and closing them on shutdown if the implementation requires it).

Copilot uses AI. Check for mistakes.
Comment on lines +44 to +59
// resolveCode returns Cadence source from either the "code" or "file" parameter.
// If "file" is provided, it reads the file contents. "code" takes precedence.
func resolveCode(req mcplib.CallToolRequest) (string, error) {
code := req.GetString("code", "")
if code != "" {
return code, nil
}
file := req.GetString("file", "")
if file == "" {
return "", fmt.Errorf("either 'code' or 'file' parameter is required")
}
data, err := os.ReadFile(file)
if err != nil {
return "", fmt.Errorf("reading file %q: %w", file, err)
}
return string(data), nil
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an MCP server context, accepting an arbitrary file path and reading it (os.ReadFile) allows the MCP client to request any readable file on the user's machine (e.g. SSH keys, env files). Consider restricting file to the current workspace (or requiring an explicit allowlist / --allow-file-access flag) to reduce accidental secret exfiltration via AI tools.

Copilot uses AI. Check for mistakes.
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.

4 participants