Skip to content

Commit 4c9c4c2

Browse files
authored
feat: forc mcp server - with forc-call tool integration (#7284)
## Description Introducing Forc-MCP module (CLI). The MCP server can be run in 3 modes: - `stdio` - `sse`: long-running server - using http server-side-events - `http`: long-running server - using http streams ### Forc-call integration The first tool to be integrated is `forc-call` - which exposes the following tool calls: - `list_contract_functions` - `transfer_assets` - `get_execution_trace` - `call_contract` And the following resources: - `MCP Type Encoding Reference` - `MCP Tool Usage Examples` - `Contract Examples with MCP Tools` ### Screenshots from `@modelcontextprotocol/inspector` <img width="2543" height="549" alt="Screenshot 2025-07-15 at 5 09 16 PM" src="https://github.com/user-attachments/assets/aaab5174-d5e6-4685-be71-fd17cd4f5d0f" /> <img width="2540" height="531" alt="Screenshot 2025-07-15 at 5 09 30 PM" src="https://github.com/user-attachments/assets/9ab39d5a-950c-4404-a3a3-6e34b89f58e7" /> ### Example usage (claude code) Pre-requisites: - Run local fuel-core node ``` fuel-core run --ip 127.0.0.1 --port 4000 --db-type rocks-db --debug --historical-execution ``` - Deploy a contract to local node: `forc deploy --default-signer` 1. Run the MCP server (using http-steam in this example): ``` cargo run -p forc-mcp -- http ``` 2. Add mcp server to claude ``` claude mcp add --transport http forc-mcp-http http://localhost:3001/mcp ``` 3. Use claude/LLM to interact with MCP server <details> <summary>Claude output</summary> ``` ╭───────────────────────────────────────────────────╮ │ ✻ Welcome to Claude Code! │ │ │ │ /help for help, /status for your current setup │ │ │ │ cwd: /Users/z/Desktop/fuellabs/sway │ ╰───────────────────────────────────────────────────╯ > Use the forc-mcp-http. Given the following sway contract: demo/demo/src/main.sw deployed at the following address: ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a903464c441eb2 Using the forc-mcp-http: > list all its callable functions. > can you get the current count? > tell me how much gas it would cost to increment the count. > show me the execution trace for incrementing the count. ⎿ ⧉ Selected 14 lines from scratch/ai/templates.md in Cursor ⏺ I'll help you interact with the deployed Sway contract using forc-mcp-http. Let me start by reading the contract to understand its structure, then perform the requested operations. ⏺ Read(demo/demo/src/main.sw) ⎿ Read 34 lines (ctrl+r to expand) ⏺ Now I need to find the ABI file for this contract to use with the forc-mcp-http tools: ⏺ Search(pattern: "demo/demo/out/**/*.json") ⎿ Found 7 files (ctrl+r to expand) ⏺ Perfect! I found the ABI file and a deployment file that matches the contract address. Let me read the ABI: ⏺ Read(demo/demo/out/release/demo-abi.json) ⎿ Read 89 lines (ctrl+r to expand) ⏺ Now let me use the forc-mcp-http tools to interact with the contract. First, I'll list all callable functions: ⏺ forc-mcp-http:list_contract_functions (MCP)(abi: "/demo/demo/out/rele ase/demo-abi.json", contract_id: "ae40e8dbe366c5ac61fe55 8e4e9139300534cd5d9463522621a903464c441eb2") ⎿ Callable functions for contract: ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a903464c441eb2 get_count() -> u64 … +186 lines (ctrl+r to expand) ⏺ Now let me get the current count: ⏺ forc-mcp-http:call_contract (MCP)(abi: "/demo/demo/out/release/demo-a bi.json", contract_id: "ae40e8dbe366c5ac61fe558e4e9139300534cd5d94 63522621a903464c441eb2", function: "get_count") ⎿ { "tx_hash": "0f1e868e75530f33d37748cac9ae25c33bf166c9e02d28e36f3ffdd67ecf3584", "total_gas": 65007, … +354 lines (ctrl+r to expand) ⏺ Now let me check the gas cost for incrementing the count using dry-run mode: ⏺ forc-mcp-http:call_contract (MCP)(abi: "/demo/demo/out/release/demo-a bi.json", contract_id: "ae40e8dbe366c5ac61fe558e4e9139300534cd5d94 63522621a903464c441eb2", function: "increment_count", function_args: ["(1, 2)"], mode: "dry-run") ⎿ Error: Contract call failed: input must start with '{' and end with '}': (1, 2) ⏺ Let me fix the format for the struct argument: ⏺ forc-mcp-http:call_contract (MCP)(abi: "/demo/demo/out/release/demo-a bi.json", contract_id: "ae40e8dbe366c5ac61fe558e4e9139300534cd5d94 63522621a903464c441eb2", function: "increment_count", function_args: ["{\"vals\": [1, 2]}"], mode: "dry-run") ⎿ Error: Contract call failed: input must start with '(' and end with ')': "vals": [1, 2] ⏺ Let me try the tuple format as shown in the example: ⏺ forc-mcp-http:call_contract (MCP)(abi: "/demo/demo/out/release/demo-a bi.json", contract_id: "ae40e8dbe366c5ac61fe558e4e9139300534cd5d94 63522621a903464c441eb2", function: "increment_count", function_args: ["((1, 2))"], mode: "dry-run") ⎿ Error: Contract call failed: input must start with '{' and end with '}': ((1, 2)) ⏺ Let me try with the correct struct format: ⏺ forc-mcp-http:call_contract (MCP)(abi: "/demo/demo/out/release/demo-a bi.json", contract_id: "ae40e8dbe366c5ac61fe558e4e9139300534cd5d94 63522621a903464c441eb2", function: "increment_count", function_args: ["{(1, 2)}"], mode: "dry-run") ⎿ { "tx_hash": "f694c73230e893b00968d8106f5bafbcd585c2cf0f4aea1c6a562ce7e1868690", "total_gas": 78940, … +399 lines (ctrl+r to expand) ⏺ Perfect! Now let me get the execution trace for that increment operation: ⏺ forc-mcp-http:get_execution_trace (MCP)(total_gas: 78940, trace_events: [{"Call":{"index":0,"method ":"increment_count","arguments":["Adder { vals: (1, 2) }"],"to":"ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621 a903464c441eb2","amount":0,"gas":26222}},{"LogData":{"index" :2,"id":"ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a 903464c441eb2","value":"3","len":8}},{"ReturnData":{"index": 4,"id":"ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a9 03464c441eb2","data":"()"}},{"Return":{"index":6,"id":"00000 00000000000000000000000000000000000000000000000000000000000" ,"val":1}},{"ScriptResult":{"index":7,"result":"Success","ga s_used":22210}}]) ⎿ Traces: [Script] ├─ [26222] 0xae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a903464c441eb2:: … +8 lines (ctrl+r to expand) ⏺ Summary Here are the results for the deployed Sway contract at ae40e8dbe366c5ac61fe558e4e9139300534cd5d9463522621a903464c441eb2: Callable Functions: 1. get_count() → returns u64 (read-only) 3. increment_count(adder: Adder) → returns () (read/write storage) Current Count: The current count is 1 Gas Cost for Increment: Incrementing the count with Adder { vals: (1, 2) } costs 78,940 gas total (22,210 gas used by script) Execution Trace: The increment operation: - Calls increment_count with Adder { vals: (1, 2) } - Uses 26,222 gas for the contract call - Emits a log with value 3 (0 + 1 + 2 = 3, the new count) - Returns () (unit/void) - Total script gas used: 22,210 - Result: Success The contract adds the two values from the Adder struct (1 + 2 = 3) to the current storage count (0) and logs the result. ``` </details> ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. --------- Co-authored-by: z <zees-dev@users.noreply.github.com>
1 parent 22d8586 commit 4c9c4c2

File tree

22 files changed

+3620
-24
lines changed

22 files changed

+3620
-24
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ jobs:
4949
- cargo-run-e2e-test-release
5050
- cargo-test-forc-debug
5151
- cargo-test-forc-client
52+
- cargo-test-forc-mcp
5253
- cargo-test-forc-node
5354
- notify-slack-on-failure
5455
runs-on: ubuntu-latest
@@ -667,6 +668,27 @@ jobs:
667668
mv fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core
668669
- name: Run tests
669670
run: cargo test --locked --release -p forc-client -- --test-threads 1
671+
cargo-test-forc-mcp:
672+
runs-on: buildjet-4vcpu-ubuntu-2204
673+
needs: get-fuel-core-version
674+
steps:
675+
- uses: actions/checkout@v3
676+
- name: Install toolchain
677+
uses: dtolnay/rust-toolchain@master
678+
with:
679+
toolchain: ${{ env.RUST_VERSION }}
680+
targets: "x86_64-unknown-linux-gnu, wasm32-unknown-unknown"
681+
- uses: Swatinem/rust-cache@v2
682+
with:
683+
cache-provider: "buildjet"
684+
- name: Install fuel-core for tests
685+
run: |
686+
curl -sSLf https://github.com/FuelLabs/fuel-core/releases/download/v${{ needs.get-fuel-core-version.outputs.fuel_core_version }}/fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu.tar.gz -L -o fuel-core.tar.gz
687+
tar -xvf fuel-core.tar.gz
688+
chmod +x fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core
689+
mv fuel-core-${{ needs.get-fuel-core-version.outputs.fuel_core_version }}-x86_64-unknown-linux-gnu/fuel-core /usr/local/bin/fuel-core
690+
- name: Run tests
691+
run: cargo test --locked --release -p forc-mcp -- --test-threads 1
670692
cargo-test-forc-node:
671693
runs-on: buildjet-4vcpu-ubuntu-2204
672694
needs: get-fuel-core-version
@@ -729,7 +751,7 @@ jobs:
729751
with:
730752
cache-provider: "buildjet"
731753
- name: Run tests
732-
run: cargo test --locked --release --workspace --exclude forc-debug --exclude sway-lsp --exclude forc-client --exclude forc --exclude forc-node
754+
run: cargo test --locked --release --workspace --exclude forc-debug --exclude sway-lsp --exclude forc-client --exclude forc-mcp --exclude forc --exclude forc-node
733755
cargo-unused-deps-check:
734756
runs-on: buildjet-4vcpu-ubuntu-2204
735757
steps:
@@ -948,13 +970,13 @@ jobs:
948970
- name: Strip release binaries x86_64-linux-gnu
949971
if: matrix.job.target == 'x86_64-unknown-linux-gnu'
950972
run: |
951-
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-migrate forc-node forc-publish forc-call; do
973+
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-mcp forc-migrate forc-node forc-publish forc-call; do
952974
strip "target/${{ matrix.job.target }}/release/$BINARY"
953975
done
954976
- name: Strip release binaries aarch64-linux-gnu
955977
if: matrix.job.target == 'aarch64-unknown-linux-gnu'
956978
run: |
957-
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-migrate forc-node forc-publish forc-call; do
979+
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-mcp forc-migrate forc-node forc-publish forc-call; do
958980
docker run --rm -v \
959981
"$PWD/target:/target:Z" \
960982
ghcr.io/cross-rs/${{ matrix.job.target }}:main \
@@ -964,7 +986,7 @@ jobs:
964986
- name: Strip release binaries mac
965987
if: matrix.job.os == 'macos-latest'
966988
run: |
967-
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-migrate forc-node forc-publish forc-call; do
989+
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-mcp forc-migrate forc-node forc-publish forc-call; do
968990
strip -x "target/${{ matrix.job.target }}/release/$BINARY"
969991
done
970992
@@ -978,7 +1000,7 @@ jobs:
9781000
ZIP_FILE_NAME=forc-binaries-${{ env.PLATFORM_NAME }}_${{ env.ARCH }}.tar.gz
9791001
echo "ZIP_FILE_NAME=$ZIP_FILE_NAME" >> $GITHUB_ENV
9801002
mkdir -pv ./forc-binaries
981-
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-migrate forc-node forc-publish forc-call; do
1003+
for BINARY in forc forc-fmt forc-lsp forc-debug forc-deploy forc-run forc-doc forc-crypto forc-tx forc-submit forc-mcp forc-migrate forc-node forc-publish forc-call; do
9821004
cp "target/${{ matrix.job.target }}/release/$BINARY" ./forc-binaries
9831005
done
9841006
tar -czvf $ZIP_FILE_NAME ./forc-binaries

.github/workflows/gh-pages.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
cargo install --locked --debug --path ./forc-plugins/forc-fmt
3232
cargo install --locked --debug --path ./forc-plugins/forc-doc
3333
cargo install --locked --debug --path ./forc-plugins/forc-lsp
34+
cargo install --locked --debug --path ./forc-plugins/forc-mcp
3435
cargo install --locked --debug --path ./forc-plugins/forc-migrate
3536
cargo install --locked --debug --path ./forc-plugins/forc-node
3637
cargo install --locked --debug --path ./forc-plugins/forc-publish

Cargo.lock

Lines changed: 188 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ forc-debug = { path = "forc-plugins/forc-debug/", version = "0.69.1" }
5050
forc-doc = { path = "forc-plugins/forc-doc/", version = "0.69.1" }
5151
forc-fmt = { path = "forc-plugins/forc-fmt/", version = "0.69.1" }
5252
forc-lsp = { path = "forc-plugins/forc-lsp/", version = "0.69.1" }
53+
forc-mcp = { path = "forc-plugins/forc-mcp/", version = "0.69.1" }
5354
forc-migrate = { path = "forc-plugins/forc-migrate/", version = "0.69.1" }
5455
forc-publish = { path = "forc-plugins/forc-publish/", version = "0.69.1" }
5556
forc-tx = { path = "forc-plugins/forc-tx/", version = "0.69.1" }
@@ -112,6 +113,7 @@ assert_matches = "1.5"
112113
async-trait = "0.1"
113114
aws-config = "1.5"
114115
aws-sdk-kms = "1.44"
116+
axum = "0.8"
115117
byte-unit = "5.1"
116118
bytes = "1.7"
117119
chrono = { version = "0.4", default-features = false }
@@ -197,6 +199,7 @@ regex = "1.10"
197199
reqwest = "0.12"
198200
rexpect = "0.6"
199201
revm = "14.0"
202+
rmcp = "0.2"
200203
ropey = "1.5"
201204
rpassword = "7.2"
202205
rustc-hash = "1.1"

docs/book/spell-check-custom-words.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,14 @@ invariants
253253
postfix
254254
impl
255255
Impl
256+
mcp
257+
MCP
258+
extensibility
259+
Extensibility
260+
integrations
261+
streamable
262+
Streamable
263+
schemas
264+
CallResponse
265+
md
266+
URIs

docs/book/src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@
109109
- [forc doc](./forc/plugins/forc_doc.md)
110110
- [forc fmt](./forc/plugins/forc_fmt.md)
111111
- [forc lsp](./forc/plugins/forc_lsp.md)
112+
- [forc mcp](./forc/plugins/forc_mcp/index.md)
113+
- [forc-call tool](./forc/plugins/forc_mcp/forc_call_tool/index.md)
112114
- [forc migrate](./forc/plugins/forc_migrate.md)
113115
- [forc node](./forc/plugins/forc_node.md)
114116
- [forc publish](./forc/plugins/forc_publish.md)

0 commit comments

Comments
 (0)