Integrating SAM with VS Code and GitHub Copilot
You can connect your sam-node to VS Code as an MCP server, so GitHub Copilot’s
agent mode can discover and call tools from anywhere on the SAM mesh.
Overview
sam-node serves a standard Model Context Protocol endpoint over Streamable
HTTP. VS Code is a generic MCP client, so once the server is registered its
tools — get_mesh_info, list_local_services, discover_remote_services,
find_remote_tools, describe_remote_tool and call_remote_tool — appear
alongside Copilot’s own.
Worth being clear about what this arrangement is. Copilot connects as an
ordinary MCP client over the node’s API, not as a sandboxed agent behind a
sam-box. It gets the node’s tools; it does not get a per-agent policy, an
egress allowance or a boundary. That is the right shape for a coding assistant
on your own machine, and the wrong one for untrusted code — for that, see
Running agents on SAM.
Prerequisites
- A running
sam-nodeserving its API (defaulthttp://localhost:8080). See the Quick Start. - The node’s API token — the
SAM_API_TOKENyou started it with, or the contents of--api-token-path. - VS Code with GitHub Copilot, in agent mode.
Configuration
Create .vscode/mcp.json in your workspace:
{
"inputs": [
{
"type": "promptString",
"id": "sam-api-token",
"description": "SAM node API token",
"password": true
}
],
"servers": {
"sam-mesh": {
"type": "http",
"url": "http://localhost:8080/mcp",
"headers": {
"X-Sam-Authentication": "Bearer ${input:sam-api-token}"
}
}
}
}
The token is deliberately not in the file. promptString with
"password": true makes VS Code ask for it once and keep it in its own secret
store, which means the file is safe to commit and safe to share — useful,
because it is also the shortest description of how to attach an agent to a node.
For every workspace rather than one, put the same JSON in your user-level
mcp.json instead: run MCP: Open User Configuration from the command
palette.
type must be http. Without it the entry is read as a stdio server and will
not load.
Start the server
Creating the file is not enough, and this is the step that most often looks like a broken node. VS Code does not automatically run a server it has just been told about: it waits to be asked.
Open .vscode/mcp.json and click the Start action VS Code
shows directly above the "sam-mesh" entry. It will prompt for the token the
first time.
If nothing seems to happen, run MCP: List Servers from the command palette. That distinguishes the two failures cleanly:
sam-meshis missing entirely — the file is not being read. Check it is at.vscode/mcp.jsonin the workspace root and is valid JSON.sam-meshis listed but stopped — it was read and is simply waiting. Start it.
A useful property of the “waiting” case is that it leaves no trace: if the server was never started, the MCP log has no entry for it at all, successful or failed. An empty log is evidence of a server that was never asked to run, not of one that ran and failed.
Verification
Ask Copilot directly:
What does
get_mesh_infosay about this node?
A healthy node reports a router_peer_id, a non-empty connected_peers, and a
local_api_socket.
New tools do not always appear in an in-progress chat. If Copilot claims it has
no mesh tools while MCP: List Servers shows the server running, start a new
chat, and check sam-mesh is ticked in the tools picker on the chat input.
Install the SAM skill
The MCP tools let Copilot reach the mesh. The skill tells it when reaching for the mesh is the right move, and how to go about it — discover before describing, describe before calling.
sam-node skill install
This writes ~/.claude/skills/sam-mesh/SKILL.md, which VS Code also reads.
sam-node skill install --project writes it into the current repository
instead, and sam-node skill list shows whether an installed copy is current:
$ sam-node skill list
SAM skill (sam-mesh):
outdated ~/.claude/skills/sam-mesh/SKILL.md Claude
Reinstall when it says outdated, and reload the window afterwards.
Examples
Copilot decides when to reach for these. What follows is what to ask, and the real responses from a node attached to the public testnet — abridged where marked, but not otherwise tidied up.
Look at the mesh
What’s the state of the mesh from this node?
Calls get_mesh_info with {}:
{
"connected_peers": [
"12D3KooWNuHQXgEu3fbaBPKnkPCfSsEVjNvpD5Rh3hcfGeJsDzm7",
"12D3KooWAjWy4GrQDq27AjXgBbKPrNgWhmmxN221mC9oAYNRcZcs"
],
"dht_size": 3,
"local_api_socket": "/home/you/.config/sam-mesh/sam.sock",
"router_peer_id": "12D3KooWQQHDRrSfZp2S4RRTYz3pd3Fremsb5ZaP3XXg4CNZGDQj"
}
Two fields are easy to misread. connected_peers was 18 entries long in this
capture while dht_size was 3 — they count different things, so a small
dht_size next to a long peer list is not a fault. And local_api_socket is a
Unix socket that answers the same HTTP API without a token, because filesystem
permissions already decide who may open it. It is the easier way to script
against your own node.
Find out what is out there
What MCP services can this node reach?
Calls discover_remote_services with {"type":"mcp"}:
[
{"peer_id": "12D3KooWQ1hk…veSLS", "srv_name": "dummy-http",
"srv_description": "Canary HTTP tool (k8s agnhost)"},
{"peer_id": "12D3KooWFQrX…9Uwe1V", "srv_name": "everything",
"srv_description": "MCP everything test server (tools, resources, prompts)"},
{"peer_id": "12D3KooWAjWy…RcZcs", "srv_name": "everything",
"srv_description": "MCP everything test server (tools, resources, prompts)"}
]
Note everything appearing twice under different peers, and dummy-http three
times. Service names are not unique across the mesh and were never meant to be —
the peer_id is the identity. Any step that remembers “the everything service”
without remembering which peer will eventually talk to the wrong one.
Find tools, and read the failures
List the tools available on the mesh.
Calls find_remote_tools with {}. The real answer mixes successes and
failures in one array:
[
{"peer_id": "12D3KooWQ1hk…veSLS", "tool_name": "mcp://dummy-http",
"error": "failed to connect: failed to connect client: calling \"initialize\": EOF"},
{"peer_id": "12D3KooWAjWy…RcZcs", "tool_name": "mcp://everything/get-sum",
"description": "Returns the sum of two numbers"},
{"peer_id": "12D3KooWAjWy…RcZcs", "tool_name": "mcp://everything/echo",
"description": "Echoes back the input string"}
]
Discovery is best-effort per peer. Three peers advertising dummy-http were
reachable enough to be listed but failed at initialize, and that is reported
as an error field on the entry rather than failing the whole call. A partly
broken mesh returns a partly populated array, so it is worth checking whether
the tool you wanted came back with a description or an error.
Narrow the search with service_name or tool_name when you already know the
target; tool_name is served from gossip announcements and is the fastest path.
Call a tool on someone else’s machine
Use the everything service on the mesh to add 2 and 3.
describe_remote_tool first, with the peer and the namespaced name exactly as
discovery returned them:
{
"tool_name": "mcp://everything/get-sum",
"description": "Returns the sum of two numbers",
"input_schema": {
"type": "object",
"properties": {
"a": {"type": "number", "description": "First number"},
"b": {"type": "number", "description": "Second number"}
},
"required": ["a", "b"]
}
}
Then call_remote_tool with arguments matching that schema — {"a": 2, "b": 3}
— which returned:
The sum of 2 and 3 is 5.
The describe step is not ceremony. Names are namespaced scheme://service/tool,
not service.tool, and the arguments here are a and b rather than the
numbers array a guess might have produced. Both are the provider’s to change.
Use a model the mesh provides
Inference is not an MCP tool and is never called with call_remote_tool.
discover_remote_services with {"type":"inference"} inventories the providers
— on this mesh, a vLLM TPU service and an OpenRouter proxy — but to actually use
one you talk HTTP to the node’s OpenAI-compatible facade at
http://localhost:8080/v1.
Over the tokenless local socket:
$ curl -s --unix-socket ~/.config/sam-mesh/sam.sock http://localhost/v1/models
{"object":"list","data":[
{"id":"openrouter/auto","object":"model","created":0,
"owned_by":"12D3KooWHfiw…8T5YP"}
]}
owned_by is the peer serving the model. Note that two inference services were
discovered but only one model is listed: discovery inventories services, while
/v1/models lists models that answered the catalog walk. Ask for a model that
is not in this list and it resolves to no provider and comes back 404 — so read
the catalog rather than assuming a name.
A completion is the ordinary OpenAI shape, and took 3.3s here:
curl -s --unix-socket ~/.config/sam-mesh/sam.sock \
http://localhost/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"openrouter/auto",
"messages":[{"role":"user","content":"Reply with exactly: mesh inference works"}]}'
Over TCP instead of the socket, add
-H "X-Sam-Authentication: Bearer $SAM_API_TOKEN". Any OpenAI SDK works too:
point base_url at http://localhost:8080/v1.
Developing an agent against the mesh
Everything above treats Copilot as a client of your node. The other half of the loop is writing an agent that runs behind a boundary, and VS Code is a comfortable place to do it: edit the harness in the editor, run it in a container with no network at all, and watch the boundary decide what it may reach.
The example lives in development/examples/agent-harness. Build it with the
repository root as the build context, because the image compiles nano-init
from source:
docker build -t agent-harness -f development/examples/agent-harness/Dockerfile .
Start a boundary for it. --sidecar-socket is the node’s local API socket, the
same tokenless socket get_mesh_info reported, so a node you are already
running is a node you can already develop against:
sam-box run \
--socket /tmp/sam-demo/agent.sock \
--sidecar-socket ~/.config/sam-mesh/sam.sock \
--bundle development/examples/agent-harness/bundle.yaml \
--insecure-unverified-bundle \
--metrics-addr 127.0.0.1:9600
It says what that insecure flag costs, and it is right to:
WARN --insecure-unverified-bundle: this bundle is taken at its word, so
whoever can write it decides which agent this sandbox is
INFO Serving agent researcher-1.prod.acme.example
INFO Agents reach the mesh at http://mesh.sam.alt
That is fine on your laptop and not fine anywhere else. In production the bundle’s claim is checked against a platform-issued credential.
Watch it bootstrap itself
Now run the agent with no network:
docker run --rm \
--network none \
--cap-add NET_ADMIN \
--device /dev/net/tun \
-v /tmp/sam-demo/agent.sock:/run/agent.sock \
agent-harness "Use your mesh tools to find out how many peers this node is connected to."
--network none is the assertion, not the arrangement. The container has no
interface, no resolver, no route and no credentials; NET_ADMIN and
/dev/net/tun exist only so nano-init can build the single tun0 that leads
to the mounted socket. If this worked because the container could route
somewhere, it would prove nothing.
The real output:
mesh offered model: openrouter/auto
mesh granted 15 tools: call_remote_tool, check_connectivity, connect_peer,
describe_remote_tool, discover_remote_services, find_remote_tools,
get_mesh_info, get_network_info, get_recent_logs, get_token_info,
list_local_services, mesh_pubsub_broadcast, poll_messages, send_message,
subscribe_topic
step 1: get_mesh_info({})
This node is connected to **23 peers**.
Nothing was configured. The harness asked mesh.sam.alt for a model, asked it
for tools, was granted fifteen, chose one, called it, and answered — from inside
a container that cannot reach anything else. The catalog is answered per agent,
so the list is not a property of the node but of who is asking.
Watch policy refuse something
The bundle allows api.github.com and *.githubusercontent.com, and nothing
else. Run a probe in the same sandbox:
docker run --rm --network none --cap-add NET_ADMIN --device /dev/net/tun \
-v /tmp/sam-demo/agent.sock:/run/agent.sock \
--entrypoint nano-init agent-harness \
run /run/agent.sock python -c '
import urllib.request as u
for url in ["https://api.github.com/zen", "https://example.com"]:
try:
r = u.urlopen(url, timeout=25)
print(url, "->", r.status, r.read(60))
except Exception as e:
print(url, "->", type(e).__name__, e)
'
[TCP] dial 169.254.64.2:443: boundary refused: not allowed by policy
https://api.github.com/zen -> 200 b'Responsive is better than fast.'
https://example.com -> URLError: [Errno 104] Connection reset by peer
The agent sees a reset connection, which is all a denied agent should learn. The reason lives outside the sandbox, on the boundary, in metrics you can read while you develop:
$ curl -s http://127.0.0.1:9600/metrics | grep sam_box_flows_total
sam_box_flows_total{outcome="allowed",route="external"} 1
sam_box_flows_total{outcome="allowed",route="mesh-entrypoint"} 19
sam_box_flows_total{outcome="denied",route="unresolved"} 1
Nineteen mesh flows for one short run — model catalog, tool listing, the tool
call — one allowed external flow, one denial. Editing bundle.yaml and
restarting sam-box changes those numbers without touching the agent at all,
which is the property worth developing against: policy is not in the code.
A trap worth knowing
The harness asks the mesh for a model rather than hardcoding one, and takes the
first the catalog offers. On a mesh offering google/gemma-2-2b-it first, that
run fails:
mesh offered model: google/gemma-2-2b-it
openai.BadRequestError: Error code: 400 -
{'error': {'message': 'System role not supported', ...}}
Not a mesh fault and not a policy denial — Gemma has no system role, and the
harness sends a system prompt. Pin a model with -e SAM_MODEL=openrouter/auto
while developing, and remember that “whatever the mesh offers first” is a
different model on different days.
Troubleshooting
401 from the server. The token does not match the node’s. It is the
SAM_API_TOKEN the node was started with. VS Code caches what you typed, so
clear it with MCP: Reset Cached Tokens and reconnect.
Server unreachable. Check the node is listening:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8080/mcp
401 is a healthy answer — it means the endpoint is there and wants the token.
Connection refused means the node is not running.
Tools do not appear. Confirm the server is actually running with MCP: List Servers — see Start the server. If it is running, start a new chat: a chat already in progress may not pick up newly registered tools.
A tool is discovered but will not run. Look for an error field on the
entry find_remote_tools returned. failed to connect … calling "initialize": EOF means the peer is advertising a service whose backend is not answering —
that is the provider’s problem, not yours, and another peer offering the same
service name may well work.
No remote tools found at all. Check get_mesh_info first. A dht_size of
zero means the node has not found the mesh, and no amount of discovery will help
until it has. An empty connected_peers means it has not even reached a router.
Everything works but nothing is sandboxed. That is by design here — see the note at the top. Copilot is a client of your node, not an agent behind a boundary.