Native SDKs
An agent written in JavaScript or Python can be a member of the mesh itself,
with no sam-node beside it. The SDK enrolls with the control plane, joins
through a router, finds services, calls MCP tools and inference or A2A
endpoints, and publishes services of its own. Every caller of those services
is checked against the mesh policy before anything reaches your code.
This guide takes you from nothing to two programs on a mesh: one that
publishes a tool and an agent card, and one that finds them and calls them.
Both programs are in the repository under
sdk/js/examples
and
sdk/python/examples,
and the repository’s tests run them against a real mesh, so what you read
here is what runs.
1. Get a mesh
A program needs the URL of a control plane and a token that lets it enroll. You get them in one of two ways.
Run your own
sam-one runs a control plane, a router and a web console in one process.
The install script provides
it. Start it with a directory for its state:
sam-one --data-dir ~/sam-one
The banner it prints has the URL, and the token is written to
~/sam-one/join-token:
API URL: http://0.0.0.0:33775
Join Token: sam_tok_…
On first boot sam-one seeds an open development policy, so any enrolled
member may publish and call any service. That is right for trying the
programs below on your laptop. Before you share the mesh, replace it, as
Your own mesh
explains.
This mesh is reachable from your machine only. To let programs on other
machines join, start sam-one with --tunnel cloudflare, which publishes
the port on a temporary public https hostname and prints that URL in the
banner instead. Use that URL as the control plane URL everywhere below. Your
own mesh covers the tunnel, a real
hostname and the console.
Join a mesh someone else runs
Ask the operator for the control plane URL and a bootstrap token. Operators
mint tokens in the console or with sam-one token create; a mesh with an
identity provider lets you mint your own from the console after logging in.
Save the token in a file that only you can read. Do not put it on a command
line: it would sit in ps output and shell history.
Tell the programs about it
The programs below read the mesh from two environment variables. Set them in every terminal you use:
export SAM_CONTROL_PLANE_URL=http://127.0.0.1:33775 # the API URL from the banner
export SAM_BOOTSTRAP_TOKEN_PATH=~/sam-one/join-token
A plain http:// URL is accepted only for a control plane on the same
machine. Anything else needs https://, because the control plane is the
trust root of every member and the SDK refuses to fetch it over plaintext
from a remote address. Inside a network you already trust, such as a
Kubernetes cluster where the control plane is a cluster-local service, set
SAM_INSECURE_CONTROL_PLANE=true (allowInsecure in code), the same choice
as sam-node --insecure-control-plane.
On a platform that issues workload identity tokens, a program needs no
bootstrap token. A mesh whose control plane trusts the platform’s issuer
enrolls the program from that token instead; on Kubernetes that is a
projected service account token, as Headless enrollment
shows for sam-node. Set SAM_JWT_PATH to the token file rather than
SAM_BOOTSTRAP_TOKEN_PATH. That is how the public testnets run these same
programs as canaries beside the sam-node ones.
2. Install the SDK
npm install @sam-mesh/sdk @modelcontextprotocol/sdk zod # Node.js 22 or later
pip install sam-mesh # Python 3.11 or later
The JavaScript package depends on the MCP SDK and zod; the install line
names them so your project can import them directly. The Python package is
imported as agent_mesh. It runs on trio, because py-libp2p does; under
asyncio, use it through anyio with the trio backend.
3. Publish a service
This program publishes an MCP tool named greet under the service
mcp://greeter and an A2A endpoint under a2a://greeter, and stays on the
mesh answering callers until you stop it. With OLLAMA_URL set, it also
publishes the Ollama server running beside it as inference://ollama, so a
model on your laptop becomes a model on the mesh.
JavaScript, serve.js:
// Publishes services on the mesh and answers callers until stopped: an MCP
// tool, an A2A endpoint and, when OLLAMA_URL is set, the Ollama server running
// beside this program as an inference service. The mesh policy decides which
// members may call; the SDK turns the others away before anything reaches
// this code or Ollama.
//
// node serve.js # publishes mcp://greeter and a2a://greeter
// node serve.js greeter-2 # the same under another name
//
// SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
// SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or
// SAM_JWT_PATH (a workload identity token your platform issues, such as a
// Kubernetes projected service account token), and keeps the identity and
// credential in SAM_STATE_DIR; later runs resume from there without it.
import { homedir } from "node:os";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { AgentMesh } from "@sam-mesh/sdk";
import { z } from "zod";
const [name = "greeter"] = process.argv.slice(2);
const mesh = await AgentMesh.enroll({
controlPlaneUrl: process.env.SAM_CONTROL_PLANE_URL ?? "https://mesh.example.com",
bootstrapTokenPath: process.env.SAM_BOOTSTRAP_TOKEN_PATH,
jwtPath: process.env.SAM_JWT_PATH,
stateDir: process.env.SAM_STATE_DIR ?? `${homedir()}/.config/sam-mesh/${name}`,
// A plaintext http:// control plane is otherwise accepted only on loopback.
allowInsecure: process.env.SAM_INSECURE_CONTROL_PLANE === "true",
});
const session = await mesh.join();
await session.serve({
type: "mcp",
name,
createServer: () => {
const server = new McpServer({ name, version: "1.0.0" });
server.registerTool("greet", { description: "Greets someone by name", inputSchema: { name: z.string() } }, async ({ name: who }) => ({
content: [{ type: "text", text: `hello ${who}` }],
}));
return server;
},
});
await session.serve({
type: "a2a",
name,
target: (request, caller) => Response.json({ name, path: new URL(request.url).pathname, caller: caller.peerId }),
});
if (process.env.OLLAMA_URL !== undefined) {
await session.serve({ type: "inference", name: "ollama", target: process.env.OLLAMA_URL });
}
console.log(`serving ${session.servedServices.map((s) => `${s.type}://${s.name}`).join(", ")} as ${session.peerId}`);
const stop = () => void session.close().then(() => process.exit(0));
process.on("SIGINT", stop);
process.on("SIGTERM", stop);
Python, serve.py:
"""Publishes services on the mesh and answers callers until stopped: an MCP
tool, an A2A endpoint and, when OLLAMA_URL is set, the Ollama server running
beside this program as an inference service. The mesh policy decides which
members may call; the SDK turns the others away before anything reaches this
code or Ollama.
python serve.py # publishes mcp://greeter and a2a://greeter
python serve.py greeter-2 # the same under another name
SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
(a workload identity token your platform issues, such as a Kubernetes
projected service account token), and keeps the identity and credential in
SAM_STATE_DIR; later runs resume from there without it.
"""
import json
import os
import sys
import trio
from agent_mesh import AgentMesh, HTTPRequest, HTTPResponse, HTTPService, MCPService, VerifiedBiscuit
from mcp.server.mcpserver import MCPServer
service_name = sys.argv[1] if len(sys.argv) > 1 else "greeter"
mesh = AgentMesh.enroll(
os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
jwt_path=os.environ.get("SAM_JWT_PATH"),
state_dir=os.environ.get("SAM_STATE_DIR", f"~/.config/sam-mesh/{service_name}"),
# A plaintext http:// control plane is otherwise accepted only on loopback.
allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
)
def create_server() -> MCPServer:
server = MCPServer(service_name)
@server.tool(description="Greets someone by name")
def greet(name: str) -> str:
return f"hello {name}"
return server
async def card(request: HTTPRequest, caller: VerifiedBiscuit) -> HTTPResponse:
body = json.dumps({"name": service_name, "path": request.path, "caller": caller.peer_id})
return HTTPResponse(status=200, headers={"content-type": "application/json"}, body=body.encode())
async def main() -> None:
async with mesh.join() as session:
await session.serve(MCPService(name=service_name, create_server=create_server))
await session.serve(HTTPService(type="a2a", name=service_name, target=card))
if "OLLAMA_URL" in os.environ:
await session.serve(HTTPService(type="inference", name="ollama", target=os.environ["OLLAMA_URL"]))
served = ", ".join(f"{t}://{n}" for t, n, _ in session.services.list())
print(f"serving {served} as {session.peer_id}", flush=True)
await trio.sleep_forever()
trio.run(main)
Run one of them:
node serve.js
# or
python serve.py
serving mcp://greeter, a2a://greeter as 12D3KooWQmB5…
The program spent the token, saved its identity and credential under
~/.config/sam-mesh/greeter, joined through the router and announced its
services. Run it again and it resumes from that directory; the token is no
longer needed. The service is reachable from every member of the mesh:
another program written with an SDK, a sam-node on any machine, or a
phone.
4. Call it
This program finds a service by name and calls it: a tool of an MCP service, or a path of an inference or A2A service.
JavaScript, call.js:
// Finds a service on the mesh and calls it: a tool of an MCP service, or a
// path of an inference or A2A service.
//
// node call.js mcp://greeter greet '{"name": "Ada"}'
// node call.js a2a://greeter /card
// node call.js inference://ollama /v1/models
//
// SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
// SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or
// SAM_JWT_PATH (a workload identity token your platform issues, such as a
// Kubernetes projected service account token), and keeps the identity and
// credential in SAM_STATE_DIR; later runs resume from there without it.
import { homedir } from "node:os";
import { AgentMesh, type DiscoveredProvider } from "@sam-mesh/sdk";
const [service = "mcp://greeter", toolOrPath = "greet", args = '{"name": "world"}'] = process.argv.slice(2);
const mesh = await AgentMesh.enroll({
controlPlaneUrl: process.env.SAM_CONTROL_PLANE_URL ?? "https://mesh.example.com",
bootstrapTokenPath: process.env.SAM_BOOTSTRAP_TOKEN_PATH,
jwtPath: process.env.SAM_JWT_PATH,
stateDir: process.env.SAM_STATE_DIR ?? `${homedir()}/.config/sam-mesh/caller`,
// A plaintext http:// control plane is otherwise accepted only on loopback.
allowInsecure: process.env.SAM_INSECURE_CONTROL_PLANE === "true",
});
const session = await mesh.join();
console.log(`on the mesh as ${session.peerId}`);
const providers = await session.discover(service);
if (providers.length === 0) {
throw new Error(`no member of the mesh serves ${service}`);
}
// A provider record can outlive its member; the first that answers is used.
let provider: DiscoveredProvider | undefined;
for (const candidate of providers) {
try {
await session.connect(candidate);
provider = candidate;
break;
} catch (err) {
console.error(`${candidate.peerId}: ${(err as Error).message}`);
}
}
if (provider === undefined) {
throw new Error(`no provider of ${service} is reachable`);
}
console.log(`${service} is served by ${provider.peerId}`);
if (service.startsWith("mcp://")) {
const tools = await session.listTools(provider, service);
console.log(`tools: ${tools.map((t) => t.name).join(", ")}`);
const result = await session.callTool(provider, service, toolOrPath, JSON.parse(args));
console.log(result.text.join("\n"));
} else {
const response = await session.request(provider, service, toolOrPath);
console.log(response.status, response.text());
}
await session.close();
Python, call.py:
"""Finds a service on the mesh and calls it: a tool of an MCP service, or a
path of an inference or A2A service.
python call.py mcp://greeter greet '{"name": "Ada"}'
python call.py a2a://greeter /card
python call.py inference://ollama /v1/models
SAM_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
SAM_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or SAM_JWT_PATH
(a workload identity token your platform issues, such as a Kubernetes
projected service account token), and keeps the identity and credential in
SAM_STATE_DIR; later runs resume from there without it.
"""
import json
import os
import sys
import trio
from agent_mesh import AgentMesh
argv = sys.argv[1:]
service = argv[0] if len(argv) > 0 else "mcp://greeter"
tool_or_path = argv[1] if len(argv) > 1 else "greet"
args = json.loads(argv[2]) if len(argv) > 2 else {"name": "world"}
mesh = AgentMesh.enroll(
os.environ.get("SAM_CONTROL_PLANE_URL", "https://mesh.example.com"),
bootstrap_token_path=os.environ.get("SAM_BOOTSTRAP_TOKEN_PATH"),
jwt_path=os.environ.get("SAM_JWT_PATH"),
state_dir=os.environ.get("SAM_STATE_DIR", "~/.config/sam-mesh/caller"),
# A plaintext http:// control plane is otherwise accepted only on loopback.
allow_insecure=os.environ.get("SAM_INSECURE_CONTROL_PLANE") == "true",
)
async def main() -> None:
async with mesh.join() as session:
print(f"on the mesh as {session.peer_id}")
providers = await session.discover(service)
if not providers:
raise SystemExit(f"no member of the mesh serves {service}")
# A provider record can outlive its member; the first that answers is used.
for provider in providers:
try:
await session.connect(provider)
break
except (ConnectionError, PermissionError) as err:
print(f"{provider.peer_id}: {err}", file=sys.stderr)
else:
raise SystemExit(f"no provider of {service} is reachable")
print(f"{service} is served by {provider.peer_id}")
if service.startswith("mcp://"):
tools = await session.list_tools(provider, service)
print("tools:", ", ".join(t.name for t in tools))
result = await session.call_tool(provider, service, tool_or_path, args)
print("\n".join(result.text))
else:
response = await session.request(provider, service, tool_or_path)
print(response.status, response.text)
trio.run(main)
In a second terminal, with the same two environment variables set, call the tool and the agent card. Either language finds a service published from the other:
node call.js mcp://greeter greet '{"name": "Ada"}'
on the mesh as 12D3KooWHZ2M…
mcp://greeter is served by 12D3KooWQmB5…
tools: greet
hello Ada
python call.py a2a://greeter /card
on the mesh as 12D3KooWHZ2M…
a2a://greeter is served by 12D3KooWQmB5…
200 {"name": "greeter", "path": "/card", "caller": "12D3KooWHZ2M…"}
If you started the server with OLLAMA_URL=http://127.0.0.1:11434, ask it
for its models:
node call.js inference://ollama /v1/models
The caller and the server are two identities on the mesh with their own
state directories, caller and greeter. Each spent a token on its first
run; the standing join token of sam-one admits any number of members.
What happened
enroll sent the token and the program’s public key to the control plane
and received a credential: a signed token that names the member, its role
and the routers it may use. The identity key and the credential live in the
state directory, so the next run resumes without a token, and the SDK
renews the credential before it expires. Delete the directory to enroll
afresh, for instance with other labels. The directory has the same layout
in both SDKs and in sam-node: a program in one language resumes a
directory written by the other, and sam-node state import runs the same
identity as a node (see the sam-node reference).
join connected to the routers named in the credential, proved the
program’s identity to each and verified the router’s own credential, and
reserved a relay slot so that other members reach the program through the
router. The program opens no port of its own. While the session is open,
the SDK follows the control plane: keys, bans and router addresses on
sam-node’s schedule, and sooner when the control plane announces a change
over the mesh.
discover looked the service up by name in a table the routers host. Every
call then dials the addresses the provider advertised and the path through
the router, and verifies the provider’s credential before sending anything.
requiredLabels (required_labels in Python) refuses a provider whose
control-plane-attested labels do not carry the values you ask for.
serve fetched the mesh policy and started answering. Every caller must
present a credential signed by a trusted control plane key, bound to the
connection’s peer and unexpired, and its role must be granted the service
by the policy. The check runs on the same Datalog text a sam-node
evaluates. A caller the policy does not admit is turned away before the
request reaches your handler or your backend. Your handler sees the verified
caller as caller.peerId (caller.peer_id) and, for a forwarded backend
such as Ollama, as the X-Peer-Id header; the caller’s credential is never
forwarded.
Under the open development policy of sam-one, every member holds the
node role and that role may call any service. On a shared mesh the policy
grants a role the services it may call (allowed_services: ["mcp://greeter"]
or a pattern) and the members it may reach; see
Mesh policy. A refused MCP caller receives
AuthResponse{success: false} on the stream; a refused HTTP caller receives
403.
Beyond the examples
- Naming a peer.
callTool,listTools,request,authenticateandconnectaccept a provider fromdiscover, a peer id, or a multiaddr. With a peer id alone the SDK goes through the routers. - Every service of a type.
discover("mcp")lists every MCP provider on the mesh;discover("mcp://greeter")those of one service. - A member’s own catalog.
listTools(peer, "")returns the provider’slist_local_servicestool, the same one asam-nodeoffers. - Forwarding to a local server.
serve({ type: "inference", name, target: "http://127.0.0.1:8000" })andHTTPService(type="inference", name=..., target="http://...")forward authorized requests to a URL; a function instead of a URL handles them in the process. - Reading the policy.
session.policyRules(session.policy_rules) holds the Datalog the session enforces, for logging or tests.
What the SDKs do not do
- They do not run a sidecar API or a sandbox. An agent that needs the egress
policy enforcement of
sam-boxruns beside asam-node. - They do not serve the discovery table. A member is a client of it; the routers hold the records.
- They do not run in a browser. Node.js and CPython only, until the transport to routers from a browser is designed.
The wire contract and the interoperability facts the tests pin are in
sdk/README.md.