Keep your orchestrator, model and agent state in your framework. Route effects through the connected MCP client so Rust still decides policy and broker admission.
When to use this
Use this when your application already uses custom Python, LangChain, LangGraph or CrewAI and needs protected effect tools.
Install the matching API preview
These examples use the API 2 preview, not names promised by the public RC5 release. Existing RC5 entry points remain available. The Python wheel below is verified on Windows x64; it is not a cross-platform release.
python -m pip install https://docs.soterias.io/downloads/api-preview/soterias_local-0.1.0rc5%2Bapi2-cp311-abi3-win_amd64.whlnpm install https://docs.soterias.io/downloads/api-preview/soterias-core-0.1.0-rc.5-api.2.tgz
npm install --save-dev typescript@5.8.3 @types/node@24Python imports soterias. TypeScript imports @soterias/core. Use Python 3.11+ for this wheel and Node 22.23.4+ for these examples. For a .mts file, compile with npx tsc example.mts --target ES2022 --module NodeNext --moduleResolution NodeNext --skipLibCheck, then run the emitted .mjs file.
Before you run
Set SOTERIAS_MCP_URL, SOTERIAS_MCP_TOKEN and SOTERIAS_REQUEST (the path to your original request JSON) from the trusted accounting host below. Python also needs python -m pip install mcp==1.30.0 httpx. The host must already hold any required exact operation approval. Install the optional preview with python -m pip install https://docs.soterias.io/downloads/api-preview/soterias_frameworks-0.1.0.dev23-py3-none-any.whl. Select custom, langchain, langgraph or crewai using SOTERIAS_FRAMEWORK. Install the selected extra: python -m pip install langchain-core==1.7.6 langgraph==1.2.20 or python -m pip install crewai==1.15.39. TypeScript's example additionally needs npm install @langchain/core. No model key is required to test these direct tools.
The SDK never invents missing identities, policy, trust or custody. For protected features, obtaining those operator artifacts is a prerequisite, just as connecting to a database requires its configured credentials. The native test provisioning helper is contributor tooling, not the application API.
Application walkthrough
Choose Python or TypeScript. Create example.py or example.mts and append that language's chunks in order. Each language forms one complete program; indentation and open blocks carry across chunks.
Import the packages
Import the SDK and standard-library helpers. These are the real package exports used by the executable program.
import asyncio
import json
import os
from pathlib import Path
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from soterias_frameworks import AccountingMcpClientimport fs from 'node:fs';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
import { tool } from '@langchain/core/tools';
import * as z from 'zod/v4';Connect your framework to the caller-owned MCP session
This optional package adds adapters; the native SDK and host remain separate.
# %% Connect your framework to the caller-owned MCP session
# This optional package adds adapters; the native SDK and host remain separate.
url = os.environ["SOTERIAS_MCP_URL"]
request = Path(os.environ["SOTERIAS_REQUEST"]).read_text(encoding="utf-8")
framework = os.environ.get("SOTERIAS_FRAMEWORK", "custom")
async def main():
headers = {"Authorization": "Bearer " + os.environ["SOTERIAS_MCP_TOKEN"]}
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
client = AccountingMcpClient(session)// %% Connect a LangChain tool to the caller-owned MCP session
// TypeScript uses the official MCP client; Soterias's optional adapters are Python.
const client = new Client({ name: 'invoice-framework', version: '1.0.0' });
try {
await client.connect(new StreamableHTTPClientTransport(new URL(process.env.SOTERIAS_MCP_URL!),
{ requestInit: { headers: { Authorization: 'Bearer ' + process.env.SOTERIAS_MCP_TOKEN },
redirect: 'error', credentials: 'omit' } }));Choose a framework surface without changing the secured backend
These direct tool calls need no model key and preserve native refusals.
# %% Choose a framework surface without changing the secured backend
# These direct tool calls need no model key and preserve native refusals.
if framework == "custom":
result = await client.execute(request)
if not result.ok:
raise RuntimeError("Protected operation refused")
outcome = result.outcome.model_dump()
elif framework == "langchain":
from soterias_frameworks.langchain import langchain_accounting_tool
outcome = json.loads(await langchain_accounting_tool(client).ainvoke(
{"requestJson": request}))
elif framework == "langgraph":
from langchain_core.messages import AIMessage
from langgraph.graph import END, MessagesState, StateGraph
from soterias_frameworks.langchain import langgraph_accounting_node
graph = StateGraph(MessagesState)
graph.add_node("accounting", langgraph_accounting_node(client))
graph.set_entry_point("accounting")
graph.add_edge("accounting", END)
messages = await graph.compile().ainvoke({"messages": [
AIMessage(content="", tool_calls=[{"name": "execute_accounting",
"id": "invoice-42", "args": {"requestJson": request}, "type": "tool_call"}])]})
outcome = json.loads(messages["messages"][-1].content)
elif framework == "crewai":
from soterias_frameworks.crewai import crewai_accounting_tool
result = await crewai_accounting_tool(client).arun(requestJson=request)
outcome = result.model_dump()
else:
raise ValueError("Choose custom, langchain, langgraph or crewai") // %% Wrap the actual protected host tool
// The host owns credentials, policy and exact approval. The wrapper cannot sign.
const accounting = tool(async ({ requestJson }) => {
const result = await client.callTool({ name: 'execute_accounting', arguments: { requestJson } });
if (result.isError) throw new Error('Protected operation refused');
const content = result.content.find(block => block.type === 'text');
if (!content || content.type !== 'text') throw new Error('Expected an accounting outcome');
return content.text;
}, { name: 'execute_accounting', description: 'Submit one exact protected accounting request.',
schema: z.object({ requestJson: z.string().min(1).max(65536) }).strict() });Inspect the original protected outcome
Do not cache or automatically retry these effect tools.
# %% Inspect the original protected outcome
# Do not cache or automatically retry these effect tools.
print(json.dumps({"status": outcome["status"], "framework": framework,
"target_observed": outcome["targetObserved"]}))
asyncio.run(main()) // %% Invoke the tool directly before adding a model or graph
// This test needs no model key. Do not cache or automatically retry effect tools.
const outcome = JSON.parse(await accounting.invoke({
requestJson: fs.readFileSync(process.env.SOTERIAS_REQUEST!, 'utf8'),
}));
console.log(JSON.stringify({ status: outcome.status, framework: 'langchain', target_observed: outcome.targetObserved }));
} finally { await client.close(); }MCP composition
Administration and signing stay on the trusted operator surface shown above. After that setup, an agent uses the real execute_accounting tool below. This client submits an operation through the configured authority; it cannot issue warrants, activate reviews or sign approvals. Native timings and administration diagnostics are read through their SDK owners, rather than invented MCP endpoints.
Configure the host
The following complete host requires the operator's basic SQLite configuration.json, transport-policy.json, exact operation-approval.json and retained checkpoint. For a document or delegated host, use its matching native profile and artifacts; do not substitute the basic host configuration into another profile. Set SOTERIAS_CONNECTION to a new private file for the endpoint/credential, and SOTERIAS_NEXT_CHECKPOINT to a new independently retained file for the shutdown pin. Share only the scoped execution credential with the agent.
host.mjs
Create host.mjs and append these chunks in order. Together they form one complete program; indentation carries across chunks.
Step 1
Import the SDK and standard-library helpers. These are the real package exports used by the executable program.
import fs from 'node:fs';
import path from 'node:path';
import { security } from '@soterias/core';
import { listenAccountingHttp } from '@soterias/mcp-server/accounting-http';Open the operator-selected native owner
Enrollment, tool review, warrants and exact operation approval happen beforehand.
// %% Open the operator-selected native owner
// Enrollment, tool review, warrants and exact operation approval happen beforehand.
const root = process.env.SOTERIAS_ROOT;
const read = name => fs.readFileSync(path.join(root, name));
const owner = security.Host.open({ root, custody: process.env.SOTERIAS_CUSTODY,
configuration: read('configuration.json'),
checkpoint: fs.readFileSync(process.env.SOTERIAS_CHECKPOINT),
passphrase: Buffer.from(process.env.SOTERIAS_UNLOCK) });Start a loopback host with authenticated native admission
The agent cannot replace the policy, custody or signed approval through a tool call.
// %% Start a loopback host with authenticated native admission
// The agent cannot replace the policy, custody or signed approval through a tool call.
let listener;
try {
listener = await listenAccountingHttp(owner, {
transportPolicy: read('transport-policy.json'),
operationApproval: read('operation-approval.json'),
});
const token = owner.issueTransportCredential().toString('base64url');
fs.writeFileSync(process.env.SOTERIAS_CONNECTION,
JSON.stringify({url: listener.url, token}), {flag: 'wx', mode: 0o600});
} catch (error) { owner.close(); throw error; }Retain the acknowledged checkpoint when shutting down
Keep this output independently of the writable host and signing custody.
// %% Retain the acknowledged checkpoint when shutting down
// Keep this output independently of the writable host and signing custody.
let stopping;
function stop() {
stopping ??= (async () => {
await listener.close();
fs.writeFileSync(process.env.SOTERIAS_NEXT_CHECKPOINT, listener.checkpoint(),
{flag: 'wx', mode: 0o600});
})();
return stopping;
}
for (const signal of ['SIGINT', 'SIGTERM']) process.once(signal, () => {
void stop().catch(() => { process.stderr.write('host_shutdown_failed\n'); process.exitCode = 1; });
});
// A parent process can close this control stream for orderly local shutdown.
process.stdin.resume();
process.stdin.once('end', () => { void stop(); });Call the protected tool
Save this complete HTTP client as mcp.mjs. It uses the operator-provided endpoint, scoped token and original request from the environment. Run it with node mcp.mjs; inspect its outcome and verify its signed receipts separately.
Install the matching adapter with npm install https://docs.soterias.io/downloads/api-preview/soterias-mcp-server-0.1.0-rc.5-api.2.tgz. Its SDK peer dependency shares the same native class identities as your installed preview.
mcp.mjs
Create mcp.mjs and append these chunks in order. Together they form one complete program; indentation carries across chunks.
Step 1
Import the SDK and standard-library helpers. These are the real package exports used by the executable program.
import fs from 'node:fs';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';Connect to an operator-configured protected MCP host
The token is scoped to execution. Reviewer keys never enter this client.
// %% Connect to an operator-configured protected MCP host
// The token is scoped to execution. Reviewer keys never enter this client.
const client = new Client({ name: 'invoice-assistant', version: '1.0.0' });
const transport = new StreamableHTTPClientTransport(new URL(process.env.SOTERIAS_MCP_URL),
{ requestInit: { headers: { Authorization: 'Bearer ' + process.env.SOTERIAS_MCP_TOKEN },
redirect: 'error', credentials: 'omit' } });
try {
await client.connect(transport);Execute through the native host's real accounting tool
The host selects routes and approval evidence; the tool receives a request.
// %% Execute through the native host's real accounting tool
// The host selects routes and approval evidence; the tool receives a request.
const requestJson = fs.readFileSync(process.env.SOTERIAS_REQUEST, 'utf8');
const result = await client.callTool({ name: 'execute_accounting', arguments: { requestJson } });
if (result.isError) throw new Error('Protected tool refused the request');
const content = result.content.find(block => block.type === 'text');
if (!content || content.type !== 'text') throw new Error('Expected an accounting outcome');
const outcome = JSON.parse(content.text);Inspect the outcome instead of treating transport success as business success
Verify original receipts separately with your own retained trust.
// %% Inspect the outcome instead of treating transport success as business success
// Verify original receipts separately with your own retained trust.
console.log(JSON.stringify({ status: outcome.status, target_observed: outcome.targetObserved }));
} finally { await client.close(); }Check the result
The application program prints a JSON result and preserves native refusals. For protected operations inspect status and target_observed: transport success does not imply a completed effect. The pure examples contain assertions for their exact expected values. Successful startup is not proof of production enrollment, a physical human review or current authority.
Put it into a workflow
Use the AML walkthrough for protected document reads, reviewed writes and signed outcome checks. Use multi-agent workflows for personal and enterprise orchestration. Keep each agent's proposals separate from operator authority.