Adding a New Tool
1. Create the tool file
Create src/tools/<category>/myNewTool.ts:
import { z } from 'zod';
import { makeAuthenticatedRequest, createToolResponse } from '../../utils/apiHelpers.js';
import { formatSuccess } from '../../utils/responseHelpers.js';
const aicBaseUrl = process.env.AIC_BASE_URL;
const SCOPES = ['<required-oauth-scopes>'];
export const myNewToolTool = {
name: 'myNewTool',
title: 'My New Tool',
description: 'What the tool does',
scopes: SCOPES,
annotations: {
readOnlyHint: true,
openWorldHint: true
},
inputSchema: {
param1: z.string().describe('Description of param1'),
param2: z.number().optional().describe('Optional parameter')
},
async toolFunction({ param1, param2 }: { param1: string; param2?: number }) {
const url = `https://${aicBaseUrl}/your/api/endpoint`;
try {
const { data, response } = await makeAuthenticatedRequest(url, SCOPES, {
method: 'GET'
});
return createToolResponse(formatSuccess(data, response));
} catch (error: any) {
return createToolResponse(`Failed to do thing: ${error.message}`);
}
}
};
2. Export from the category index
Add to src/tools/<category>/index.ts:
export { myNewToolTool } from './myNewTool.js';
The tool auto-registers — toolHelpers.ts collects via Object.values() on each category module.
Response Formatting
formatSuccess(data, response) accepts objects or strings — it handles JSON serialization internally. Always pass the response object from makeAuthenticatedRequest so the transaction ID (x-forgerock-transactionid header) is automatically appended. Transaction IDs are critical for tracing tool calls back to AIC API requests.
For write operations returning 204 (DELETE, some PUT), there's no response body to pass to formatSuccess. Manually extract the transaction ID:
const { response } = await makeAuthenticatedRequest(url, SCOPES, { method: 'DELETE' });
const transactionId = response.headers.get('x-forgerock-transactionid') || 'unknown';
return createToolResponse(`Resource deleted successfully.\nTransaction ID: ${transactionId}`);
Annotations
Every tool should have an annotations object describing its behavior to MCP clients. These are hints that guide client behavior (e.g., requiring confirmation, enabling retries).
| Annotation | Meaning | Default |
|---|---|---|
readOnlyHint |
Tool only reads data, does not modify state | false |
destructiveHint |
Tool may perform destructive updates (only meaningful when readOnlyHint is false) |
true |
idempotentHint |
Calling repeatedly with the same arguments has no additional effect (only meaningful when readOnlyHint is false) |
false |
openWorldHint |
Tool may interact with an open world of external entities (e.g., web search). If false, the domain of interaction is closed (e.g., a memory tool) |
true |
Common combinations used in this codebase:
- Read-only (GET, list, query):
{ readOnlyHint: true, openWorldHint: true } - Create:
{ openWorldHint: true } - Update (idempotent):
{ idempotentHint: true, openWorldHint: true } - Update (non-idempotent):
{ openWorldHint: true } - Delete:
{ destructiveHint: true, openWorldHint: true }
Key Conventions
- Define
SCOPESas a module-level constant — reference it in both the tool object andmakeAuthenticatedRequest()calls - Use
makeAuthenticatedRequest+createToolResponsehelpers, not rawfetch - Use
safePathSegmentSchemafromvalidationHelpersfor any user-provided ID that goes into a URL path - Use
z.enum(REALMS)fromvalidationHelpersfor realm parameters - Export name convention:
<toolName>Tool(e.g.,deleteManagedObjectTool)
Adding a New Tool Category
If the tool doesn't fit an existing category (managedObjects, themes, esv, logs, am):
- Create
src/tools/<newCategory>/with your tool files - Create
src/tools/<newCategory>/index.tsre-exporting all tools - Wire it into
src/utils/toolHelpers.ts:- Add
import * as newCategoryTools from '../tools/<newCategory>/index.js'; - Add
...(Object.values(newCategoryTools) as Tool[])to the tools array ingetAllTools() - If the category requires browser-based auth (like AM tools), add it inside the
!isDockerModeguard instead
- Add