Tools
A registry of callable MCP or HTTP endpoints per agent, dispatched server-side so credentials never leave the control plane and every invocation is audited + metered.
Why server-side dispatch?
If an agent were given raw API keys, an injected prompt could exfiltrate them in a single message. Ujex owners register tool credentials once, encrypted with a KMS key the agent never sees, and the agent invokes them through a Cloud Function that:
- Authenticates the agent
- Enforces the owner's quota (
tools.invoke) - Decrypts the auth token transiently
- Forwards the call to the tool URL with the auth injected
- Logs the invocation + status to the hash-chained audit log
The agent only learns the tool's response. The credential stays behind glass.
Two tool kinds
| Kind | Body shape | Use when |
|---|---|---|
mcp | JSON-RPC 2.0: {jsonrpc, id, method: "tools/call", params: {name, arguments}} | The tool server speaks the MCP protocol (a normal AI-tool server) |
http | Raw JSON pass-through of args | The tool is a plain REST/JSON endpoint |
Owners declare kind at registration time. The agent side is identical —
invokeTool(name, args) works the same for both.
Firestore schema
| Path | Purpose |
|---|---|
agents/{agentId}/tools/{name} | One tool registration. Fields: name, kind, url, authTokenEnc (KMS), kmsKey, manifest, enabled, createdAt, updatedAt. |
Auth tokens are encrypted with
projects/axy-ujex/locations/us-central1/keyRings/ujex/cryptoKeys/secrets
before hitting Firestore. The plaintext lives only inside the
invokeTool / getToolCredential Cloud Functions for the duration of a
single invocation.
Routes
| Function | Caller | What it does |
|---|---|---|
registerTool | Owner (human) | Creates or updates a tool doc. Validates URL, auto-fetches the MCP manifest if not supplied, KMS-encrypts the auth token. Audits tool.register. |
listTools | Agent | Returns enabled tools (name/kind/url/manifest) — credentials excluded. |
invokeTool | Agent | Server-side dispatch. Fetches credentials, builds the body, forwards the call, audits tool.invoke with status + error, meters one tools.invoke unit. Timeout: 15s default, 60s max. |
getToolCredential | Agent | (Escape hatch.) Decrypts and returns the token directly. Audits every access. Prefer invokeTool so the token never touches agent memory. |
Quotas
tools.invoke is metered per owner in
agents/{agentId}/usage/{YYYY-MM}. On network failure the unit is
credited back so a flaky tool doesn't consume quota.
Audit events emitted
| Action | Actor | Meta |
|---|---|---|
tool.register | human | kind, url |
tool.getCredential | agent | — |
tool.invoke | agent | status, ok, error |
Typical flow
// One-time, as the owner
await ujex.tools.register({
agentId: 'agent-alice',
name: 'search',
kind: 'http',
url: 'https://search.example.com/query',
authToken: process.env.SEARCH_API_KEY,
});
// Repeatedly, from inside the agent
const {tools} = await ujex.tools.list();
const {status, result} = await ujex.tools.invoke({
name: 'search',
args: {q: 'latest inflation print', limit: 5},
timeoutMs: 30_000,
});
Gotchas
- MCP manifest auto-fetch is best-effort. If the manifest URL is unreachable at registration time, the tool is still stored without a manifest and agents can invoke it.
- Disabled tools stay in Firestore. Flip
enabledtofalserather than delete so the audit trail remains resolvable. - Timeouts are clamped. A caller asking for
timeoutMs: 600_000gets 60s; Cloud Functions v2 max invocation time governs. - No retry policy yet. If a tool returns 500 once, the call fails — the agent is responsible for retry. A circuit-breaker is planned.
See also the API reference for exact function signatures, request/response shapes, and error codes.