Tool contract
The defineTool reference: fields, server execution, render, and typing.
defineTool describes one agent-callable capability. The definition is what the agent sees; the
chained .server and .render attach behavior.
import {z} from 'zod'
import {defineTool} from '@conciv/extension'
const tool = defineTool({
name: 'deploy_run',
description: 'Deploy the current branch to an environment.',
inputSchema: z.object({env: z.enum(['staging', 'prod'])}),
approval: 'ask',
})
.server(async (input, ctx, request) => ({ok: true}))
.render((props) => <div>done</div>)Definition fields
Prop
Type
.server(execute)
The node-side action. It receives three arguments:
.server(async (input, ctx, request) => { ... })inputis alreadyinputSchema.parsed, so it is fully typed.ctxis the context your extension's.server(...)factory returned (see below).unknownunless you annotate the handler parameter.requestis{sessionId: string; model: string | null}for the calling chat session.
The return value is the tool result. Return small structured data; the agent reads it verbatim.
Typed context
Tools that need shared server state (a db handle, a child process manager) declare a context type,
and the extension's server factory must provide it. The RequiredContext of all tools is enforced
at the defineExtension(...).server(...) boundary:
const inputSchema = z.object({by: z.number().default(1)})
type Ctx = {counter: {next: () => number}}
const bump = defineTool({
name: 'bump',
description: 'Increment the counter.',
inputSchema,
}).server((input, ctx: Ctx) => ({value: ctx.counter.next()}))
export default defineExtension({name: 'counter', tools: [bump]}).server(() => {
let value = 0
return {context: {counter: {next: () => ++value}}}
}).render(renderer)
Replaces the generic result card. The renderer is a Solid component receiving
{part, result, ctx, durationMs}:
partis the tool call (id, name, arguments).resultis the tool result once it lands,undefinedwhile running. Itscontentcarries what your server returned,erroris set on failure.ctxis the widget view context (theme, expansion state).
Extension-level server
defineExtension(...).server(factory) runs once at engine boot. It receives
{config, cwd, sessions, harness}: your parsed configSchema config, the project root, and
session and harness handles. To add custom routes (SSE streams, uploads, anything a tool result
cannot carry), return a hono app; it is mounted under your extension's API
namespace (/api/ext/<name>).
import {Hono} from 'hono'
export default defineExtension({name: 'metrics', configSchema, tools: [...]}).server((server) => {
const app = new Hono().get('/stream', (c) => openStream(c))
const poller = startPolling(server.config.interval)
return {context: {poller}, app, dispose: () => poller.stop()}
})Return {context, app, dispose}: context feeds your tools, app is optional, dispose runs on
shutdown.
Exports
Everything above comes from one package:
import {defineExtension, defineTool} from '@conciv/extension'
import type {ExtensionBuilder, ToolBuilder, ServerApi, ToolRequest} from '@conciv/extension'Client-side building blocks (mountExtension, useSlot, useContext, the client API) are
covered in Widget UI.