Tools Management
Endpoints for managing the individual tools within a Toolkit.
Every tools route resolves its parent Toolkit through the toolkit.lookup
server method, and that lookup calls ai.verify() first. AI must therefore be
enabled on the tenant for any of these routes to succeed: when AI is disabled,
even the read routes answer 400. Read routes require Toolkit read access
(enforced by the Toolkit's read_access scope, not a permission). Write routes
(POST, PUT, DELETE) additionally require permission.toolkit.write, which
resolves to data-wizard access on the Toolkit.
GET /api/toolkits/{id}/tools
List all tools in a Toolkit.
Authentication: Toolkit read access (AI-gated via toolkit.lookup)
Response:
A HAL collection embedding inf:toolkit-tool, ordered by sortOrder ASC then
name ASC. Each tool carries the full row, including config (the
api-request script), parameters, needsApproval, and the discovery
metadata (discoveredAt / discoveredBy, both null for hand-authored tools).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools"
}
},
"_embedded": {
"inf:toolkit-tool": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "api-request",
"name": "close_issue",
"description": "Close a GitHub issue",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues/1', method: 'patch' };",
"parameters": null,
"sortOrder": 0,
"needsApproval": false,
"discoveredAt": null,
"discoveredBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000003"
}
},
"id": "00000000-0000-4000-8000-000000000003",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "api-request",
"name": "create_issue",
"description": "Create a new GitHub issue",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'post' };",
"parameters": {
"type": "object",
"required": [
"title"
],
"properties": {
"body": {
"type": "string",
"description": "Issue body"
},
"title": {
"type": "string",
"description": "Issue title"
}
}
},
"sortOrder": 0,
"needsApproval": false,
"discoveredAt": null,
"discoveredBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
]
}
}
Tool properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Tool id |
toolkitId | string | Parent Toolkit id |
type | string | Tool type (api-request for custom tools, mcp for discovered tools) |
name | string | Tool name |
description | string | null | Tool description |
config | string | null | Tool-specific configuration (the api-request script); null for discovered MCP tools |
parameters | object | null | JSON Schema describing the tool's parameters |
sortOrder | integer | Display order |
needsApproval | boolean | Whether the tool requires approval before it runs |
discoveredAt | date | null | When the tool was discovered (MCP sync only) |
discoveredBy | string | null | Discovery source (MCP sync only) |
createdAt / updatedAt | date | Timestamps |
POST /api/toolkits/{id}/tools
Add a custom tool to a Toolkit. toolkitId is taken from the URL.
Authentication: permission.toolkit.write
Request Body:
The payload is stripUnknown, so unrecognized fields are dropped rather than
rejected.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tool name; must match /^[a-z][a-zA-Z0-9_]*$/ (max 64 chars) |
config | string | Yes | The api-request script string (non-empty) |
description | string | No | Tool description |
type | string | No | Tool type (default api-request) |
parameters | object | No | JSON Schema for tool parameters |
sortOrder | integer | No | Display order (default 0) |
needsApproval | boolean | No | Require approval before running (default false) |
Both config and name are required. config is the axios-config script the
tool runs; the old "optional configuration" framing was wrong.
Example Request:
{
"name": "list_issues",
"description": "List open issues in the repository",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'get' };",
"parameters": {
"type": "object",
"properties": {
"state": { "type": "string", "description": "open, closed, or all" }
},
"required": []
},
"sortOrder": 1
}
Response:
Responds 200 with the created tool. The handler returns the row directly; it
does not use .created(), so the status is 200, not 201.
{
"name": "list_issues",
"description": "List open issues in the repository",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'get' };",
"parameters": {
"type": "object",
"properties": {
"state": {
"type": "string",
"description": "open, closed, or all"
}
},
"required": []
},
"sortOrder": 1
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"tenant": "acme",
"name": "list_issues",
"description": "List open issues in the repository",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'get' };",
"parameters": {
"type": "object",
"required": [],
"properties": {
"state": {
"type": "string",
"description": "open, closed, or all"
}
}
},
"sortOrder": 1,
"type": "api-request",
"needsApproval": false,
"toolkitId": "00000000-0000-4000-8000-000000000001",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"discoveredAt": null,
"discoveredBy": null
}
PUT /api/toolkits/{id}/tools
Sync discovered tools. This is the destructive MCP discovery sync: it deletes
all existing tools for the Toolkit, then bulk-creates new rows from the
tools array. It backs both mcp-remote (server-side) and mcp-stdio (Informer
GO client) discovery.
Authentication: permission.toolkit.write
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
tools | array | Yes | Discovered tools to create |
discoveredBy | string | No | Discovery source; defaults to unknown |
Each entry in tools is normalized on the way in: type is forced to mcp,
parameters is taken from the entry's inputSchema (falling back to
parameters), sortOrder is the entry's array index, discoveredAt is set to
the current time, and discoveredBy defaults to unknown when the top-level
field is omitted.
Example Request:
{
"tools": [
{
"name": "get_repo",
"description": "Get repository details",
"inputSchema": {
"type": "object",
"properties": {
"owner": { "type": "string" },
"repo": { "type": "string" }
},
"required": ["owner", "repo"]
}
}
],
"discoveredBy": "server"
}
Response:
Responds 200 with { items, discoveredAt }, where items is the freshly
created tool list and discoveredAt is the sync timestamp. It is not a bare
array.
{
"tools": [
{
"name": "get_repo",
"description": "Get repository details",
"inputSchema": {
"type": "object",
"properties": {
"owner": {
"type": "string"
},
"repo": {
"type": "string"
}
},
"required": [
"owner",
"repo"
]
}
},
{
"name": "list_branches",
"description": "List repository branches",
"inputSchema": {
"type": "object",
"properties": {
"owner": {
"type": "string"
},
"repo": {
"type": "string"
}
},
"required": [
"owner",
"repo"
]
}
}
],
"discoveredBy": "server"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools"
}
},
"items": [
{
"id": "00000000-0000-4000-8000-000000000002",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "mcp",
"name": "get_repo",
"description": "Get repository details",
"config": null,
"parameters": {
"type": "object",
"required": [
"owner",
"repo"
],
"properties": {
"repo": {
"type": "string"
},
"owner": {
"type": "string"
}
}
},
"sortOrder": 0,
"needsApproval": false,
"discoveredAt": "2026-01-15T16:20:00.000Z",
"discoveredBy": "server",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
},
{
"id": "00000000-0000-4000-8000-000000000003",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "mcp",
"name": "list_branches",
"description": "List repository branches",
"config": null,
"parameters": {
"type": "object",
"required": [
"owner",
"repo"
],
"properties": {
"repo": {
"type": "string"
},
"owner": {
"type": "string"
}
}
},
"sortOrder": 1,
"needsApproval": false,
"discoveredAt": "2026-01-15T16:20:00.000Z",
"discoveredBy": "server",
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
],
"discoveredAt": "2026-01-15T16:20:00.000Z"
}
This endpoint replaces ALL tools in the Toolkit. Every existing tool is deleted before the new rows are created. Use it for MCP Toolkit discovery workflows, not for incremental edits.
GET /api/toolkits/{id}/tools/{toolId}
Get a specific tool, scoped to its parent Toolkit.
Authentication: Toolkit read access (AI-gated via toolkit.lookup)
Response:
The single inf:toolkit-tool. A toolId that does not belong to this Toolkit
answers 404 with Tool not found.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "api-request",
"name": "create_issue",
"description": "Create a new GitHub issue",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'post' };",
"parameters": {
"type": "object",
"required": [
"title"
],
"properties": {
"body": {
"type": "string",
"description": "Issue body"
},
"title": {
"type": "string",
"description": "Issue title"
}
}
},
"sortOrder": 0,
"needsApproval": false,
"discoveredAt": null,
"discoveredBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
PUT /api/toolkits/{id}/tools/{toolId}
Update a tool. All fields are optional; the handler updates the supplied fields then re-reads the row.
Authentication: permission.toolkit.write
Request Body:
The payload is stripUnknown. Accepts the same fields as the create route
(name, config, description, type, parameters, sortOrder,
needsApproval), but all are optional here. When config is present it must be
a non-empty string; an empty string or null answers 400.
Example Request:
{
"description": "Create a new GitHub issue with labels",
"needsApproval": true
}
Response:
Responds 200 with the updated tool.
{
"description": "Create a new GitHub issue with labels",
"needsApproval": true
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"toolkitId": "00000000-0000-4000-8000-000000000001",
"type": "api-request",
"name": "create_issue",
"description": "Create a new GitHub issue with labels",
"config": "return { url: 'https://api.github.com/repos/acme/widgets/issues', method: 'post' };",
"parameters": {
"type": "object",
"required": [
"title"
],
"properties": {
"body": {
"type": "string",
"description": "Issue body"
},
"title": {
"type": "string",
"description": "Issue title"
}
}
},
"sortOrder": 0,
"needsApproval": true,
"discoveredAt": null,
"discoveredBy": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
DELETE /api/toolkits/{id}/tools/{toolId}
Delete a tool from the Toolkit.
Authentication: permission.toolkit.write
Response:
Responds 200 with an empty body (Informer convention), not 204. The handler
returns null after destroying the row. A toolId that does not belong to this
Toolkit answers 404.
Tool Parameters Schema
Tools use JSON Schema to define their parameters:
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"default": 10
}
},
"required": ["query"]
}
This schema is used by the AI to understand how to call the tool correctly. For
custom (api-request) tools you supply it directly in parameters; for
discovered MCP tools it comes from each tool's inputSchema during the sync.