diff --git a/docs/tool-annotations.md b/docs/tool-annotations.md index 1a8275a..4d9f478 100644 --- a/docs/tool-annotations.md +++ b/docs/tool-annotations.md @@ -1,15 +1,50 @@ # Angie Tool Annotations -Angie exposes a set of annotation constants and interfaces that MCP tool authors can use to attach Angie-specific metadata to their tools. These are exported directly from `@elementor/angie-sdk`. +Angie exposes a set of annotation constants and interfaces that MCP tool authors can use to attach metadata to their tools. These are exported directly from `@elementor/angie-sdk`. -## Available Annotations +## Annotations vs `_meta` + +The MCP protocol distinguishes between two types of tool metadata: + +| Location | Purpose | Interface | +|---|---|---| +| `annotations` | **Standard MCP annotations** — recognized by the MCP protocol | `AngieToolAnnotations` | +| `_meta` | **Custom Angie metadata** — vendor-specific extensions | `AngieToolMeta` | + +### Standard Annotations (`annotations`) + +| Field | Type | Purpose | +|---|---|---| +| `title` | `string` | Human-readable title for the tool | +| `readOnlyHint` | `boolean` | Mark a tool as read-only | +| `destructiveHint` | `boolean` | Mark a tool as potentially destructive | + +### Custom Angie Metadata (`_meta`) | Constant | Value | Purpose | |---|---|---| | `ANGIE_REQUIRED_RESOURCES` | `'angie/requiredResources'` | Declare resources the tool needs | | `ANGIE_MODEL_PREFERENCES` | `'angie/modelPreferences'` | Request a specific AI model | | `ANGIE_EXTENDED_TIMEOUT` | `'angie/extendedTimeout'` | Request a longer execution timeout | -| `MCP_READONLY` | `'readOnlyHint'` | Mark a tool as read-only | + +--- + +## Tool Registration API + +Use `server.registerTool()` to register tools with the MCP server: + +```typescript +server.registerTool( + 'tool-name', + { + description: 'Tool description', + inputSchema: { /* zod schema */ }, + annotations: { /* standard MCP annotations */ }, + _meta: { /* custom Angie metadata */ }, + }, + async (args) => { /* handler */ } +); +``` --- @@ -28,20 +63,22 @@ interface AngieRequiredResource { **Example:** ```typescript -import { ANGIE_REQUIRED_RESOURCES, ToolAnnotations } from '@elementor/angie-sdk'; +import { ANGIE_REQUIRED_RESOURCES, AngieToolMeta } from '@elementor/angie-sdk'; -server.tool( +server.registerTool( 'update-page-styles', - 'Updates the CSS styles for the current page', - { /* input schema */ }, { - [ANGIE_REQUIRED_RESOURCES]: [ - { - uri: 'elementor://page/styles', - whenToUse: 'Always — needed to read current page styles before updating', - } - ] - } as ToolAnnotations, + description: 'Updates the CSS styles for the current page', + inputSchema: { /* ... */ }, + _meta: { + [ANGIE_REQUIRED_RESOURCES]: [ + { + uri: 'elementor://page/styles', + whenToUse: 'Always — needed to read current page styles before updating', + } + ] + } as AngieToolMeta, + }, async (args) => { /* handler */ } ); ``` @@ -72,21 +109,23 @@ Angie resolves the model in this order: **Example:** ```typescript -import { ANGIE_MODEL_PREFERENCES, ToolAnnotations } from '@elementor/angie-sdk'; +import { ANGIE_MODEL_PREFERENCES, AngieToolMeta } from '@elementor/angie-sdk'; -server.tool( +server.registerTool( 'generate-custom-css', - 'Generates CSS code based on design requirements', - { /* input schema */ }, { - [ANGIE_MODEL_PREFERENCES]: { - hints: [ - { name: 'claude-sonnet' }, // First choice - { name: 'gpt-4.1' } // Fallback - ], - intelligencePriority: 0.9 // Optional: for future use - } - } as ToolAnnotations, + description: 'Generates CSS code based on design requirements', + inputSchema: { /* ... */ }, + _meta: { + [ANGIE_MODEL_PREFERENCES]: { + hints: [ + { name: 'claude-sonnet' }, // First choice + { name: 'gpt-4.1' } // Fallback + ], + intelligencePriority: 0.9 // Optional: for future use + } + } as AngieToolMeta, + }, async (args) => { /* handler */ } ); ``` @@ -108,39 +147,41 @@ interface AngieExtendedTimeout { **Example:** ```typescript -import { ANGIE_EXTENDED_TIMEOUT, ToolAnnotations } from '@elementor/angie-sdk'; +import { ANGIE_EXTENDED_TIMEOUT, AngieToolMeta } from '@elementor/angie-sdk'; -server.tool( +server.registerTool( 'bulk-update-elements', - 'Updates all elements on the page in one operation', - { /* input schema */ }, { - [ANGIE_EXTENDED_TIMEOUT]: { - timeoutMs: 60000 // 60 seconds - } - } as ToolAnnotations, + description: 'Updates all elements on the page in one operation', + inputSchema: { /* ... */ }, + _meta: { + [ANGIE_EXTENDED_TIMEOUT]: { + timeoutMs: 60000 // 60 seconds + } + } as AngieToolMeta, + }, async (args) => { /* handler */ } ); ``` --- -## `MCP_READONLY` +## `readOnlyHint` (Standard MCP Annotation) Mark a tool as read-only. Angie uses this hint to understand that the tool does not mutate any state, which can affect planning and user confirmation flows. **Example:** ```typescript -import { MCP_READONLY, ToolAnnotations } from '@elementor/angie-sdk'; - -server.tool( +server.registerTool( 'get-page-structure', - 'Returns the structure of the current page', - { /* input schema */ }, { - [MCP_READONLY]: true - } as ToolAnnotations, + description: 'Returns the structure of the current page', + inputSchema: { /* ... */ }, + annotations: { + readOnlyHint: true + }, + }, async (args) => { /* handler */ } ); ``` @@ -149,35 +190,38 @@ server.tool( ## Using Multiple Annotations Together -All annotations can be combined on a single tool: +Standard annotations and custom Angie metadata can be combined on a single tool: ```typescript import { ANGIE_REQUIRED_RESOURCES, ANGIE_MODEL_PREFERENCES, ANGIE_EXTENDED_TIMEOUT, - MCP_READONLY, - ToolAnnotations, + AngieToolMeta, } from '@elementor/angie-sdk'; -server.tool( +server.registerTool( 'analyze-page-layout', - 'Analyzes the current page layout and returns suggestions', - { /* input schema */ }, { - [MCP_READONLY]: true, - [ANGIE_EXTENDED_TIMEOUT]: { timeoutMs: 30000 }, - [ANGIE_REQUIRED_RESOURCES]: [ - { - uri: 'elementor://page/layout', - whenToUse: 'Always — needed to read the page structure', + description: 'Analyzes the current page layout and returns suggestions', + inputSchema: { /* ... */ }, + annotations: { + readOnlyHint: true, + }, + _meta: { + [ANGIE_EXTENDED_TIMEOUT]: { timeoutMs: 30000 }, + [ANGIE_REQUIRED_RESOURCES]: [ + { + uri: 'elementor://page/layout', + whenToUse: 'Always — needed to read the page structure', + } + ], + [ANGIE_MODEL_PREFERENCES]: { + hints: [{ name: 'claude-sonnet' }], + intelligencePriority: 0.9 } - ], - [ANGIE_MODEL_PREFERENCES]: { - hints: [{ name: 'claude-sonnet' }], - intelligencePriority: 0.9 - } - } as ToolAnnotations, + } as AngieToolMeta, + }, async (args) => { /* handler */ } ); ``` diff --git a/example/angie-demo-plugin/src/demo-mcp-server.ts b/example/angie-demo-plugin/src/demo-mcp-server.ts index abf469b..8a2df8c 100644 --- a/example/angie-demo-plugin/src/demo-mcp-server.ts +++ b/example/angie-demo-plugin/src/demo-mcp-server.ts @@ -58,11 +58,16 @@ function createSeoMcpServer() { } ); - server.tool( + server.registerTool( 'analyze-page-seo', - 'Analyzes the SEO of the current page including meta tags, headings, and content structure', { - url: z.string().describe( 'The URL of the page to analyze' ), + description: 'Analyzes the SEO of the current page including meta tags, headings, and content structure', + inputSchema: { + url: z.string().describe( 'The URL of the page to analyze' ), + }, + annotations: { + readOnlyHint: true, + }, }, async ( { url }: { url: string } ) => { const response = await makeApiRequest( 'angie-demo/v1/analyze-page-seo', { url } ); @@ -72,14 +77,17 @@ function createSeoMcpServer() { text: JSON.stringify( response, null, 2 ), } ], }; - } ); + } + ); - server.tool( + server.registerTool( 'manage-post-types', - 'Manages post types with Angie', { - postType: z.string().describe( 'The post type to register' ), - action: z.enum( [ 'register', 'unregister' ] ).describe( 'The action to perform' ), + description: 'Manages post types with Angie', + inputSchema: { + postType: z.string().describe( 'The post type to register' ), + action: z.enum( [ 'register', 'unregister' ] ).describe( 'The action to perform' ), + }, }, async ( { postType, action }: { postType: string, action: string } ) => { const response = await makeApiRequest( 'angie-demo/v1/post-types', { postType, action } ); @@ -89,12 +97,18 @@ function createSeoMcpServer() { text: JSON.stringify( response, null, 2 ), } ], }; - } ); + } + ); - server.tool( + server.registerTool( 'security-check', - 'Checks the security of current WordPress installation', - {}, + { + description: 'Checks the security of current WordPress installation', + inputSchema: {}, + annotations: { + readOnlyHint: true, + }, + }, async () => { const response = await makeApiRequest( 'angie-demo/v1/security-check', {} ); return { @@ -103,11 +117,15 @@ function createSeoMcpServer() { text: JSON.stringify( response, null, 2 ), } ], }; - } ); + } + ); - server.tool( 'run-fireworks', - 'Creates a celebratory fireworks display effect on the current screen. Use this when you want to add visual excitement or celebrate a successful action. The tool will create a full-screen canvas overlay with animated fireworks that automatically stop after 5 seconds.', - {}, + server.registerTool( + 'run-fireworks', + { + description: 'Creates a celebratory fireworks display effect on the current screen. Use this when you want to add visual excitement or celebrate a successful action. The tool will create a full-screen canvas overlay with animated fireworks that automatically stop after 5 seconds.', + inputSchema: {}, + }, async () => { try { // Create canvas element if it doesn't exist @@ -161,7 +179,8 @@ function createSeoMcpServer() { } ], }; } - } ); + } + ); return server; } diff --git a/src/angie-annotations.test.ts b/src/angie-annotations.test.ts index 755c544..96c9fae 100644 --- a/src/angie-annotations.test.ts +++ b/src/angie-annotations.test.ts @@ -4,6 +4,8 @@ import { ANGIE_MODEL_PREFERENCES, ANGIE_EXTENDED_TIMEOUT, MCP_READONLY, + AngieToolMeta, + AngieToolAnnotations, } from './angie-annotations'; describe('angie-annotations', () => { @@ -22,4 +24,26 @@ describe('angie-annotations', () => { it('MCP_READONLY has the correct value', () => { expect(MCP_READONLY).toBe('readOnlyHint'); }); + + it('AngieToolMeta interface accepts valid custom metadata', () => { + const meta: AngieToolMeta = { + [ANGIE_REQUIRED_RESOURCES]: [{ uri: 'resource://test', whenToUse: 'always' }], + [ANGIE_MODEL_PREFERENCES]: { intelligencePriority: 0.8 }, + [ANGIE_EXTENDED_TIMEOUT]: { timeoutMs: 60000 }, + }; + expect(meta[ANGIE_REQUIRED_RESOURCES]).toHaveLength(1); + expect(meta[ANGIE_MODEL_PREFERENCES]?.intelligencePriority).toBe(0.8); + expect(meta[ANGIE_EXTENDED_TIMEOUT]?.timeoutMs).toBe(60000); + }); + + it('AngieToolAnnotations interface accepts standard MCP annotations', () => { + const annotations: AngieToolAnnotations = { + title: 'My Tool', + readOnlyHint: true, + destructiveHint: false, + }; + expect(annotations.title).toBe('My Tool'); + expect(annotations.readOnlyHint).toBe(true); + expect(annotations.destructiveHint).toBe(false); + }); }); diff --git a/src/angie-annotations.ts b/src/angie-annotations.ts index 471c061..c8a1f4a 100644 --- a/src/angie-annotations.ts +++ b/src/angie-annotations.ts @@ -19,3 +19,34 @@ export interface AngieModelPreferences { export interface AngieExtendedTimeout { timeoutMs: number; } + +/** + * Custom Angie metadata to be placed in the `_meta` field of a tool. + * These are vendor-specific extensions and should NOT be placed in `annotations`. + * + * @example + * server.registerTool('my-tool', { + * description: 'My tool description', + * inputSchema: { ... }, + * annotations: { readOnlyHint: true }, // Standard MCP annotations + * _meta: { + * [ANGIE_REQUIRED_RESOURCES]: [{ uri: 'resource://...', whenToUse: '...' }], + * [ANGIE_MODEL_PREFERENCES]: { intelligencePriority: 0.8 }, + * } + * }, handler); + */ +export interface AngieToolMeta { + [ANGIE_REQUIRED_RESOURCES]?: AngieRequiredResource[]; + [ANGIE_MODEL_PREFERENCES]?: AngieModelPreferences; + [ANGIE_EXTENDED_TIMEOUT]?: AngieExtendedTimeout; +} + +/** + * Standard MCP tool annotations. + * Use these in the `annotations` field of a tool. + */ +export interface AngieToolAnnotations { + title?: string; + destructiveHint?: boolean; + readOnlyHint?: boolean; +}