Skip to main content

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).

List a toolkit’s toolsGET /api/toolkits/00000000-0000-4000-8000-000000000001/tools
GET /api/toolkits/{id}/tools returns a HAL collection embedding inf:toolkit-tool, ordered by sortOrder ASC then name ASC. AI-gated: the route resolves the toolkit via toolkit.lookup, which requires AI enabled on the tenant. Requires toolkit read access.
Response · 200
{
"_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"
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

Tool properties:

FieldTypeDescription
idstring (UUID)Tool id
toolkitIdstringParent Toolkit id
typestringTool type (api-request for custom tools, mcp for discovered tools)
namestringTool name
descriptionstring | nullTool description
configstring | nullTool-specific configuration (the api-request script); null for discovered MCP tools
parametersobject | nullJSON Schema describing the tool's parameters
sortOrderintegerDisplay order
needsApprovalbooleanWhether the tool requires approval before it runs
discoveredAtdate | nullWhen the tool was discovered (MCP sync only)
discoveredBystring | nullDiscovery source (MCP sync only)
createdAt / updatedAtdateTimestamps

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.

FieldTypeRequiredDescription
namestringYesTool name; must match /^[a-z][a-zA-Z0-9_]*$/ (max 64 chars)
configstringYesThe api-request script string (non-empty)
descriptionstringNoTool description
typestringNoTool type (default api-request)
parametersobjectNoJSON Schema for tool parameters
sortOrderintegerNoDisplay order (default 0)
needsApprovalbooleanNoRequire 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.

Add a custom toolPOST /api/toolkits/00000000-0000-4000-8000-000000000001/tools
POST /api/toolkits/{id}/tools. `config` is REQUIRED (the axios-config script string), not optional. `name` must match /^[a-z][a-zA-Z0-9_]*$/ (max 64). `type` defaults to "api-request", `sortOrder` to 0, `needsApproval` to false. Payload is stripUnknown; toolkitId comes from the URL. The handler returns the created tool with status 200 (it does NOT use .created()/201). Requires permission.toolkit.write (data wizard).
Request body
{
"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 · 200
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

FieldTypeRequiredDescription
toolsarrayYesDiscovered tools to create
discoveredBystringNoDiscovery 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.

Sync discovered MCP tools (destructive)PUT /api/toolkits/00000000-0000-4000-8000-000000000001/tools
PUT /api/toolkits/{id}/tools is the DESTRUCTIVE discovery sync: it deletes ALL existing tools for the toolkit, then bulk-creates from the `tools` array. Each tool is forced to type "mcp"; `parameters` is taken from `inputSchema` (falling back to `parameters`); `sortOrder` is the array index; `discoveredBy` defaults to "unknown" (not "server"); `discoveredAt` is set to now. Returns { items, discoveredAt } with status 200. Used by mcp-remote (server) and mcp-stdio (GO client) discovery.
Request body
{
"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"
}
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.
Destructive operation

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.

Get a single toolGET /api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002
GET /api/toolkits/{id}/tools/{toolId} returns the one ToolkitTool, scoped to the parent toolkit. A toolId not in this toolkit 404s ("Tool not found"). Requires toolkit read access (AI-gated via toolkit.lookup).
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Update a toolPUT /api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002
PUT /api/toolkits/{id}/tools/{toolId} updates the named fields (all optional) and re-reads the row. When `config` is present it must be a non-empty string (min 1); an empty string or null 400s. Payload is stripUnknown. Returns the updated tool (200). Requires permission.toolkit.write.
Request body
{
"description": "Create a new GitHub issue with labels",
"needsApproval": true
}
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Delete a toolDELETE /api/toolkits/00000000-0000-4000-8000-000000000001/tools/00000000-0000-4000-8000-000000000002
DELETE /api/toolkits/{id}/tools/{toolId} destroys the tool. The handler returns null, so the response is status 200 with an EMPTY body (Informer convention), NOT 204. A toolId not in this toolkit 404s. Requires permission.toolkit.write.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

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.