# Tools and approvals Besides its built-in tools, an agent can call your own tools: a **toolset** is one MCP server (`kind: mcp`) or a set of HTTP endpoints (`kind: http`) under one base URL. The model sees each tool as `__`, for example `github__create_issue`. ## Keys stay with orkestr The agent's VM only knows tool names and schemas. When it calls a tool, the orkestr gateway adds the toolset's credential and makes the call server-side, then hands back the result. A credential is stored encrypted and is never shown again, not even to you. ## Add a toolset **Python:** ```python from orkestr import Toolset docs = Toolset.create("docs", "mcp", "https://mcp.example.com/mcp") print([t["name"] for t in docs["tools"]]) # discovered from the server # A server that needs a key: orkestr keeps it, the agent never sees it. github = Toolset.create( "github", "mcp", "https://api.githubcopilot.com/mcp/", auth_header_name="Authorization", auth_secret="Bearer " + token, ) ``` **JavaScript:** ```javascript import { Toolset } from "orkestr"; const docs = await Toolset.create("docs", "mcp", "https://mcp.example.com/mcp"); console.log(docs.tools.map((t) => t.name)); // discovered from the server // A server that needs a key: orkestr keeps it, the agent never sees it. const github = await Toolset.create("github", "mcp", "https://api.githubcopilot.com/mcp/", { authHeaderName: "Authorization", authSecret: `Bearer ${token}`, }); ``` **cURL:** ```bash curl -X POST https://api.orkestr.eu/v1/agent-toolsets \ -H "Authorization: Bearer $ORKESTR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "docs", "kind": "mcp", "url": "https://mcp.example.com/mcp"}' ``` From the CLI, pass the key by the *name* of an environment variable, so it never appears in a terminal history or a coding assistant's chat: ```bash export GITHUB_TOKEN=... # in your own shell orkestr agents tools add github --url https://api.githubcopilot.com/mcp/ --auth-env GITHUB_TOKEN orkestr agents publish my-agent --tools github ``` `--auth-header` (default `Authorization`) and `--auth-scheme` (default `Bearer` for that header) shape the value. For `--kind http`, describe the endpoints in a JSON file: `--http-tools-file tools.json`, a list of `{name, description, method, path, input_schema}`. Toolsets are not versioned: rotating a key or turning a tool off applies to every version that uses the toolset. Which toolsets an agent may use is part of its version (`toolset_ids`). ## Connectors (OAuth) Many services run their own MCP server and sign you in with their own OAuth: Linear, Notion, Sentry, Atlassian and others. Add one as a toolset with `auth_type: oauth` and connect it once: orkestr finds the provider's sign-in, registers itself as a client, and gives you a link. You approve in your browser; orkestr keeps the tokens encrypted, refreshes them, and sends them only from its gateway. The agent's VM never sees a token. ```bash orkestr agents tools catalog orkestr agents tools add linear --catalog linear orkestr agents tools connect linear # prints the link, waits for the approval orkestr agents publish my-agent --tools linear ``` **Python:** ```python from orkestr import Toolset print([c["slug"] for c in Toolset.catalog()]) linear = Toolset.create("linear", "mcp", "https://mcp.linear.app/mcp", auth_type="oauth", catalog_slug="linear") print(Toolset.connect(linear["id"])) # a person opens this link and approves ``` **JavaScript:** ```javascript import { Toolset } from "orkestr"; console.log((await Toolset.catalog()).map((c) => c.slug)); const linear = await Toolset.create("linear", "mcp", "https://mcp.linear.app/mcp", { authType: "oauth", catalogSlug: "linear", }); console.log(await Toolset.connect(linear.id)); // a person opens this link and approves ``` **cURL:** ```bash curl -X POST https://api.orkestr.eu/v1/agent-toolsets/$TOOLSET_ID/oauth/start -H "Authorization: Bearer $ORKESTR_API_KEY" # -> {"authorize_url": "https://...", "expires_in": 600} ``` Any other MCP server that uses OAuth works the same with `--url URL --oauth`. A few providers (Google, Microsoft, Slack) do not let clients register themselves: create an OAuth app there with the redirect URL `https://api.orkestr.eu/api/agent-toolsets/oauth/callback` and pass `--client-id` and `--client-secret-env VAR`. If the provider stops accepting the tokens (you revoked access, or removed a scope), the toolset shows `needs_reconnect` and the agent gets a plain "reconnect it" tool error instead of a crash. Run `connect` again. `orkestr agents tools disconnect NAME` forgets the tokens and asks the provider to revoke them where it can. A connector acts on a real account, so every tool its server does not mark read-only (the MCP `readOnlyHint`) starts as `approval: always`. Change that per tool below; your choice is kept when the tool list is refreshed. ## Connections per end user By default a connector has one connection, yours, and every run uses it. If your agent serves your own users and should act in *their* Linear or Jira, turn on `oauth_per_end_user`: each end user connects their own account, and a run for that `end_user_id` uses only theirs. **Python:** ```python from orkestr import Toolset # Once: each end user connects their own account. Toolset.update(linear_id, oauth_per_end_user=True) # In your app, when a user clicks "Connect Linear": url = Toolset.connect(linear_id, end_user_id="cust-42", return_url="https://app.example.com/settings/integrations") # redirect the user to url Toolset.connections(linear_id) # who is connected ("-" = you) Toolset.disconnect_end_user(linear_id, "cust-42") ``` **JavaScript:** ```javascript import { Toolset } from "orkestr"; // Once: each end user connects their own account. await Toolset.update(linearId, { oauth_per_end_user: true }); // In your app, when a user clicks "Connect Linear": const url = await Toolset.connect(linearId, { endUserId: "cust-42", returnUrl: "https://app.example.com/settings/integrations", }); // redirect the user to url await Toolset.connections(linearId); // who is connected ("-" = you) await Toolset.disconnectEndUser(linearId, "cust-42"); ``` **cURL:** ```bash curl -X POST https://api.orkestr.eu/v1/agent-toolsets/$TOOLSET_ID/oauth/start -H "Authorization: Bearer $ORKESTR_API_KEY" -H "Content-Type: application/json" -d '{"end_user_id": "cust-42", "return_url": "https://app.example.com/settings/integrations"}' # -> {"authorize_url": "https://...", "expires_in": 600} curl https://api.orkestr.eu/v1/agent-toolsets/$TOOLSET_ID/connections -H "Authorization: Bearer $ORKESTR_API_KEY" curl -X DELETE https://api.orkestr.eu/v1/agent-toolsets/$TOOLSET_ID/connections/cust-42 -H "Authorization: Bearer $ORKESTR_API_KEY" ``` Your app asks for a link with the user's `end_user_id` and sends them to it. After they approve, they land on your `return_url` (https) with `?orkestr_connected=1`, or `?orkestr_error=...` if they declined. Without a `return_url` they see a short "connected, you can close this tab" page. From the CLI: `orkestr agents tools connect linear --end-user cust-42`. > **Never your account** > > In this mode a run for an end user who has not connected never falls back to your connection: the tool tells the agent the user has not connected yet, so it can ask them to. Runs without an end user (your own tests) use your connection. Erasing an end user also deletes their connected accounts. ## Turn tools off, or require approval ```bash orkestr agents tools set github create_issue --approval always orkestr agents tools set github delete_repo --disable orkestr agents tools refresh github # re-read the MCP server's tool list ``` Over the API this is the toolset's `tool_overrides`: `{"create_issue": {"enabled": true, "approval": "always"}}`. ## Approving a call When the model calls a tool marked `approval: always`, the run pauses: an invoke returns `status: pending_approval` with the tool and its arguments, and a stream ends with `run.paused`. Approve or deny from your app, the API, or the console (the agent page, and the workspace's "Waiting for you"). On approval orkestr makes the call and the run continues where it stopped. Approvals expire after 24 hours by default. **Python:** ```python from orkestr import AgentRun result = agent.invoke("Open an issue for the login bug") if result.status == "pending_approval": print(result.approval.tool, result.approval.arguments) AgentRun.get(result.run_id).approve() # or .deny() ``` **JavaScript:** ```javascript import { AgentRun } from "orkestr"; const result = await agent.invoke("Open an issue for the login bug"); if (result.status === "pending_approval") { await (await AgentRun.get(result.runId)).approve(); // or .deny() } ``` **cURL:** ```bash curl -X POST https://api.orkestr.eu/v1/agents/runs/$RUN_ID/approvals/$REQUEST_ID \ -H "Authorization: Bearer $ORKESTR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"decision": "approve"}' ``` > **Tokens** > > Deciding needs `agents:invoke`; managing toolsets needs `agents:write`.