feat: added MCP support and plugin system for external tool integration

- Added MCP (Model Context Protocol) support with stdio and HTTP transports for external tool integration.
- Implemented plugin system with install, enable, configure, and doctor commands for extensibility.
- Added 22 Exa MCP tools for web search, LinkedIn, company research, and websets operations.
- Introduced LSP tool with rust-analyzer support for code intelligence (diagnostics, hover, references, code actions).
- Added task tool for delegating work to specialized subagents with parallel execution support.
- Implemented web_search and web_fetch tools with Anthropic and Perplexity provider support.
This commit is contained in:
can1357
2026-01-02 17:54:42 +01:00
parent aa37aafde8
commit 560c9670a8
110 changed files with 17880 additions and 368 deletions
@@ -1,16 +1,15 @@
import type { CustomToolFactory } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
const factory: CustomToolFactory = (_pi) => ({
const factory: CustomToolFactory = (pi) => ({
name: "hello",
label: "Hello",
description: "A simple greeting tool",
parameters: Type.Object({
name: Type.String({ description: "Name to greet" }),
parameters: pi.typebox.Type.Object({
name: pi.typebox.Type.String({ description: "Name to greet" }),
}),
async execute(_toolCallId, params, _onUpdate, _ctx, _signal) {
const { name } = params as { name: string };
const { name } = params;
return {
content: [{ type: "text", text: `Hello, ${name}!` }],
details: { greeted: name },
@@ -3,8 +3,6 @@
*/
import type { CustomTool, CustomToolFactory } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
interface QuestionDetails {
question: string;
@@ -12,12 +10,15 @@ interface QuestionDetails {
answer: string | null;
}
const QuestionParams = Type.Object({
question: Type.String({ description: "The question to ask the user" }),
options: Type.Array(Type.String(), { description: "Options for the user to choose from" }),
});
const factory: CustomToolFactory = (pi) => {
const { Type } = pi.typebox;
const { Text } = pi.pi;
const QuestionParams = Type.Object({
question: Type.String({ description: "The question to ask the user" }),
options: Type.Array(Type.String(), { description: "Options for the user to choose from" }),
});
const tool: CustomTool<typeof QuestionParams, QuestionDetails> = {
name: "question",
label: "Question",
@@ -17,15 +17,7 @@ import * as os from "node:os";
import * as path from "node:path";
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { Message } from "@mariozechner/pi-ai";
import { StringEnum } from "@mariozechner/pi-ai";
import {
type CustomTool,
type CustomToolAPI,
type CustomToolFactory,
getMarkdownTheme,
} from "@mariozechner/pi-coding-agent";
import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import type { CustomTool, CustomToolAPI, CustomToolFactory } from "@mariozechner/pi-coding-agent";
import { type AgentConfig, type AgentScope, discoverAgents, formatAgentList } from "./agents.js";
const MAX_PARALLEL_TASKS = 8;
@@ -401,36 +393,39 @@ async function runSingleAgent(
}
}
const TaskItem = Type.Object({
agent: Type.String({ description: "Name of the agent to invoke" }),
task: Type.String({ description: "Task to delegate to the agent" }),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const ChainItem = Type.Object({
agent: Type.String({ description: "Name of the agent to invoke" }),
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
default: "user",
});
const SubagentParams = Type.Object({
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" })),
agentScope: Type.Optional(AgentScopeSchema),
confirmProjectAgents: Type.Optional(
Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
});
const factory: CustomToolFactory = (pi) => {
const { Type } = pi.typebox;
const { StringEnum, Container, Markdown, Spacer, Text, getMarkdownTheme } = pi.pi;
const TaskItem = Type.Object({
agent: Type.String({ description: "Name of the agent to invoke" }),
task: Type.String({ description: "Task to delegate to the agent" }),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const ChainItem = Type.Object({
agent: Type.String({ description: "Name of the agent to invoke" }),
task: Type.String({ description: "Task with optional {previous} placeholder for prior output" }),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
});
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
description: 'Which agent directories to use. Default: "user". Use "both" to include project-local agents.',
default: "user",
});
const SubagentParams = Type.Object({
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
chain: Type.Optional(Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" })),
agentScope: Type.Optional(AgentScopeSchema),
confirmProjectAgents: Type.Optional(
Type.Boolean({ description: "Prompt before running project-local agents. Default: true.", default: true }),
),
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
});
const tool: CustomTool<typeof SubagentParams, SubagentDetails> = {
name: "subagent",
label: "Subagent",
@@ -454,7 +449,7 @@ const factory: CustomToolFactory = (pi) => {
parameters: SubagentParams,
async execute(_toolCallId, params, onUpdate, _ctx, signal) {
const agentScope: AgentScope = params.agentScope ?? "user";
const agentScope = (params.agentScope ?? "user") as AgentScope;
const discovery = discoverAgents(pi.cwd, agentScope);
const agents = discovery.agents;
const confirmProjectAgents = params.confirmProjectAgents ?? true;
@@ -683,7 +678,7 @@ const factory: CustomToolFactory = (pi) => {
},
renderCall(args, theme) {
const scope: AgentScope = args.agentScope ?? "user";
const scope = (args.agentScope ?? "user") as AgentScope;
if (args.chain && args.chain.length > 0) {
let text =
theme.fg("toolTitle", theme.bold("subagent ")) +
@@ -8,15 +8,12 @@
* The onSession callback reconstructs state by scanning past tool results.
*/
import { StringEnum } from "@mariozechner/pi-ai";
import type {
CustomTool,
CustomToolContext,
CustomToolFactory,
CustomToolSessionEvent,
} from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
interface Todo {
id: number;
@@ -32,14 +29,16 @@ interface TodoDetails {
error?: string;
}
// Define schema separately for proper type inference
const TodoParams = Type.Object({
action: StringEnum(["list", "add", "toggle", "clear"] as const),
text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
});
const factory: CustomToolFactory = (pi) => {
const { Type } = pi.typebox;
const { StringEnum, Text } = pi.pi;
const factory: CustomToolFactory = (_pi) => {
// Define schema separately for proper type inference
const TodoParams = Type.Object({
action: StringEnum(["list", "add", "toggle", "clear"] as const),
text: Type.Optional(Type.String({ description: "Todo text (for add)" })),
id: Type.Optional(Type.Number({ description: "Todo ID (for toggle)" })),
});
// In-memory state (reconstructed from session on load)
let todos: Todo[] = [];
let nextId = 1;
@@ -148,7 +147,7 @@ const factory: CustomToolFactory = (_pi) => {
},
renderCall(args, theme) {
let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", args.action);
let text = theme.fg("toolTitle", theme.bold("todo ")) + theme.fg("muted", String(args.action));
if (args.text) text += ` ${theme.fg("dim", `"${args.text}"`)}`;
if (args.id !== undefined) text += ` ${theme.fg("accent", `#${args.id}`)}`;
return new Text(text, 0, 0);