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 owner policy and Budgets preflight
- 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, sessionId, approvalId?}) works the same for both.
OAuth / SaaS connection metadata
V2 adds provider-agnostic toolConnections metadata for SaaS credentials:
owner, agent, provider, scopes, status, createdAt, revokedAt, and encrypted
token references. V1 is manual registration plus server-side token storage; it
does not ship a catalog of SaaS integrations. The pattern is simple: agents
invoke tools without seeing credentials, and revoked connections block future
credential reads or invocations.
Firestore schema
| Path | Purpose |
|---|---|
agents/{agentId}/tools/{name} | One tool registration. Fields: name, kind, url, authTokenEnc (KMS), kmsKey, manifest, enabled, createdAt, updatedAt. |
agents/{agentId}/toolConnections/{id} | OAuth/SaaS connection metadata. Fields: provider, scopes, status, createdAt, revokedAt, encrypted token references. |
Auth tokens are encrypted with
projects/axy-ujex/locations/us-central1/keyRings/ujex/cryptoKeys/secrets
before hitting Firestore. The plaintext lives only inside invokeTool 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. Runs policy + Budgets preflight before fetch, injects credentials, audits tool.invoke with status + error, meters one tools.invoke unit. Timeout: 15s default, 60s max. |
getToolCredential | Agent | Deprecated compatibility export. Production servers deny plaintext credential reads; use invokeTool. |
toolsRegisterConnection | Owner | Manually registers provider/scopes and KMS-encrypted access/refresh tokens for a SaaS connection. |
toolsListConnections | Owner or agent | Lists connection metadata; token references are not returned. |
toolsRevokeConnection | Owner | Marks a connection revoked; linked tools stop decrypting credentials. |
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 | Deprecated compatibility call denied before token return |
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},
sessionId: 'sess_run_20260618_001',
timeoutMs: 30_000,
});
If policy requires human review, the first call returns
{status: 'waiting_approval', approvalId, reason} without calling the remote
tool. Retry with the returned approvalId; Ujex executes only when the approval
matches the exact name,args digest.
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.