Skip to main content
Documentation

Local agent tools

Let a local coding agent use the same app tools as your website assistant through the DeepSpace CLI.

On this page

A DeepSpace app can expose one set of server-side tools in two places:

  • the in-app assistant is the AI chat running on the app's website;
  • a local agent is Codex, Claude Code, or another agent running on the user's machine.

The local agent reaches the app through the DeepSpace CLI. The CLI uses the user's current DeepSpace login, the app checks whether that user has access, and DeepSpace record tools run with that user's normal permissions.

These are direct REST requests, not an MCP connection. There is no app connection to create, approve, list, or delete, and normal tool calls do not open a browser.

Define tools once#

Keep tool definitions in src/ai/tools.ts. Its buildTools(executor) function defines each tool's name, description, input schema, and implementation.

The website assistant and local agent both use that function. When you add, remove, or change a tool, you change it once. The local discovery command returns only tools that have an implementation, together with their descriptions and JSON Schemas, so a local agent knows which arguments it can send.

See adding custom tools for the tool definition used by buildTools.

Enable the website and local agents#

Every new scaffold includes src/ai/agent.ts. Its registerAgent function belongs to the app and is imported from that local file; it is not an additional package or service. It lets the website assistant and local agent share one tool function and one access rule.

The copilot scaffold enables both by default:

// worker.ts
import { registerAgent } from './src/ai/agent.js'
import { buildTools } from './src/ai/tools.js'

registerAgent(app, { tools: buildTools })
ts

Choose a different arrangement only when the product needs it:

// Website assistant and local agent.
registerAgent(app, { tools: buildTools })

// Website assistant only.
registerAgent(app, { tools: buildTools, local: false })

// Local agent only. No chat UI or chat collections are required.
registerAgent(app, { tools: buildTools, inApp: false })

// Neither: do not call registerAgent.
ts

Keep this registration with the app's other API routes in worker.ts, before the platform proxy and static fallback. A starter app enables neither assistant until you make one of the calls above.

Control access#

Every request from the website assistant or local agent passes through the same checks:

  1. DeepSpace verifies the caller's login token.
  2. The app owner is accepted; another caller must already have a user row in that app. Using the signed-in app normally creates or refreshes that row.
  3. Your optional authorize function can apply an additional app-specific rule.
  4. DeepSpace applies the caller's normal collection and row permissions when a tool uses the provided executor.

A DeepSpace CLI login identifies the caller. It does not automatically give that person access to every DeepSpace app.

Use authorize for subscriptions, teams, roles, account status, or any other condition owned by the app:

registerAgent(app, {
  tools: buildTools,
  authorize: async ({ userId, env }) => hasActiveSubscription(env, userId),
})
ts

The callback returns true or false and applies to both assistants. It runs after the user's identity and app membership have been verified. It can make access stricter; it cannot add a user to the app or bypass the app's existing permissions.

Those permissions protect DeepSpace record tools. A custom tool that calls another service directly must enforce any resource-specific rules in its own implementation.

The developer chooses the available tools in buildTools. DeepSpace does not show the user a separate tool checklist or consent screen for local access.

Tools that need more than one record call#

buildTools receives a second argument from both the website assistant and the local agent routes:

import type { AgentToolContext, UserToolExecutor } from 'deepspace/worker'

export function buildTools(executor: UserToolExecutor, context: AgentToolContext<Env>): ToolSet {
  // context.env      the Worker bindings of this request
  // context.userId   the verified caller
  // context.request  the incoming request
}
ts

The executor runs record calls as the caller and returns whole results, so a tool can read a whole collection and compute its own answer. Use context.env for the Worker's bindings, for example appFiles(context.env, { scope: 'self', userId: context.userId }) to store a file. A factory that takes one argument keeps working.

Result size and paging#

Each tool's result is limited to the model's tool-result budget, about 30 KB, where it leaves the app: for the website assistant and the local agent alike. The limit applies to what a tool returns, not to the record calls inside it.

An oversized list is trimmed to the leading items that fit and flagged with truncated, returned, and total. records.query takes an offset and returns nextOffset when more records match; pass it back as offset, with the same filter and order, for the next page. A trimmed page's nextOffset starts at the first record it dropped. A result with no list to trim is refused with tool_result_too_large, which states the result's size and the limit.

A room per user or team#

Tools run in the app's record room. An app that gives each user or team its own room passes room to registerAgent:

registerAgent(app, {
  tools: buildTools,
  room: (userId, env) => `notes:${userId}`,
})
ts

DeepSpace calls it once per request with the verified caller, never with tool input. Membership is read in that room, so a user becomes a member there after a signed-in visit to the app, and the tools run in the same room.

An app that writes its own access check for registerAgentToolRoutes does the same in two steps: check membership with resolveAppMembership(env, userId, signal, { room }), then grant access with that room, { ok: true, auth, room }. The caller's tools run in the room the grant names.

Use tools from a local agent#

Sign in once on the machine if the CLI has no active session:

npx deepspace auth login
bash

Then discover the app's tools before invoking one:

npx deepspace agent tools my-app --json
bash

The result identifies the app and returns each tool's name, description, input schema, and optional output schema. Choose a returned tool and construct its input from that schema.

npx deepspace agent invoke my-app records_query \
  --input-file tool-input.json \
  --json
bash

For small inputs, pass inline JSON with --input. Use --input-file <path> for a file or --input-file - for stdin. --timeout sets the request timeout in milliseconds.

The app argument is either its canonical name (my-app) or its root canonical DeepSpace URL. Use a loopback URL for local development. Custom domains are not accepted as agent targets; use the app's canonical *.app.space URL instead. Production and staging use separate CLI sessions and app domains.

Existing apps#

An SDK dependency update cannot rewrite Worker files owned by the app. An app created before this feature existed must copy src/ai/agent.ts from the current scaffold, bring the authentication changes from the current src/ai/chat-routes.ts into its existing file, and then register the desired arrangement in worker.ts.

Review those source changes, type-check, test, and redeploy the app. Deploying the app adds its local tool routes. This feature needs no database migration or separate DeepSpace platform-worker deployment.

To give an existing app's tools the context or a room per user, follow migration 2026-10-agent-tool-context from npx deepspace@latest app update. It is optional.

See also#