Files
flynn/docs/api/TOOLS.md
T
2026-02-26 14:06:53 -08:00

1406 lines
31 KiB
Markdown

# Tools API Documentation
Flynn's tool framework provides a standardized interface for capabilities that can be invoked by AI agents. This document describes the tool contracts, schemas, and implementation patterns.
## Table of Contents
- [Overview](#overview)
- [Tool Interface](#tool-interface)
- [Input Schema Format](#input-schema-format)
- [Result Format](#result-format)
- [Tool Patterns](#tool-patterns)
- [Tool Registration](#tool-registration)
- [Tool Policy](#tool-policy)
- [Tool Execution Flow](#tool-execution-flow)
- [Builtin Tools Reference](#builtin-tools-reference)
- [MCP Tools](#mcp-tools)
- [Example Implementation](#example-implementation)
## Overview
Tools are executable capabilities that the AI agent can call to perform actions beyond text generation.
### Tool Categories
- **File System**: `file.read`, `file.write`, `file.edit`, `file.list`
- **Shell/Process**: `shell.exec`, `process.start`, `process.kill`
- **Web**: `web.fetch`, `web.search`
- **Browser**: `browser.navigate`, `browser.screenshot`, `browser.click`, `browser.type`, `browser.content`, `browser.wait_for`, `browser.assert`, `browser.extract`, `browser.checkpoint.save`, `browser.checkpoint.resume`, `browser.eval`, `browser.evaluate` (alias of `browser.eval`)
- **Memory**: `memory.read`, `memory.write`, `memory.search`
- **MinIO**: `minio.share`, `minio.ingest`, `minio.sync`
- **Kubernetes**: `k8s.pods`, `k8s.deployments`, `k8s.logs`
- **Media**: `media.send`, `image.analyze`, `audio.transcribe`
- **System**: `system.info`
- **Session**: `sessions.list`, `sessions.delete`
### Tool Benefits
- **Modular**: Easy to add, remove, or modify tools
- **Secure**: Policy-based access control with hooks
- **Sandboxed**: Optional Docker isolation
- **Observable**: Tool execution events streamed to clients
- **Type-safe**: JSON Schema validation for inputs
## Tool Interface
### Definition
```typescript
export interface Tool {
/** Unique tool name (e.g., 'shell.exec', 'file.read'). */
name: string;
/** Human-readable description (shown to AI model). */
description: string;
/** JSON Schema for input validation. */
inputSchema: JSONSchema;
/** Secret scopes required to execute this tool (optional). */
requiredSecretScopes?: string[];
/** Optional execution context (abort signal, runtime metadata). */
execute: (args: unknown, context?: ToolExecutionContext) => Promise<ToolResult>;
}
export interface ToolExecutionContext {
signal?: AbortSignal;
}
```
`signal` is wired to run-level cancellation (`agent.cancel`, `/stop`, `/cancel`) and tool timeout. Tools should honor it for fast cooperative aborts.
### ToolResult
```typescript
export interface ToolResult {
/** Whether execution succeeded. */
success: boolean;
/** Text output (shown to model and user). */
output: string;
/** Error message if execution failed. */
error?: string;
}
```
## Input Schema Format
Tools use JSON Schema for input validation. This schema is both:
1. Validated by Flynn before tool execution
2. Sent to the AI model for automatic parameter generation
### Schema Structure
```typescript
export interface JSONSchema {
type: 'object' | 'array' | 'string' | 'number' | 'boolean';
properties?: Record<string, JSONSchema>;
required?: string[];
additionalProperties?: boolean;
description?: string;
enum?: unknown[];
items?: JSONSchema;
}
```
### Example Schemas
#### Simple String Parameter
```typescript
{
type: 'object',
properties: {
message: {
type: 'string',
description: 'The message to display'
}
},
required: ['message']
}
```
#### Multiple Parameters with Options
```typescript
{
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path to read'
},
encoding: {
type: 'string',
enum: ['utf8', 'base64'],
description: 'File encoding'
}
},
required: ['path']
}
```
#### Nested Object
```typescript
{
type: 'object',
properties: {
command: {
type: 'object',
properties: {
program: {
type: 'string',
description: 'Program to execute'
},
args: {
type: 'array',
items: { type: 'string' },
description: 'Command arguments'
}
},
required: ['program']
}
},
required: ['command']
}
```
### Schema Best Practices
1. **Use descriptions**: The AI model uses descriptions to understand parameters
2. **Be specific**: Narrow types (e.g., `enum` instead of `string`)
3. **Provide defaults**: Use optional parameters with sensible defaults
4. **Document constraints**: Include min/max, patterns, or enum values
## Result Format
### Success Result
```typescript
{
success: true,
output: "Tool executed successfully. Result: ..."
}
```
### Error Result
```typescript
{
success: false,
output: "",
error: "File not found: /path/to/file"
}
```
### Output Guidelines
- **Keep output concise**: The AI model has limited context
- **Use structured data**: JSON or table format for complex results
- **Include metadata**: Timestamps, counts, file paths
- **Handle large outputs**: Truncate or use file operations
## Tool Patterns
Flynn supports three tool implementation patterns:
### Pattern 1: Static Tool (No Dependencies)
Use for tools that don't need external dependencies.
```typescript
// src/tools/builtin/shell.ts
import type { Tool, ToolResult } from '../types.js';
interface ShellExecArgs {
command: string;
cwd?: string;
timeout?: number;
}
export const shellExecTool: Tool = {
name: 'shell.exec',
description: 'Execute a shell command and return stdout/stderr',
inputSchema: {
type: 'object',
properties: {
command: {
type: 'string',
description: 'The shell command to execute'
},
cwd: {
type: 'string',
description: 'Working directory (optional)'
},
timeout: {
type: 'number',
description: 'Timeout in milliseconds (default 30000)'
}
},
required: ['command']
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as ShellExecArgs;
const timeout = args.timeout ?? 30_000;
// Implementation
const result = await executeShell(args.command, args.cwd, timeout);
return {
success: true,
output: result.stdout
};
}
};
```
**Registration:**
```typescript
// src/tools/builtin/index.ts
export const allBuiltinTools: Tool[] = [
shellExecTool,
// ... other static tools
];
```
### Pattern 2: Factory Tool (Single Tool with Dependencies)
Use for tools that need injected dependencies (e.g., memory store, HTTP client).
```typescript
// src/tools/builtin/memory-read.ts
import type { Tool, ToolResult } from '../types.js';
import type { MemoryStore } from '../../memory/store.js';
interface MemoryReadArgs {
namespace: string;
}
export function createMemoryReadTool(store: MemoryStore): Tool {
return {
name: 'memory.read',
description: 'Read memory from a namespace',
inputSchema: {
type: 'object',
properties: {
namespace: {
type: 'string',
description: 'Memory namespace to read from'
}
},
required: ['namespace']
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as MemoryReadArgs;
try {
const content = store.read(args.namespace);
return {
success: true,
output: content
};
} catch (error) {
return {
success: false,
output: '',
error: error instanceof Error ? error.message : String(error)
};
}
}
};
}
```
**Registration:**
```typescript
// src/daemon/index.ts
const memoryReadTool = createMemoryReadTool(memoryStore);
toolRegistry.register(memoryReadTool);
```
### Pattern 3: Multi-Factory (Related Tool Set)
Use for tools that share a common dependency or manager.
```typescript
// src/tools/builtin/browser/tools.ts
import type { Tool, ToolResult } from '../../types.js';
import type { BrowserManager } from './manager.js';
export function createBrowserTools(manager: BrowserManager, options?: BrowserToolsOptions): Tool[] {
return [
{
name: 'browser.navigate',
description: 'Navigate to a URL in the browser',
inputSchema: {
type: 'object',
properties: {
url: {
type: 'string',
description: 'URL to navigate to'
}
},
required: ['url']
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as { url: string };
await manager.navigate(args.url);
return {
success: true,
output: `Navigated to ${args.url}`
};
}
},
{
name: 'browser.screenshot',
description: 'Take a screenshot of the current page',
inputSchema: {
type: 'object',
properties: {}
},
execute: async (): Promise<ToolResult> => {
const screenshot = await manager.screenshot();
return {
success: true,
output: 'Screenshot taken'
};
}
}
];
}
```
**Registration:**
```typescript
// src/daemon/index.ts
const browserTools = createBrowserTools(browserManager);
browserTools.forEach(tool => toolRegistry.register(tool));
```
## Tool Registration
### Registration Chain
1. Tool implementation file
2. `src/tools/builtin/index.ts` (exports for static tools)
3. `src/tools/index.ts` (re-exports)
4. `src/daemon/index.ts` (registers with ToolRegistry)
### ToolRegistry
```typescript
class ToolRegistry {
/** Register a tool. Throws if tool name already exists. */
register(tool: Tool): void;
/** Unregister a tool by name. */
unregister(name: string): void;
/** Get a tool by name. */
get(name: string): Tool | undefined;
/** Get tool by API name (handles MCP prefixing). */
getByApiName(name: string): Tool | undefined;
/** List all registered tools. */
list(): Tool[];
/** Check if a tool is registered. */
has(name: string): boolean;
/** Set tool policy. */
setPolicy(policy: ToolPolicy): void;
/** Get current tool policy. */
getPolicy(): ToolPolicy | undefined;
}
```
### Registration Example
```typescript
// src/daemon/index.ts
import { ToolRegistry } from './tools/registry.js';
import { allBuiltinTools } from './tools/builtin/index.js';
import { createMemoryReadTool } from './tools/builtin/memory-read.js';
// Create registry
const toolRegistry = new ToolRegistry();
// Register static tools
for (const tool of allBuiltinTools) {
toolRegistry.register(tool);
}
// Register factory tools
const memoryReadTool = createMemoryReadTool(memoryStore);
toolRegistry.register(memoryReadTool);
// Register multi-factory tools
const browserTools = createBrowserTools(browserManager);
for (const tool of browserTools) {
toolRegistry.register(tool);
}
```
## Tool Policy
Tool policy controls which tools are available to agents based on profiles and patterns.
### Profiles
Flynn ships 4 built-in profiles:
- `minimal`: read-only (file read/list + web.fetch + system.info)
- `messaging`: read-only + web search + memory + connected read APIs (gmail/gcal/gdocs/gdrive/gtasks)
- `coding`: adds filesystem writes, shell/process, and browser automation
- `full`: all registered tools
The authoritative profile tool sets live in `src/tools/policy.ts`.
### Groups
Tools are organized into groups:
- `group:fs`: File system tools
- `group:runtime`: Process/execution tools
- `group:web`: Web and browser tools
- `group:memory`: Memory and search tools
There are additional groups for specific integrations (gmail/gcal/gdocs/gdrive/gtasks/cron/minio/k8s). See `TOOL_GROUPS` in `src/tools/policy.ts`.
### Policy Resolution
When resolving tools for an execution context:
1. Start with global `tools.profile`
2. Apply global `tools.allow` (adds tools back in)
3. Apply global `tools.deny` (deny always wins)
4. If `context.agent` override exists, intersect with agent override resolution
5. If `context.provider` override exists, intersect with provider override resolution
6. If `context.skillName` is set, intersect with skill capability allowlist (deny-by-default for skills)
Hooks/autonomy are enforced at execution-time (ToolExecutor), not during list resolution.
### Example Policy Config
```yaml
tools:
profile: messaging
allow: []
deny: ["browser.*"]
# Per-agent overrides
agents:
fast:
profile: minimal
allow: []
deny: []
# Per-provider overrides
providers:
ollama:
profile: messaging
allow: []
deny: ["web.search"]
```
### Skill Capabilities (Skill Context)
If a request is routed into a skill context (via intents), Flynn applies an additional restriction layer using the skill's `manifest.json.permissions`.
- A skill with no `permissions` manifest has no tool access.
- `permissions.tools` (explicit allowlist) overrides `permissions.tool_groups`.
See `docs/security/SAFE_PERSONAL_AGENT.md`.
## Tool Execution Flow
### Execution Pipeline
```
Agent Request
Model Generates Tool Call
ToolExecutor.execute()
Policy Check (is tool allowed?)
Hook Check (confirm/log/silent)
Tool.execute(args)
Result Returned
Agent Continues
```
### Policy Check
```typescript
const policy = this.registry.getPolicy();
if (policy) {
const allNames = this.registry.list().map(t => t.name);
if (!policy.isAllowed(toolName, allNames, context)) {
return {
success: false,
output: '',
error: `Tool '${toolName}' is not allowed by tool policy`
};
}
}
```
### Hook Check
```typescript
const baseAction = this.hooks.getAction(toolName);
const autonomyLevel = context?.autonomyLevel ?? 'standard';
const autonomyDecision = resolveAutonomy(toolName, baseAction, autonomyLevel);
if (autonomyDecision.action === 'confirm') {
// Emit confirmation event, wait for user response
const confirmed = await waitForConfirmation(toolName, args);
if (!confirmed) {
return {
success: false,
output: '',
error: 'Tool execution cancelled by user'
};
}
}
```
### Timeout Handling
```typescript
const timeout = args.timeout ?? this.defaultTimeoutMs;
const result = await Promise.race([
tool.execute(args),
new Promise<ToolResult>((_, reject) => {
setTimeout(() => {
reject(new Error(`Tool '${toolName}' timed out after ${timeout}ms`));
}, timeout);
})
]);
```
### Output Truncation
```typescript
if (result.output.length > this.maxOutputBytes) {
result.output = result.output.slice(0, this.maxOutputBytes) +
`\n[Output truncated - ${result.output.length - this.maxOutputBytes} bytes hidden]`;
}
```
## Builtin Tools Reference
### File System Tools
#### `file.read`
Read a file's contents.
```json
{
"name": "file.read",
"description": "Read the contents of a file",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
},
"encoding": {
"type": "string",
"enum": ["utf8", "base64"],
"description": "File encoding (default: utf8)"
}
},
"required": ["path"]
}
}
```
#### `file.write`
Write or overwrite a file.
```json
{
"name": "file.write",
"description": "Write content to a file (creates if not exists)",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to write"
},
"content": {
"type": "string",
"description": "Content to write"
}
},
"required": ["path", "content"]
}
}
```
#### `file.edit`
Apply a patch to a file.
```json
{
"name": "file.edit",
"description": "Apply an edit to a file using search/replace",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit"
},
"search": {
"type": "string",
"description": "Text to search for"
},
"replace": {
"type": "string",
"description": "Replacement text"
}
},
"required": ["path", "search", "replace"]
}
}
```
#### `file.list`
List files in a directory.
```json
{
"name": "file.list",
"description": "List files and directories in a path",
"inputSchema": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Directory path to list (default: current directory)"
},
"recursive": {
"type": "boolean",
"description": "List recursively (default: false)"
}
},
"required": []
}
}
```
### Shell/Process Tools
#### `shell.exec`
Execute a shell command.
```json
{
"name": "shell.exec",
"description": "Execute a shell command and return stdout/stderr",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
},
"cwd": {
"type": "string",
"description": "Working directory (optional)"
},
"timeout": {
"type": "number",
"description": "Timeout in milliseconds (default 30000)"
}
},
"required": ["command"]
}
}
```
#### `process.start`
Start a background process.
```json
{
"name": "process.start",
"description": "Start a background process",
"inputSchema": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command to execute"
},
"args": {
"type": "array",
"items": { "type": "string" },
"description": "Command arguments"
},
"cwd": {
"type": "string",
"description": "Working directory"
}
},
"required": ["command"]
}
}
```
#### `process.status`
Check status of a background process.
```json
{
"name": "process.status",
"description": "Get the status of a background process",
"inputSchema": {
"type": "object",
"properties": {
"pid": {
"type": "number",
"description": "Process ID"
}
},
"required": ["pid"]
}
}
```
#### `process.kill`
Kill a background process.
```json
{
"name": "process.kill",
"description": "Kill a background process",
"inputSchema": {
"type": "object",
"properties": {
"pid": {
"type": "number",
"description": "Process ID"
},
"signal": {
"type": "string",
"enum": ["SIGTERM", "SIGKILL"],
"description": "Signal to send (default: SIGTERM)"
}
},
"required": ["pid"]
}
}
```
### Web Tools
#### `web.fetch`
Fetch a webpage or API endpoint.
```json
{
"name": "web.fetch",
"description": "Fetch content from a URL",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to fetch"
},
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "DELETE"],
"description": "HTTP method (default: GET)"
},
"headers": {
"type": "object",
"description": "HTTP headers"
},
"body": {
"type": "string",
"description": "Request body"
},
"timeout": {
"type": "number",
"description": "Timeout in milliseconds (default 10000)"
}
},
"required": ["url"]
}
}
```
#### `web.search`
Search the web.
```json
{
"name": "web.search",
"description": "Search the web and return results",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"count": {
"type": "number",
"description": "Number of results to return (default: 5, max: 20)"
},
"country": {
"type": "string",
"description": "Optional country code (Brave provider)"
},
"searchLang": {
"type": "string",
"description": "Optional language code (Brave provider)"
},
"safeSearch": {
"type": "string",
"description": "Brave-only safesearch mode: off | moderate | strict"
},
"freshness": {
"type": "string",
"description": "Brave freshness filter: pd | pw | pm | py"
}
},
"required": ["query"]
}
}
```
#### `web.search.news`
Search Brave News results (available when `web_search.provider: brave`).
```json
{
"name": "web.search.news",
"description": "Search Brave News and return results",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "News search query"
},
"count": {
"type": "number",
"description": "Number of results to return (default: 5, max: 20)"
},
"country": {
"type": "string",
"description": "Optional country code"
},
"searchLang": {
"type": "string",
"description": "Optional language code"
},
"freshness": {
"type": "string",
"description": "Freshness filter: pd | pw | pm | py"
}
},
"required": ["query"]
}
}
```
### Memory Tools
#### `memory.read`
Read memory from a namespace.
```json
{
"name": "memory.read",
"description": "Read memory from a namespace",
"inputSchema": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Memory namespace to read from"
}
},
"required": ["namespace"]
}
}
```
#### `memory.write`
Write memory to a namespace.
```json
{
"name": "memory.write",
"description": "Write memory to a namespace",
"inputSchema": {
"type": "object",
"properties": {
"namespace": {
"type": "string",
"description": "Memory namespace to write to"
},
"content": {
"type": "string",
"description": "Content to write"
}
},
"required": ["namespace", "content"]
}
}
```
#### `memory.search`
Search memory using hybrid (keyword + vector) search.
```json
{
"name": "memory.search",
"description": "Search memory using hybrid search",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query"
},
"namespace": {
"type": "string",
"description": "Namespace to search in (optional)"
},
"limit": {
"type": "number",
"description": "Number of results to return (default: 10)"
},
"keywordOnly": {
"type": "boolean",
"description": "Use keyword search only (no vector search)"
}
},
"required": ["query"]
}
}
```
### MinIO Tools
Runtime extractor requirements for binary document ingestion:
- PDF (`.pdf`): requires `pdftotext`
- DOCX (`.docx`): requires either `pandoc` or `docx2txt`
#### `minio.share`
Upload a local file to MinIO and return a temporary presigned download URL.
#### `minio.ingest`
Read a text-like object from MinIO (and PDF/DOCX via local extraction tools when available) and write it into a memory namespace.
```json
{
"name": "minio.ingest",
"description": "Read a text-like object from MinIO and ingest it into memory namespace for later retrieval/search.",
"inputSchema": {
"type": "object",
"properties": {
"object_key": {
"type": "string",
"description": "Object key in MinIO bucket"
},
"bucket": {
"type": "string",
"description": "Optional bucket override"
},
"namespace": {
"type": "string",
"description": "Memory namespace (default: global/knowledge)"
},
"mode": {
"type": "string",
"enum": ["append", "replace"],
"description": "Write mode"
},
"max_chars": {
"type": "number",
"description": "Maximum characters to ingest"
},
"force": {
"type": "boolean",
"description": "Override non-text extension/content safety checks"
}
},
"required": ["object_key"]
}
}
```
#### `minio.sync`
Sync text-like objects from a MinIO prefix into nested memory namespaces (with PDF/DOCX extraction when available).
```json
{
"name": "minio.sync",
"description": "Sync text-like objects from a MinIO prefix into memory namespaces.",
"inputSchema": {
"type": "object",
"properties": {
"prefix": {
"type": "string",
"description": "MinIO object prefix to sync recursively"
},
"bucket": {
"type": "string",
"description": "Optional bucket override"
},
"namespace_base": {
"type": "string",
"description": "Base memory namespace"
},
"mode": {
"type": "string",
"enum": ["append", "replace"],
"description": "Write mode per object namespace"
},
"max_objects": {
"type": "number",
"description": "Maximum objects to ingest in one run"
},
"max_chars_per_object": {
"type": "number",
"description": "Maximum characters per object"
},
"force": {
"type": "boolean",
"description": "Override non-text extension/content safety checks"
}
},
"required": ["prefix"]
}
}
```
### Kubernetes Tools
#### `k8s.pods`
List Kubernetes pods with status summary.
#### `k8s.deployments`
List Kubernetes deployments with replica readiness summary.
#### `k8s.logs`
Fetch recent logs for a pod.
```json
{
"name": "k8s.logs",
"description": "Fetch recent logs for a Kubernetes pod.",
"inputSchema": {
"type": "object",
"properties": {
"pod": {
"type": "string",
"description": "Pod name"
},
"namespace": {
"type": "string",
"description": "Namespace"
},
"container": {
"type": "string",
"description": "Container name (optional)"
},
"lines": {
"type": "number",
"description": "Tail line count"
},
"since": {
"type": "string",
"description": "Lookback duration (e.g. 10m, 1h)"
}
},
"required": ["pod"]
}
}
```
### Media Tools
#### `media.send`
Send a media attachment (image, audio, file) to the user.
```json
{
"name": "media.send",
"description": "Send a media attachment to the user",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "Base64-encoded data"
},
"mimeType": {
"type": "string",
"description": "MIME type (e.g., 'image/jpeg', 'audio/ogg')"
},
"filename": {
"type": "string",
"description": "Filename (optional)"
}
},
"required": ["data", "mimeType"]
}
}
```
#### `image.analyze`
Analyze an image.
```json
{
"name": "image.analyze",
"description": "Analyze an image and describe its contents",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "Base64-encoded image data"
},
"mimeType": {
"type": "string",
"enum": ["image/jpeg", "image/png", "image/gif", "image/webp"],
"description": "Image MIME type"
},
"prompt": {
"type": "string",
"description": "Specific analysis request (optional)"
}
},
"required": ["data", "mimeType"]
}
}
```
#### `audio.transcribe`
Transcribe audio to text.
```json
{
"name": "audio.transcribe",
"description": "Transcribe audio to text",
"inputSchema": {
"type": "object",
"properties": {
"data": {
"type": "string",
"description": "Base64-encoded audio data"
},
"mimeType": {
"type": "string",
"enum": ["audio/ogg", "audio/wav", "audio/mpeg", "audio/webm"],
"description": "Audio MIME type"
}
},
"required": ["data", "mimeType"]
}
}
```
## MCP Tools
Flynn supports external tools via the Model Context Protocol (MCP).
### MCP Tool Names
MCP tools are prefixed with `mcp:`:
```typescript
'mcp:filesystem:read_file'
'mcp:filesystem:write_file'
'mcp:brave_search:search'
```
### MCP Registration
```typescript
// src/mcp/manager.ts
export class McpManager {
async startServer(config: McpServerConfig): Promise<void> {
const client = new McpClient(config);
await client.connect();
// Bridge MCP tools to Flynn's tool registry
const tools = client.listTools();
for (const tool of tools) {
const bridgedTool = bridgeMcpTool(tool, client);
this.toolRegistry.register(bridgedTool);
}
}
}
```
### MCP Tool Format
MCP tools are converted to Flynn's Tool format:
```typescript
function bridgeMcpTool(mcpTool: McpTool, client: McpClient): Tool {
return {
name: `mcp:${mcpTool.name}`,
description: mcpTool.description,
inputSchema: mcpTool.inputSchema,
execute: async (rawArgs: unknown) => {
const result = await client.callTool(mcpTool.name, rawArgs);
return {
success: true,
output: JSON.stringify(result.content)
};
}
};
}
```
## Example Implementation
### Complete Example: Custom Git Tool
```typescript
// src/tools/builtin/git-status.ts
import { execFile } from 'child_process';
import type { Tool, ToolResult } from '../types.js';
interface GitStatusArgs {
path?: string;
}
export const gitStatusTool: Tool = {
name: 'git.status',
description: 'Get the status of a Git repository',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'Path to the repository (default: current directory)'
}
},
required: []
},
execute: async (rawArgs: unknown): Promise<ToolResult> => {
const args = rawArgs as GitStatusArgs;
const cwd = args.path || process.cwd();
try {
const result = await new Promise<{ stdout: string; stderr: string }>(
(resolve, reject) => {
execFile('git', ['status', '--short'], { cwd }, (error, stdout, stderr) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
}
);
return {
success: true,
output: result.stdout || 'No changes'
};
} catch (error) {
return {
success: false,
output: '',
error: error instanceof Error
? `Git status failed: ${error.message}`
: String(error)
};
}
}
};
```
### Register the Tool
```typescript
// src/tools/builtin/index.ts
import { gitStatusTool } from './git-status.js';
export const allBuiltinTools: Tool[] = [
// ... existing tools
gitStatusTool,
];
```
### Testing
```typescript
// src/tools/builtin/git-status.test.ts
import { describe, it, expect } from 'vitest';
import { gitStatusTool } from './git-status.js';
describe('git.status tool', () => {
it('should return git status', async () => {
const result = await gitStatusTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toBeDefined();
});
it('should handle non-git directory', async () => {
const result = await gitStatusTool.execute({ path: '/tmp' });
expect(result.success).toBe(false);
expect(result.error).toContain('Git status failed');
});
});
```
---
For more information:
- Tool types: `src/tools/types.ts`
- Builtin tools: `src/tools/builtin/*.ts`
- Tool policy: `src/tools/policy.ts`
- Tool executor: `src/tools/executor.ts`