diff --git a/GitAgent_Redesigned.pptx b/GitAgent_Redesigned.pptx new file mode 100644 index 0000000..7fa214c Binary files /dev/null and b/GitAgent_Redesigned.pptx differ diff --git a/README.md b/README.md index 526cd8d..7d0257f 100644 --- a/README.md +++ b/README.md @@ -607,6 +607,70 @@ my-plugin/ └── index.ts # Programmatic entry point ``` +## MCP (Model Context Protocol) + +Gitagent is an **MCP client**: point it at any [MCP server](https://modelcontextprotocol.io) and that server's tools are automatically discovered and made available to the agent — no integration code to write. This unlocks the whole ecosystem of ready-made servers (filesystem, GitHub, Postgres, Slack, fetch, …). + +### Configure servers in `agent.yaml` + +```yaml +mcp_servers: + filesystem: # local server over stdio (default) + command: npx + args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/data"] + env: + LOG_LEVEL: "${MCP_LOG_LEVEL}" # ${VAR} interpolated from the environment + timeoutMs: 30000 # connect/list timeout (default 30000) + + analytics: # remote server over Streamable HTTP + type: http + url: "https://mcp.example.com/mcp" + headers: + Authorization: "Bearer ${ANALYTICS_TOKEN}" + + legacy: # legacy SSE transport (deprecated) + type: sse + url: "https://old.example.com/sse" +``` + +On startup gitagent connects to each server, lists its tools, and registers them as **`__`** (e.g. `filesystem__read_file`, `analytics__query`). Connections are torn down automatically when the session ends. + +| Field | Applies to | Description | +|---|---|---| +| `command` / `args` / `env` / `cwd` | stdio | How to launch a local server | +| `type: http \| sse` + `url` + `headers` | remote | Connect to a remote server | +| `timeoutMs` | both | Connect + list-tools timeout (default `30000`) | + +### Use via the SDK + +```typescript +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: "Summarize last week's signups from the database", + mcpServers: { + postgres: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-postgres", process.env.DB_URL!], + }, + }, +})) { + if (msg.type === "tool_use") console.log(`calling ${msg.toolName}`); +} +``` + +SDK `mcpServers` are merged with any `agent.yaml` `mcp_servers` (the SDK value wins on a key collision). + +### Behavior & guarantees + +- **Fail-soft:** a server that can't start (or times out) is logged and skipped — other servers and built-in tools keep working. +- **Namespaced & sanitized:** tool names are prefixed with the server name and cleaned to satisfy provider naming rules. +- **Pagination:** servers that paginate their tool list are fully enumerated. +- **Cleanup:** stdio servers (child processes) are shut down on every exit path (normal, `/quit`, Ctrl+C, error). +- **Lazy:** if no servers are configured, the MCP SDK is never loaded. + +> Note: v1 supports MCP **tools**. Resources and prompts are not yet exposed. + ## Multi-Model Support Gitagent works with any LLM provider supported by [pi-ai](https://github.com/badlogic/pi-mono/tree/main/packages/ai): diff --git a/docs/firstsource-session.md b/docs/firstsource-session.md new file mode 100644 index 0000000..1b04261 --- /dev/null +++ b/docs/firstsource-session.md @@ -0,0 +1,1263 @@ +# GitAgent Developer Training Session +### First Source Dev Team Onboarding +**Duration:** ~2 hours | **Format:** Live walkthrough + hands-on exercise + +--- + +> **Facilitator note:** This document is your script, demo guide, and reference sheet in one. Each section includes talking points (what to say), demo steps (what to show), and key concepts to drive home. Code blocks are copy-paste ready for live demos. + +--- + +## Pre-session Checklist + +Before you start, make sure every participant has: + +- [ ] Node.js 20+ installed (`node --version`) +- [ ] npm installed (`npm --version`) +- [ ] git installed (`git --version`) +- [ ] Terminal access (macOS/Linux/WSL) +- [ ] At least one API key ready: Anthropic, OpenAI, or a Lyzr Studio agent ID + +--- + +## Section 1 — Introduction (5 min) + +### What to say + +"Most agent frameworks treat your AI configuration like application code — scattered across files, environment variables, and framework-specific APIs. GitAgent flips that completely. + +In GitAgent, **your agent IS a git repository**. The personality is a markdown file. The rules are a markdown file. The memory is a markdown file that gets committed every time the agent remembers something. The tools, the skills, the hooks — all files in a repo you can clone, fork, branch, and diff. + +Think about what that actually gives you. You can `git log` your agent's memory and see exactly how it evolved. You can `git diff` to see when a rule changed. You can branch off a 'strict-mode' version of your agent for production and a more experimental one for testing. You can fork a teammate's agent, inherit their entire personality and toolset, and customize from there. That's 'agents as repos' — and it's a fundamentally different mental model. + +For a dev team like yours, this is powerful because you already know git. Every workflow you use for code — PRs, branch protection, CI checks — works exactly the same for your agents." + +### Key points to emphasize + +- The core insight: **the agent IS the git repo**, not code that describes an agent +- Git primitives (fork, diff, log, branch) become agent primitives +- No framework lock-in — configuration is plain text files + +### What to show + +Open a terminal and show the structure of a running GitAgent repo: + +``` +my-agent/ +├── agent.yaml # The manifest — model, tools, runtime config +├── SOUL.md # Personality and identity +├── RULES.md # Behavioral constraints +├── DUTIES.md # Job responsibilities +├── AGENTS.md # Sub-agent relationships +├── memory/ +│ └── MEMORY.md # Primary memory (auto-committed by the agent) +├── skills/ +│ └── my-skill/ +│ ├── SKILL.md # Skill definition +│ └── scripts/ # Supporting scripts +├── hooks/ +│ └── hooks.yaml # Lifecycle hooks +└── tools/ + └── *.yaml # Declarative tool definitions +``` + +"This is everything your agent needs to exist. Back it up, share it, version it, deploy it — it's just a directory." + +--- + +## Section 2 — Installation & First Agent (15 min) + +### What to say + +"Let's get everyone running. There are two ways to install. The fastest is a one-command installer that handles everything interactively — API key setup, scaffolding, and launching the web UI." + +### What to show + +#### Option A: One-command install (recommended for today) + +```bash +bash <(curl -fsSL "https://raw.githubusercontent.com/open-gitagent/gitagent/main/install.sh?$(date +%s)") +``` + +"That curl-bash will: +1. Install `@open-gitagent/gitagent` globally via npm (the slim CLI + SDK) +2. Install `@open-gitagent/voice` for the web UI at `localhost:3333` +3. Walk you through API key setup in interactive mode +4. Launch the web UI in your browser" + +**Requirements:** Node.js 18+ (20+ recommended), npm, git + +#### Option B: Manual install (for CI/sandboxed environments) + +```bash +# Core CLI + SDK only (no voice, no web UI — good for headless/CI) +npm install -g @open-gitagent/gitagent + +# Add voice mode + web UI +npm install -g @open-gitagent/voice +``` + +"If your security scanner flags the full install, use the slim core. It's about 85KB vs 180KB and has no third-party scanner triggers." + +#### Scaffold your first agent + +```bash +# Create a directory for your agent +mkdir ~/my-first-agent && cd ~/my-first-agent + +# Run gitagent in it — it auto-scaffolds everything on first run +export OPENAI_API_KEY="sk-..." # or ANTHROPIC_API_KEY, or LYZR_API_KEY +gitagent "Hello, what are you?" +``` + +"Watch what happens. GitAgent detects there's no `agent.yaml`, so it scaffolds one along with `SOUL.md`, `RULES.md`, and `memory/MEMORY.md` automatically. Then it answers your question." + +#### Walk through what was created + +```bash +ls -la ~/my-first-agent +cat ~/my-first-agent/agent.yaml +cat ~/my-first-agent/SOUL.md +cat ~/my-first-agent/RULES.md +cat ~/my-first-agent/memory/MEMORY.md +``` + +#### Launch the web UI + +```bash +gitagent --voice # Opens localhost:3333 in your browser +``` + +"The web UI has tabs for Chat, Skills, Integrations, Communication, SkillFlows, Scheduler, and Settings. We'll come back to several of these. For now, confirm everyone can open `localhost:3333`." + +### Key points to emphasize + +- Auto-scaffolding means zero manual setup to get started +- The slim install (`GITAGENT_SLIM=1`) skips voice for pipeline/CI use cases +- The web UI is optional — everything works headlessly too + +--- + +## Section 3 — The #1 Question: Connecting to Lyzr Studio (10 min) + +### What to say + +"Before we go deeper into configuration, I want to address the question we get more than any other: 'How do I connect GitAgent to Lyzr Studio?' This is probably relevant to several of you, so let's do it now. + +Lyzr Studio lets you build, manage, and orchestrate AI agents visually. GitAgent can use a Lyzr Studio agent as its model backend — meaning the intelligence comes from your Studio agent, and GitAgent provides the git-native structure, tools, memory, and hooks around it." + +### What to show — step by step + +#### Step 1: Get your LYZR_API_KEY + +1. Go to [https://studio.lyzr.ai](https://studio.lyzr.ai) and log in +2. Navigate to **Settings → API Keys** +3. Copy your API key + +```bash +export LYZR_API_KEY="lyzr-sk-..." +``` + +#### Step 2: Get your Agent ID + +1. In Lyzr Studio, open the agent you want to connect +2. The agent ID is in the URL: `https://studio.lyzr.ai/agents//...` +3. Or find it in the agent's **Settings** panel — it looks like `agent-abc123xyz` + +#### Step 3: Set the model in agent.yaml + +```yaml +# agent.yaml +spec_version: "0.1.0" +name: firstsource-agent +version: 0.1.0 +description: First Source's GitAgent connected to Lyzr Studio + +model: + preferred: "lyzr:agent-abc123xyz@https://agent-prod.studio.lyzr.ai/v4" + fallback: + - "openai:gpt-4o" # optional fallback if Studio is unreachable + +tools: + - cli + - read + - write + - memory + +runtime: + max_turns: 40 +``` + +The model string format is: `lyzr:@` + +#### Step 4: Run with explicit flags (if you prefer not to edit agent.yaml yet) + +```bash +gitagent \ + --model "lyzr:agent-abc123xyz@https://agent-prod.studio.lyzr.ai/v4" \ + "Hello from GitAgent" +``` + +#### Step 5: Verify the connection + +You should see the response come from your Lyzr Studio agent. Check the Studio dashboard — the agent's invocation count should increment. + +### Common errors and fixes + +| Error | Likely cause | Fix | +|---|---|---| +| `401 Unauthorized` | Wrong or missing API key | Check `echo $LYZR_API_KEY` is set correctly | +| `404 Not Found` | Wrong agent ID in the model string | Verify the agent ID from Studio URL | +| `Model provider not found: lyzr` | Outdated gitagent version | `npm install -g @open-gitagent/gitagent@latest` | +| Agent responds but ignores SOUL.md | Studio agent has its own system prompt | Either merge them in Studio, or use `systemPromptSuffix` in SDK | +| Timeout on first call | Studio agent cold start | Retry once; subsequent calls are faster | + +### What to say (wrap-up) + +"Once that's working, everything else we cover today — memory, skills, hooks — wraps around your Lyzr Studio agent. The Studio agent provides the intelligence; GitAgent provides the structure and control layer." + +--- + +## Section 4 — Configuring Your Agent (15 min) + +### What to say + +"Now let's understand what you can actually configure. The starting point is always `agent.yaml` — it's the manifest that describes everything about your agent. But `agent.yaml` mostly wires things together. The real character of your agent lives in the identity files." + +### What to show + +#### agent.yaml — full reference example + +```yaml +# agent.yaml +spec_version: "0.1.0" +name: firstsource-support-agent +version: 1.0.0 +description: Customer support agent for First Source + +# Model configuration +model: + preferred: "anthropic:claude-sonnet-4-5-20250929" + fallback: + - "openai:gpt-4o" + - "google:gemini-2.0-flash" + +# Built-in tools to enable +tools: + - cli + - read + - write + - memory + - task_tracker + +# Runtime limits +runtime: + max_turns: 40 + +# MCP servers (covered in Section 8) +mcp_servers: + github: + command: npx + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT}" + +# Compliance (covered in Section 9) +compliance: + risk_level: medium + human_in_the_loop: false + audit_logging: true + regulatory_frameworks: [SOC2] + +# Sub-agents (optional — covered later) +agents: + researcher: + dir: "./agents/researcher" + delegation: + mode: explicit +``` + +**Multi-provider model strings:** + +```yaml +# Anthropic +preferred: "anthropic:claude-sonnet-4-5-20250929" + +# OpenAI +preferred: "openai:gpt-4o" + +# Google +preferred: "google:gemini-2.0-flash" + +# Groq (fast inference) +preferred: "groq:llama-3.3-70b-versatile" + +# Local via Ollama +preferred: "ollama:llama3.2" + +# Local via LM Studio +preferred: "lmstudio:mistral-7b" + +# Lyzr Studio +preferred: "lyzr:agent-abc123@https://agent-prod.studio.lyzr.ai/v4" +``` + +#### SOUL.md — writing a good personality + +"SOUL.md is your agent's personality file. It defines who the agent is — how it speaks, what it cares about, how it approaches problems. It becomes part of the system prompt on every query." + +**What makes a good SOUL.md:** + +```markdown +# Alex — First Source Support Agent + +You are Alex, a senior customer support specialist at First Source Financial Services. You've been with the company for five years and you know the product inside out. + +## How you work + +- You respond concisely and directly. Support tickets aren't the place for preamble. +- You ask one clarifying question at a time — never a list of five questions at once. +- When you don't know something, you say so, then point to where the answer can be found. +- You use the ticket tracker to log every resolution step so teammates can pick up mid-thread. + +## Tone + +- Professional but human. You're not a bot — you're a specialist. +- Calm under pressure. Escalations don't fluster you. +- Never overpromise. If you say "I'll check on that," you check on it. + +## Knowledge domain + +You specialize in: account management, billing disputes, integration support, and API troubleshooting. +``` + +"Notice: no emoji, no corporate speak, concrete behaviors. Write it the way you'd brief a new hire on their first day." + +#### RULES.md — behavioral constraints + +"RULES.md is where you put hard constraints — things the agent must never do, always do, or require explicit approval for." + +```markdown +# Rules + +1. **Never share customer PII in responses.** Redact account numbers, SSNs, and contact details from any output visible to third parties. +2. **Read before modifying.** Always read a file before editing or overwriting it. +3. **Require approval for external API calls.** Any outbound HTTP request to a non-approved domain needs confirmation. +4. **No credentials in memory.** Never store API keys, tokens, or passwords in MEMORY.md. +5. **Escalate unresolved issues after 3 turns.** If a customer issue isn't resolved within three exchanges, create an escalation ticket and notify a human. +6. **Stay in scope.** Only operate within the current repository and approved external services. +``` + +#### DUTIES.md — job responsibilities + +"DUTIES.md describes the agent's recurring responsibilities — what it's supposed to proactively do, what workflows it owns." + +```markdown +# Duties + +## Daily responsibilities +- Review open support tickets and triage by severity +- Check integration health dashboards and flag anomalies +- Update MEMORY.md with any new resolution patterns discovered + +## On each new ticket +1. Classify: billing, access, integration, or other +2. Check MEMORY.md for a matching prior resolution +3. Attempt resolution; document steps taken +4. If resolved: close ticket and log pattern to memory +5. If unresolved after 3 turns: escalate per RULES.md +``` + +### Key points to emphasize + +- `agent.yaml` is the wiring; identity files are the character +- SOUL.md is read on every query — keep it focused and specific +- RULES.md constraints are enforced via the agent's reasoning, not code — keep rules unambiguous +- These files are committed to git, so you get a full audit trail of every personality change + +--- + +## Section 5 — Tools & Skills (15 min) + +### What to say + +"Tools are the actions your agent can take. Skills are composable instruction modules — think of them as prompts-plus-scripts you can snap in and invoke on demand." + +### What to show + +#### Built-in tools + +| Tool | What it does | +|---|---| +| `cli` | Run any shell command | +| `read` | Read files from the filesystem | +| `write` | Write or create files | +| `memory` | Save to `memory/MEMORY.md` (auto-commits) | +| `capture_photo` | Take a photo via webcam | +| `task_tracker` | Create and update tasks | +| `skill_learner` | Learn and save new skills automatically | + +Enable them in `agent.yaml`: + +```yaml +tools: + - cli + - read + - write + - memory + - task_tracker +``` + +Or restrict them from the SDK: + +```typescript +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: "Summarize the logs", + dir: "./my-agent", + allowedTools: ["read", "memory"], // whitelist + disallowedTools: ["cli"], // or blacklist +})) { + if (msg.type === "delta") process.stdout.write(msg.content); +} +``` + +#### Creating your first custom skill + +"Skills live in `skills//SKILL.md`. The frontmatter registers the skill; the markdown body becomes the agent's instructions when the skill is invoked." + +```bash +mkdir -p ~/my-first-agent/skills/summarize-pr +``` + +Create `skills/summarize-pr/SKILL.md`: + +```markdown +--- +name: summarize-pr +description: Summarizes a GitHub pull request — what changed, why, and risk level. +--- + +# Summarize Pull Request + +When this skill is invoked: + +1. Ask for the PR number if not provided. +2. Use the `cli` tool to run: `gh pr view --json title,body,files,additions,deletions` +3. Analyze the diff for: + - What problem it solves + - What files changed and why + - Estimated risk level: Low / Medium / High + - Any obvious issues or missing test coverage +4. Output a summary in this format: + +**PR #: ** +- **What:** <one sentence> +- **Why:** <one sentence> +- **Changed:** <N files, +X -Y lines> +- **Risk:** Low / Medium / High +- **Flags:** <any issues, or "None"> +``` + +#### Invoke skills from the REPL + +```bash +# In the gitagent REPL or web UI chat: +/skill:summarize-pr Review PR #142 +``` + +#### Skills with supporting scripts + +```bash +mkdir -p ~/my-first-agent/skills/run-tests/scripts +``` + +Create `skills/run-tests/SKILL.md`: + +```markdown +--- +name: run-tests +description: Runs the project test suite and summarizes failures. +--- + +# Run Tests + +Execute the test script and report results: + +```bash +bash scripts/run.sh +``` + +Summarize: how many passed, how many failed, and what the failures are. +``` + +Create `skills/run-tests/scripts/run.sh`: + +```bash +#!/usr/bin/env bash +npm test 2>&1 | tail -30 +``` + +Scripts receive args as JSON on stdin and return output on stdout. + +#### Automatic skill learning + +"The `skill_learner` built-in tool is interesting. When you enable it, the agent can learn new skills from conversation and save them automatically with a confidence score. If you show it how to do something once, it can codify that as a reusable skill." + +```yaml +# agent.yaml +tools: + - cli + - read + - write + - memory + - skill_learner # enables automatic skill capture +``` + +"The agent won't just save anything — it assigns confidence scores and only promotes high-confidence patterns to permanent skills. Lower confidence entries stay as memory notes until they're validated through repeated use." + +### Key points to emphasize + +- Skills are version-controlled — you can review, rollback, or branch skill changes +- A skill is just markdown + optional scripts, so anyone on the team can write or edit one +- `/skill:name` invocation works in both the REPL and the web UI chat + +--- + +## Section 6 — Memory System (10 min) + +### What to say + +"Memory in GitAgent is unlike any other framework I've seen. Most agents use a vector database or hidden in-memory state. GitAgent's memory is a markdown file in your repo that the agent commits every time it saves something. Your agent's memory has a git history. + +That means you can `git log memory/MEMORY.md` and see every memory entry in order. You can `git diff HEAD~5 memory/MEMORY.md` to see exactly what the agent remembered over the last five runs. You can `git revert` to roll back a bad memory. You can fork a repo and give the fork a completely different memory history. This is extraordinarily powerful for debugging, auditing, and collaboration." + +### What to show + +#### Primary memory — MEMORY.md + +```bash +cat ~/my-first-agent/memory/MEMORY.md +``` + +"Every time the agent calls the `memory` tool, it appends to this file and creates a git commit. No external database required." + +Example of what an agent writes to memory: + +```markdown +# Agent Memory + +## Resolved Patterns + +### Billing dispute: duplicate charge +- Root cause: race condition in payment processor webhook +- Resolution: Void the duplicate, issue credit note, flag account for 30-day monitoring +- First seen: 2025-06-10, recurred: 2025-06-14 + +### API auth failure: 401 on valid token +- Root cause: Token cached before timezone-offset expiry recalculation +- Resolution: Force token refresh + advise client to add 5-min buffer to expiry +``` + +#### Memory layers via memory.yaml + +For more advanced use, you can define layered memory: + +```yaml +# memory/memory.yaml +layers: + - name: primary + path: memory/MEMORY.md + description: Core working memory + - name: journal + path: memory/journal.md + description: Daily activity log + - name: mood + path: memory/mood.md + description: Current agent state and context +``` + +#### Why git-native memory is powerful + +```bash +# See full memory history +git log --oneline memory/MEMORY.md + +# See what the agent remembered in the last 10 runs +git diff HEAD~10 memory/MEMORY.md + +# Roll back a bad memory entry +git revert <commit-hash> + +# Fork the repo, fork the memory history +git checkout -b experiment +# edit SOUL.md, run agent, memory diverges independently +``` + +"In a team context: if two people fork the same agent and run it for a week, you can literally `git merge` their memory histories. Try doing that with a vector database." + +### Key points to emphasize + +- Memory is plain text + git, not a hidden opaque database +- `git log` on memory = full audit trail of agent decisions +- Layered memory lets you separate short-term working memory from long-term patterns +- Auto-archiving keeps MEMORY.md from growing unbounded — the agent summarizes old entries + +--- + +## Section 7 — Hooks for Control & Safety (10 min) + +### What to say + +"Hooks are how you put guardrails on your agent without having to modify its core behavior. A hook fires at a specific lifecycle event — before a tool runs, after a failure, when a file changes — and it can block, modify, or allow the action. + +This is critical for production deployments. You probably don't want an agent that can run `rm -rf` on your production server, even if it thinks it's a good idea. Hooks let you enforce that at the infrastructure level." + +### What to show + +#### Hook events reference + +| Event | Fires when | Can block? | +|---|---|---| +| `pre_tool_use` | Before any tool executes | Yes | +| `post_tool_failure` | After a tool fails | No (logging) | +| `pre_query` | Before sending to LLM | Yes | +| `post_response` | After LLM responds | No (logging) | +| `file_changed` | A tracked file is modified | Yes | +| `on_error` | Any unhandled error | No (logging) | + +#### Script-based hooks (hooks/hooks.yaml) + +```bash +mkdir -p ~/my-first-agent/hooks +``` + +Create `hooks/hooks.yaml`: + +```yaml +hooks: + pre_tool_use: + - script: hooks/safety-check.sh + description: Block dangerous commands and require approval for deployments + + post_response: + - script: hooks/audit-log.sh + description: Log all responses to audit trail + + on_error: + - script: hooks/alert.sh + description: Alert team on unhandled errors +``` + +Create `hooks/safety-check.sh`: + +```bash +#!/usr/bin/env bash + +# Read context from stdin +CONTEXT=$(cat) +TOOL=$(echo "$CONTEXT" | jq -r '.tool // .toolName // ""') +COMMAND=$(echo "$CONTEXT" | jq -r '.args.command // ""') + +# Block rm -rf under any circumstances +if echo "$COMMAND" | grep -qE 'rm\s+-rf|rm\s+--recursive\s+-f'; then + echo '{"action":"block","reason":"Destructive rm -rf is not permitted. Use trash or move to a backup location instead."}' + exit 0 +fi + +# Block git push --force to main/master +if echo "$COMMAND" | grep -qE 'git push.*--force.*(main|master)|git push.*-f.*(main|master)'; then + echo '{"action":"block","reason":"Force push to main/master is not permitted. Open a PR."}' + exit 0 +fi + +# Require human approval for deploy commands +if echo "$COMMAND" | grep -qE 'kubectl apply|helm upgrade|terraform apply|fly deploy'; then + echo '{"action":"block","reason":"Production deployments require human approval. Use the deployment checklist PR flow."}' + exit 0 +fi + +# Everything else: allow +echo '{"action":"allow"}' +``` + +```bash +chmod +x ~/my-first-agent/hooks/safety-check.sh +``` + +#### Programmatic hooks via SDK (for inline use) + +```typescript +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: "Deploy the new service version", + dir: "./my-agent", + hooks: { + preToolUse: async (ctx) => { + // Block destructive commands + if (ctx.toolName === "cli") { + const cmd = ctx.args.command ?? ""; + if (/rm\s+-rf/.test(cmd)) { + return { action: "block", reason: "Destructive rm -rf blocked by policy" }; + } + // Require approval for deploys + if (/kubectl apply|helm upgrade/.test(cmd)) { + return { action: "block", reason: "Deploy requires human approval via PR" }; + } + } + + // Rewrite unsafe file writes to a sandboxed path + if (ctx.toolName === "write" && !ctx.args.path.startsWith("/workspace/")) { + return { + action: "modify", + args: { ...ctx.args, path: `/workspace/${ctx.args.path}` }, + }; + } + + return { action: "allow" }; + }, + + onError: async (ctx) => { + // Send alert — could call a webhook here + console.error(`[ALERT] Agent error: ${ctx.error}`); + }, + }, +})) { + if (msg.type === "delta") process.stdout.write(msg.content); +} +``` + +"The three hook return values are: +- `{ action: 'allow' }` — proceed normally +- `{ action: 'block', reason: '...' }` — stop the tool call, show reason to agent +- `{ action: 'modify', args: {...} }` — let the tool run but with different arguments" + +### Key points to emphasize + +- Hooks are the safety layer between the agent and the world +- `pre_tool_use` is the most important hook — it runs before any tool executes +- Scripts are simpler for ops teams; programmatic hooks are better for complex conditional logic +- Hooks compose — you can have multiple scripts registered for the same event + +--- + +## Section 8 — MCP Client Integration (10 min) + +### What to say + +"MCP stands for Model Context Protocol — it's an open standard for connecting AI models to external tools and data sources. Think of it like a plugin system that any MCP-compatible agent can use. + +GitAgent is an MCP client. Point it at any MCP server and that server's tools are automatically discovered and available to your agent, no integration code required. There's already a large ecosystem of ready-made MCP servers for GitHub, Slack, PostgreSQL, filesystem operations, web fetch, and more." + +### What to show + +#### Configure MCP servers in agent.yaml + +```yaml +# agent.yaml +mcp_servers: + # Local server launched as a child process (stdio transport) + github: + command: npx + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT}" + timeoutMs: 30000 + + # Remote server over Streamable HTTP + analytics: + type: http + url: "https://mcp.yourcompany.com/mcp" + headers: + Authorization: "Bearer ${ANALYTICS_TOKEN}" + + # Legacy SSE transport (deprecated but still supported) + legacy-service: + type: sse + url: "https://old.example.com/sse" +``` + +#### How tool namespacing works + +"When GitAgent connects to the `github` MCP server, it discovers all the tools that server exposes and registers them as `github__<tool_name>`. So `read_file` becomes `github__read_file`, `create_pr` becomes `github__create_pr`. This prevents naming collisions when you have multiple MCP servers connected." + +``` +MCP server: github + └─ list_pulls → agent sees: github__list_pulls + └─ create_issue → agent sees: github__create_issue + └─ get_pull_request → agent sees: github__get_pull_request + +MCP server: analytics + └─ query → agent sees: analytics__query + └─ get_dashboard → agent sees: analytics__get_dashboard +``` + +#### Practical example: GitHub MCP server + +```bash +# Install the GitHub MCP server +npm install -g @modelcontextprotocol/server-github + +# Add to agent.yaml +``` + +```yaml +mcp_servers: + github: + command: npx + args: ["-y", "@modelcontextprotocol/server-github"] + env: + GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT}" +``` + +```bash +export GITHUB_PAT="ghp_yourtoken" + +# Now ask the agent something that requires GitHub +gitagent "List the open PRs on our main repo and summarize what each one is doing" +``` + +"The agent will call `github__list_pulls`, `github__get_pull_request`, etc., automatically — no code, no glue layer." + +#### MCP via the SDK + +```typescript +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: "Summarize last week's signups from the database", + mcpServers: { + postgres: { + command: "npx", + args: ["-y", "@modelcontextprotocol/server-postgres", process.env.DB_URL!], + }, + }, +})) { + if (msg.type === "tool_use") console.log(`Calling: ${msg.toolName}`); + if (msg.type === "delta") process.stdout.write(msg.content); +} +``` + +"SDK-level `mcpServers` merge with `agent.yaml` `mcp_servers`. If there's a key collision, the SDK value wins. This lets you override per-query without touching the manifest." + +### Behavior guarantees to know about + +| Behavior | Detail | +|---|---| +| Fail-soft | A server that can't start is logged and skipped — other tools keep working | +| Namespaced | Tool names are prefixed with server name, cleaned to satisfy provider naming rules | +| Pagination | Servers that paginate tool lists are fully enumerated | +| Cleanup | Stdio child processes are shut down on every exit path | +| Lazy loading | If no MCP servers are configured, the MCP SDK is never loaded | + +### Key points to emphasize + +- MCP is the "npm for agent tools" — install a server, point your agent at it, done +- Namespacing (`server__tool`) prevents conflicts when using multiple servers +- Fail-soft means a broken MCP server won't take down your agent session + +--- + +## Section 9 — Going to Production (10 min) + +### What to say + +"At some point you're going to want to deploy an agent that runs continuously, handles real workloads, and operates in an audited environment. Let's cover the production-readiness checklist." + +### What to show + +#### 1. Password-protect the web UI + +```bash +# Set before launching +export GITAGENT_PASSWORD="your-secure-password" +gitagent --voice # Now localhost:3333 requires this password +``` + +"Anyone who can reach the web UI can chat with your agent. In production, either set a strong password, put it behind a VPN/reverse proxy, or don't expose the UI at all and use the CLI/SDK only." + +#### 2. Branch-based deployment strategy + +"Because your agent is a git repo, you can use branches exactly like you do for code." + +```bash +# main branch = production agent +git checkout main +gitagent --dir . "Help the customer" + +# feature branch = experiment safely +git checkout -b experiment/new-personality +# edit SOUL.md, RULES.md +gitagent --dir . "Test the new behavior" + +# Merge when ready — peer review the SOUL.md diff just like code review +git checkout main +git merge experiment/new-personality +``` + +"This means your agent changes go through code review. Someone changes RULES.md to remove a safety constraint? That's a diff in a PR. Your team reviews it. CI can run tests against it. You get the same safety net you have for application code." + +#### 3. Compliance and audit logging + +```yaml +# agent.yaml +compliance: + risk_level: high # low | medium | high + human_in_the_loop: true # pause and require human approval for high-risk actions + data_classification: confidential + regulatory_frameworks: [SOC2, GDPR, HIPAA] + recordkeeping: + audit_logging: true + retention_days: 90 +``` + +"Audit logs are written to `.gitagent/audit.jsonl` — JSONL format, one entry per tool invocation, with full traces. If you need to answer 'what did the agent do at 14:32 on June 15?' you have a complete record." + +#### 4. Schedules for recurring tasks + +Create a schedule file: + +```bash +mkdir -p ~/my-first-agent/schedules +``` + +Create `schedules/daily-triage.yaml`: + +```yaml +name: daily-triage +description: Morning ticket triage at 8 AM every weekday +cron: "0 8 * * 1-5" # 8 AM Mon–Fri +prompt: | + Review all open support tickets from the last 24 hours. + For each: classify severity, check MEMORY.md for prior similar issues, + and prepare a triage summary. Save the summary to memory. +enabled: true +``` + +"Manage schedules in the web UI under the **Scheduler** tab, or define them as YAML files in the `schedules/` directory. Cron syntax, one-time runs, and recurring are all supported." + +#### 5. E2B sandbox for untrusted code execution + +```bash +# Run agent in an isolated VM sandbox +gitagent --sandbox "Analyze this uploaded CSV and generate a report" +``` + +```yaml +# Or in agent.yaml for a specific environment config +runtime: + max_turns: 40 + sandbox: true +``` + +#### 6. Secrets management + +```bash +# .gitignore — this is non-negotiable +cat >> ~/.gitignore_global << 'EOF' +.env +.env.* +*.pem +*.key +secrets/ +EOF + +git config --global core.excludesfile ~/.gitignore_global +``` + +"And use the global env fallback for keys that apply to all your agents:" + +```bash +mkdir -p ~/.gitagent +echo 'ANTHROPIC_API_KEY=sk-ant-...' >> ~/.gitagent/.env +echo 'LYZR_API_KEY=lyzr-sk-...' >> ~/.gitagent/.env +``` + +"Keys in `~/.gitagent/.env` are available to all your agents without being in any individual repo. The web UI also lets you save keys via the Settings tab, and they auto-reload without restarting the server." + +### Key points to emphasize + +- Branch-based deployment means agent changes get the same review process as code +- Audit logs in `.gitagent/audit.jsonl` are your compliance paper trail +- `~/.gitagent/.env` keeps secrets out of individual repos +- Schedules let you turn an interactive agent into an autonomous worker + +--- + +## Section 10 — Hands-on Exercise (15 min) + +### What to say + +"Now it's your turn. Each person is going to create their own agent, give it a personality and rules, and write one custom skill. By the end of this exercise you'll have a working agent you can take back to your team." + +### Exercise steps + +#### Step 1: Create your agent directory (2 min) + +```bash +mkdir ~/firstsource-<yourname>-agent +cd ~/firstsource-<yourname>-agent +git init +``` + +#### Step 2: Create agent.yaml (2 min) + +```yaml +# agent.yaml +spec_version: "0.1.0" +name: <yourname>-agent +version: 0.1.0 +description: My First Source GitAgent + +model: + preferred: "anthropic:claude-sonnet-4-5-20250929" # or your preferred provider + fallback: + - "openai:gpt-4o" + +tools: + - cli + - read + - write + - memory + +runtime: + max_turns: 20 +``` + +#### Step 3: Write your SOUL.md (3 min) + +"Write a SOUL.md for yourself as if you were describing your working style to a new team member. Be specific — what do you care about, how do you communicate, what's your expertise." + +```markdown +# <Your Agent Name> + +You are <name>, a <role> at First Source. + +## How you work +- <3 specific behavioral traits> + +## Tone +- <How you communicate> + +## Domain expertise +- <What you know> +``` + +#### Step 4: Write your RULES.md (2 min) + +```markdown +# Rules + +1. **Read before modifying.** Always read a file before editing it. +2. **No credentials in memory.** Never store API keys or passwords. +3. **<Add one rule specific to your role>** +4. **Report failures honestly.** If something didn't work, say so. +``` + +#### Step 5: Create a custom skill (4 min) + +"Create a skill that's useful for your actual work. Here are some ideas: +- `standup-summary` — summarizes what you did today from git log + notes +- `code-review-checklist` — runs through a standard review checklist +- `ticket-template` — generates a properly formatted support ticket +- `api-health-check` — pings a list of endpoints and reports status" + +```bash +mkdir -p skills/my-skill +``` + +```markdown +--- +name: my-skill +description: <one sentence describing what this skill does> +--- + +# <Skill Name> + +When this skill is invoked: + +1. <Step one> +2. <Step two> +3. Output the result in this format: <format> +``` + +#### Step 6: Run your agent and invoke the skill (2 min) + +```bash +gitagent "Hello — tell me who you are" + +# Then invoke your skill: +# /skill:my-skill <input> +``` + +#### Share with the group + +"Once everyone has their skill working, take two minutes to share: what skill did you build, and what would it actually save you time on?" + +--- + +## Section 11 — Q&A (10 min) + +### Anticipated questions with answers + +**Q: Can multiple developers share one agent repo?** + +Yes — that's the point. Treat it like a shared service repo. Use branch protection on `main`, require PR reviews for changes to `SOUL.md`, `RULES.md`, or `hooks/`. Anyone can add skills on feature branches. + +**Q: How do I handle secrets in a shared agent repo?** + +Two approaches: +1. Use environment variable references in agent.yaml (`"${MY_KEY}"`) and have each developer set the var locally or in CI +2. Put team-level secrets in `~/.gitagent/.env` on each machine — never committed + +Never put actual key values in any tracked file. + +**Q: What happens when the agent runs out of turns?** + +It stops with a `max_turns` system message. The state (including memory) is preserved. You can resume the conversation by running the agent again — it'll read MEMORY.md and have context. + +**Q: Can I use GitAgent with our existing CI/CD pipeline?** + +Yes. The SDK is the right approach for CI integration: + +```typescript +// In your CI script +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: `Review PR #${process.env.PR_NUMBER} for security issues`, + dir: "./agent", + model: "anthropic:claude-sonnet-4-5-20250929", + allowedTools: ["read", "cli"], // restrict for CI +})) { + if (msg.type === "assistant") console.log(msg.content); +} +``` + +**Q: How does SkillFlow work for multi-step workflows?** + +SkillFlows are YAML files that define multi-step workflows. They support `__approval_gate__` steps that pause execution and ping via Telegram or WhatsApp before continuing. Good for workflows where a human needs to review an intermediate result before the agent proceeds. Manage them in the web UI's **SkillFlows** tab. + +**Q: Can the agent talk to our internal tools, not just public MCP servers?** + +Yes. Write a simple MCP server that wraps your internal API (there are SDKs for Python, TypeScript, and more at [modelcontextprotocol.io](https://modelcontextprotocol.io)) and configure it as a `stdio` server in `agent.yaml`. It runs as a child process on the same machine — no public exposure needed. + +**Q: What's the difference between `DUTIES.md` and a scheduled task?** + +DUTIES.md tells the agent what it's responsible for conceptually — it shapes behavior during any session. A schedule actually triggers the agent to run at a specific time. You'd typically have related content in both: DUTIES.md says "you own daily triage", and a schedule actually runs the triage at 8 AM. + +**Q: Can I connect GitAgent to our Lyzr Studio agents programmatically in a script?** + +Yes: + +```typescript +import { query } from "gitagent"; + +for await (const msg of query({ + prompt: "Handle this support request", + model: `lyzr:${process.env.LYZR_AGENT_ID}@https://agent-prod.studio.lyzr.ai/v4`, + dir: "./my-agent", +})) { + if (msg.type === "delta") process.stdout.write(msg.content); +} +``` + +Set `LYZR_API_KEY` in the environment before running. + +--- + +## Quick Reference Card + +Save this for daily use: + +```bash +# Install +bash <(curl -fsSL "https://raw.githubusercontent.com/open-gitagent/gitagent/main/install.sh?$(date +%s)") +npm install -g @open-gitagent/gitagent @open-gitagent/voice + +# Run +gitagent "prompt" # run in current dir +gitagent --dir ~/my-agent "prompt" # specific dir +gitagent --model anthropic:claude-sonnet-4-5-20250929 "prompt" # override model +gitagent --voice # open web UI at localhost:3333 +gitagent --sandbox "prompt" # run in isolated VM + +# Lyzr Studio +export LYZR_API_KEY="lyzr-sk-..." +gitagent --model "lyzr:<agent-id>@https://agent-prod.studio.lyzr.ai/v4" "prompt" + +# Invoke a skill +/skill:my-skill <input> + +# Plugins +gitagent plugin install https://github.com/org/plugin.git +gitagent plugin list +gitagent plugin init my-plugin + +# Telemetry (optional) +OTEL_TRACES_EXPORTER=console gitagent "prompt" # print spans to stdout +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 gitagent "prompt" # Jaeger + +# Git memory inspection +git log --oneline memory/MEMORY.md # see memory history +git diff HEAD~5 memory/MEMORY.md # see recent memory changes +``` + +### Key environment variables + +| Variable | Purpose | +|---|---| +| `ANTHROPIC_API_KEY` | Anthropic / Claude | +| `OPENAI_API_KEY` | OpenAI | +| `GOOGLE_GENERATIVE_AI_API_KEY` | Google Gemini | +| `GROQ_API_KEY` | Groq | +| `LYZR_API_KEY` | Lyzr Studio | +| `GITHUB_TOKEN` | GitHub repo access | +| `GITAGENT_PASSWORD` | Web UI password | +| `GITAGENT_SLIM` | Set to `1` to skip voice on install | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry collector URL | + +### Agent directory structure reference + +``` +my-agent/ +├── agent.yaml # Required: model, tools, runtime, compliance +├── SOUL.md # Personality and identity +├── RULES.md # Behavioral constraints +├── DUTIES.md # Recurring responsibilities +├── AGENTS.md # Sub-agent relationships +├── memory/ +│ ├── MEMORY.md # Primary memory (auto-committed) +│ ├── memory.yaml # Memory layer config (optional) +│ ├── mood.md # Agent state (optional) +│ └── journal.md # Activity log (optional) +├── skills/ +│ └── <name>/ +│ ├── SKILL.md # Skill definition (frontmatter + instructions) +│ └── scripts/ # Supporting scripts +├── hooks/ +│ └── hooks.yaml # Lifecycle hook scripts +├── tools/ +│ └── *.yaml # Declarative tool definitions +├── plugins/ +│ └── <name>/ # Local plugins +├── schedules/ +│ └── *.yaml # Cron schedule definitions +└── .gitagent/ + └── audit.jsonl # Audit log (when audit_logging: true) +``` + +--- + +## Resources + +- GitHub: [https://github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent) +- Lyzr Studio: [https://studio.lyzr.ai](https://studio.lyzr.ai) +- MCP servers: [https://modelcontextprotocol.io](https://modelcontextprotocol.io) +- Issues / support: [https://github.com/open-gitagent/gitagent/issues](https://github.com/open-gitagent/gitagent/issues) + +--- + +*Session prepared for First Source dev team onboarding — GitAgent v1.1.1+* diff --git a/docs/serverless-blog.md b/docs/serverless-blog.md new file mode 100644 index 0000000..16897bb --- /dev/null +++ b/docs/serverless-blog.md @@ -0,0 +1,113 @@ +# Kill the Server. Your AI Agent Belongs in Git. + +Most AI agents are servers. They sit idle, burning compute, waiting for the next request. This is not a technical requirement. It is a habit — and an expensive one. + +GitAgent is not an agent you run. It is an agent you invoke. + +--- + +## The Server Assumption + +When developers build AI agents, they reach for the same mental model they use for web applications: start a process, keep it alive, let it handle requests. The model feels natural because it is familiar. But it rests on an assumption that almost nobody questions. + +Agents need state. They need to remember what happened last time — what tasks are in progress, what decisions were made, what the user told them yesterday. If you kill the process, you lose the context. So the process stays alive. And a process that stays alive is a server. + +To make that server durable, teams add infrastructure. Redis for short-term memory. Postgres for long-term state. S3 for file storage. A message queue so the agent survives a restart. Before long, you have a distributed system — and a monthly bill — just to keep an agent's memory alive between conversations. + +The assumption nobody examined: state lives in a database, so compute must live near the database. + +GitAgent challenges this at the foundation. + +--- + +## Git Is Already a Database + +Every piece of state a GitAgent agent needs is stored in a git repository. Memory is a markdown file. Task history is a structured file. Audit logs are append-only. The agent's personality, goals, and behavioral constraints live in `agent.yaml` and `SOUL.md`. Everything that must survive a session is a file that git tracks. + +This is not a workaround. It is a deliberate architecture. + +Git is already distributed. It is already durable — commits are fsync'd to disk before returning. It is already versioned, meaning every state change has a timestamp, an author, and a reason. It is already replicated the moment you push to a remote. It has been battle-tested as a persistence layer by millions of teams for two decades. + +Every AI agent team building a custom state management layer is rebuilding something git already provides — worse, without the distribution, the versioning, or the auditability. + +The agent repo is not where the code lives. It is the database, the audit log, the memory store, and the deployment artifact simultaneously. + +--- + +## What Serverless Actually Means Here + +When you invoke GitAgent in single-shot mode, a precise lifecycle runs and terminates: + +The agent starts by cloning its repo and loading its identity from configuration files. It connects any declared tools and MCP servers. It runs the task. When the task completes, a `finally` block commits any state changes to git, pushes them to the remote, and exits. The process does not linger. Nothing idles. The compute existed for exactly as long as the work took. + +When you invoke it again — an hour later, a week later, on a different machine — it clones the same repo, reads the same memory file, and picks up exactly where it left off. The continuity of the agent is in git, not in a running process. The compute is disposable. The state is permanent. + +This is the inversion. Traditional agents keep compute alive to protect state. GitAgent makes state durable so compute can be ephemeral. + +--- + +## Memory That Survives Process Death + +The memory system is where this becomes concrete. + +GitAgent writes memory to a markdown file in the agent repo. Every save is a synchronous git commit — the write is durable before the function returns. If the process crashes after the commit, the memory is not lost. It is in git history. The next invocation reads the same file and continues. + +This is fundamentally different from in-memory state or a database that a long-running process manages. There is no connection to close, no transaction to roll back, no cache to warm. The state is just files. Files that git manages with the same reliability guarantees git has always provided. + +When memory grows large, older entries are archived automatically — moved to a dated archive file in the same atomic commit. The agent's knowledge base is self-managing without any server process watching over it. + +--- + +## $0 Between Runs + +The cost argument follows directly from the architecture. + +An always-on agent on a cloud VM costs money every hour, whether it processes one request or none. An agent on managed infrastructure — ECS, Cloud Run, Kubernetes — costs money to keep warm, to maintain availability, to replicate state. The infrastructure bill does not care whether your agent was useful today. + +A GitAgent agent costs nothing between invocations. You pay only for the seconds it is actually working. On GitHub Actions, the compute is not just cheap — for most usage patterns it is free entirely. The agent runs, commits its state, and the runner shuts down. There is no idle cost because there is no idle state. + +The agent repo itself is free on any public repository host. The memory, the skills, the audit log, the agent's entire history — stored at zero marginal cost in a git repository that would exist anyway. + +--- + +## Triggering an Ephemeral Agent + +Because the agent is stateless compute, any system that can run a command can trigger it. A GitHub Actions workflow on a schedule. A webhook handler in a serverless function. A CI pipeline step. A cron job on any machine. Even a developer running a one-liner from a terminal. + +The trigger mechanism does not matter because the agent does not care how it was invoked. It reads its state from the repo, does the work, commits the result, and exits. The scheduler is external infrastructure — managed, reliable, already paid for — not something the agent process has to maintain. + +This also means concurrent runs are naturally safe. Each session creates an isolated git branch. Ten parallel invocations produce ten branches, each with its own memory writes, none colliding with the others. Git's branching model gives you isolation without coordination, for free. + +--- + +## The Honest Caveat + +GitAgent ships with a built-in cron scheduler, accessible through its voice and web UI server. That scheduler runs inside a long-lived process — if you stop the server, scheduled jobs do not fire. + +This is the right trade-off for interactive use cases: a developer running a personal assistant locally, an agent that needs to respond to voice commands, a setup where sub-minute scheduling matters. + +For production workloads where reliability and cost matter — use an external scheduler to trigger single-shot runs. GitHub Actions, AWS EventBridge, GCP Cloud Scheduler, Render cron jobs — any of these will invoke the agent more reliably than an in-process scheduler, with no infrastructure to babysit, and with the agent's state persisting safely in git regardless. + +--- + +## The Bigger Shift + +The industry defaulted to always-on agents because it inherited the mental model of always-on services. But a service needs to be alive to handle requests. An agent needs to be capable — and capability lives in configuration files, memory, and learned skills, not in a running process. + +When you store agent state in git, you decouple capability from availability. The agent does not need to be running to exist. It does not need a server to remember. It does not need uptime to be useful. It needs a repo. + +This changes what it means to deploy an agent. There is no server to provision, no container to scale, no process to monitor. There is a repository. Fork it to create a new agent. Branch it to experiment. Tag it to pin a release. Push it to deploy. The entire operational model for AI agents collapses into git workflows that developers already know. + +You do not need infrastructure to run intelligence. You need a repo and a reason to invoke it. + +--- + +## Get Started + +```bash +npm install -g @open-gitagent/gitagent +gitagent --prompt "Hello from a serverless agent" +``` + +- Website: [gitagent.sh](https://gitagent.sh) +- Repo: [github.com/open-gitagent/gitagent](https://github.com/open-gitagent/gitagent) diff --git a/package-lock.json b/package-lock.json index 2316271..ae5a5aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@mariozechner/pi-agent-core": "^0.70.2", "@mariozechner/pi-ai": "^0.70.2", + "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.215.0", "@opentelemetry/exporter-trace-otlp-http": "^0.215.0", @@ -535,6 +536,18 @@ "node": ">=6" } }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -596,6 +609,46 @@ "zod-to-json-schema": "^3.25.0" } }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, "node_modules/@nodable/entities": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.1.tgz", @@ -1770,6 +1823,19 @@ "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -1800,6 +1866,39 @@ "node": ">= 14" } }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1880,6 +1979,43 @@ "node": "*" } }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", @@ -1892,6 +2028,44 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -1942,6 +2116,77 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -1982,6 +2227,29 @@ "node": ">= 14" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1991,12 +2259,57 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2006,6 +2319,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", @@ -2058,12 +2377,125 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz", + "integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fast-xml-builder": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", @@ -2124,6 +2556,27 @@ "node": "^12.20 || >= 14.13" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -2136,6 +2589,33 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gaxios": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", @@ -2173,6 +2653,43 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-uri": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", @@ -2222,17 +2739,82 @@ "node": ">=14" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, "engines": { - "node": ">= 14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" } }, "node_modules/https-proxy-agent": { @@ -2248,6 +2830,22 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/import-in-the-middle": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.0.1.tgz", @@ -2263,6 +2861,12 @@ "node": ">=18" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/ip-address": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", @@ -2272,6 +2876,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2281,6 +2894,27 @@ "node": ">=8" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", + "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-yaml": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", @@ -2325,6 +2959,18 @@ "node": ">=16" } }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2364,7 +3010,62 @@ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "license": "ISC", "engines": { - "node": ">=12" + "node": "20 || >=22" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/module-details-from-path": { @@ -2379,6 +3080,15 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", @@ -2438,6 +3148,48 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/openai": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", @@ -2504,6 +3256,15 @@ "node": ">= 14" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/partial-json": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", @@ -2525,6 +3286,34 @@ "node": ">=14.0.0" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/protobufjs": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.2.tgz", @@ -2549,6 +3338,19 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", @@ -2574,6 +3376,45 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -2583,6 +3424,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-in-the-middle": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", @@ -2605,6 +3455,22 @@ "node": ">= 4" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2625,6 +3491,156 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -2673,6 +3689,15 @@ "node": ">=0.10.0" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2711,6 +3736,15 @@ ], "license": "MIT" }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/ts-algebra": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", @@ -2723,6 +3757,37 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typebox": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.2.1.tgz", @@ -2758,6 +3823,15 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -2768,6 +3842,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -2777,6 +3860,21 @@ "node": ">= 8" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -2794,6 +3892,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.21.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", diff --git a/package.json b/package.json index 00aa77c..a2eca62 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "dependencies": { "@mariozechner/pi-agent-core": "^0.70.2", "@mariozechner/pi-ai": "^0.70.2", + "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.215.0", "@opentelemetry/exporter-trace-otlp-http": "^0.215.0", diff --git a/src/env-utils.ts b/src/env-utils.ts new file mode 100644 index 0000000..799e7ee --- /dev/null +++ b/src/env-utils.ts @@ -0,0 +1,36 @@ +/** + * Shared `${VAR}` environment-variable interpolation. + * + * Replaces `${VAR_NAME}` occurrences in strings with `process.env.VAR_NAME`, + * or an empty string when the variable is unset (matching the long-standing + * plugin-config behavior). Recurses through arrays and plain objects so an + * entire config object (e.g. an MCP server definition) can be interpolated in + * one call. Non-string leaf values are returned unchanged. + */ +const ENV_VAR_PATTERN = /\$\{(\w+)\}/g; + +export function interpolateEnvString(value: string): string { + return value.replace(ENV_VAR_PATTERN, (_, envName) => { + if (process.env[envName] === undefined) { + console.warn(`[mcp] env var ${envName} is not set; substituting empty string`); + } + return process.env[envName] ?? ""; + }); +} + +export function interpolateEnv<T>(value: T): T { + if (typeof value === "string") { + return interpolateEnvString(value) as unknown as T; + } + if (Array.isArray(value)) { + return value.map((item) => interpolateEnv(item)) as unknown as T; + } + if (value && typeof value === "object") { + const out: Record<string, any> = {}; + for (const [key, v] of Object.entries(value as Record<string, any>)) { + out[key] = interpolateEnv(v); + } + return out as unknown as T; + } + return value; +} diff --git a/src/exports.ts b/src/exports.ts index 787d60f..514bc3a 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -23,6 +23,7 @@ export type { // Internal types (for advanced usage) export type { AgentManifest, LoadedAgent } from "./loader.js"; +export type { McpServerConfig } from "./mcp/types.js"; export type { SkillMetadata } from "./skills.js"; export type { WorkflowMetadata } from "./workflows.js"; export type { SubAgentMetadata } from "./agents.js"; diff --git a/src/index.ts b/src/index.ts index 68ea4ca..0ffe99b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import { expandSkillCommand, refreshSkills } from "./skills.js"; import { loadHooksConfig, runHooks, wrapToolWithHooks } from "./hooks.js"; import type { HooksConfig } from "./hooks.js"; import { loadDeclarativeTools } from "./tool-loader.js"; +import { setupMcp } from "./mcp/manager.js"; import { toAgentTool } from "./tool-utils.js"; import { AuditLogger, isAuditEnabled } from "./audit.js"; import { formatComplianceWarnings } from "./compliance.js"; @@ -559,6 +560,10 @@ async function main(): Promise<void> { } } + // MCP tools (manifest-declared servers) + const mcpSetup = await setupMcp(manifest.mcp_servers, existingToolNames); + tools.push(...mcpSetup.tools); + // Wrap with hooks if configured if (hooksConfig) { tools = tools.map((t) => wrapToolWithHooks(t, hooksConfig, agentDir, sessionId)); @@ -651,6 +656,7 @@ async function main(): Promise<void> { } throw err; } finally { + await mcpSetup.cleanup().catch(() => {}); if (localSession) { console.log(dim("Finalizing session...")); localSession.finalize(); @@ -685,6 +691,7 @@ async function main(): Promise<void> { if (trimmed === "/quit" || trimmed === "/exit") { rl.close(); + await mcpSetup.cleanup().catch(() => {}); if (localSession) { console.log(dim("Finalizing session...")); localSession.finalize(); @@ -836,7 +843,7 @@ async function main(): Promise<void> { try { _session.end({ "gitagent.cost_usd": _totalCostUsd }); } catch { /* ignore */ } - stopSandbox().finally(() => process.exit(0)); + Promise.all([mcpSetup.cleanup(), stopSandbox()]).finally(() => process.exit(0)); } }); diff --git a/src/loader.ts b/src/loader.ts index b8d193e..3fe06ba 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -22,6 +22,7 @@ import type { ComplianceWarning } from "./compliance.js"; import { discoverAndLoadPlugins } from "./plugins.js"; import type { LoadedPlugin } from "./plugin-types.js"; import type { PluginConfig } from "./plugin-types.js"; +import type { McpServerConfig } from "./mcp/types.js"; export interface AgentManifest { spec_version: string; @@ -55,6 +56,7 @@ export interface AgentManifest { delegation?: { mode: "auto" | "explicit" | "router"; router?: string }; compliance?: Record<string, any>; plugins?: Record<string, PluginConfig>; + mcp_servers?: Record<string, McpServerConfig>; } async function readFileOr(path: string, fallback: string): Promise<string> { diff --git a/src/mcp/manager.ts b/src/mcp/manager.ts new file mode 100644 index 0000000..ac29d69 --- /dev/null +++ b/src/mcp/manager.ts @@ -0,0 +1,262 @@ +import { createRequire } from "node:module"; +import { buildTool } from "../tool-factory.js"; +import { interpolateEnv } from "../env-utils.js"; +import type { AgentTool } from "@mariozechner/pi-agent-core"; +import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import type { McpServerConfig, McpStdioServerConfig, McpSetupResult } from "./types.js"; + +const _require = createRequire(import.meta.url); +const { version: PACKAGE_VERSION } = _require("../../package.json") as { version: string }; + +const DEFAULT_TIMEOUT_MS = 30000; + +/** A live connection to one MCP server. */ +interface McpConnection { + name: string; + client: Client; + close: () => Promise<void>; +} + +function withTimeout<T>(op: Promise<T>, ms: number, label: string): Promise<T> { + return new Promise<T>((resolve, reject) => { + const timer = setTimeout( + () => reject(new Error(`${label} timed out after ${ms}ms`)), + ms, + ); + op.then( + (v) => { clearTimeout(timer); resolve(v); }, + (e) => { clearTimeout(timer); reject(e); }, + ); + }); +} + +/** + * Flatten an MCP tool result (an array of content blocks plus an optional + * `isError` flag) into the plain string that an AgentTool's execute() returns. + * Binary blocks are summarized rather than inlined to protect the token budget. + */ +export function flattenToolResult(result: any): string { + const blocks: any[] = Array.isArray(result?.content) ? result.content : []; + const parts: string[] = []; + for (const block of blocks) { + switch (block?.type) { + case "text": + parts.push(String(block.text ?? "")); + break; + case "image": + parts.push(`[image: ${block.mimeType || "unknown"}, data omitted]`); + break; + case "audio": + parts.push(`[audio: ${block.mimeType || "unknown"}, data omitted]`); + break; + case "resource": { + const res = block.resource ?? {}; + if (typeof res.text === "string") parts.push(res.text); + else parts.push(`[resource: ${res.uri || "unknown"} (${res.mimeType || "binary"})]`); + break; + } + case "resource_link": + parts.push(`[resource_link: ${block.uri || "unknown"}]`); + break; + default: + if (block?.text) parts.push(String(block.text)); + break; + } + } + let text = parts.join("\n"); + // Some tools (those with an outputSchema) return only `structuredContent` + // with no text blocks — fall back to its JSON so the model isn't handed "". + if (!text && result?.structuredContent !== undefined) { + try { + text = JSON.stringify(result.structuredContent); + } catch { + /* leave text empty if not serializable */ + } + } + return result?.isError ? `Error: ${text}` : text; +} + +/** + * Make a tool name safe for LLM provider APIs, which require names to match + * roughly `^[a-zA-Z0-9_-]{1,64}$` (OpenAI/Anthropic). Invalid characters become + * `_`; names longer than 64 chars are truncated. Collisions after truncation + * are caught by the existing-name check in setupMcp. + */ +function sanitizeToolName(name: string): string { + const cleaned = name.replace(/[^a-zA-Z0-9_-]/g, "_"); + return cleaned.length > 64 ? cleaned.slice(0, 64) : cleaned; +} + +/** List every tool a server offers, following pagination cursors to the end. */ +async function listAllTools(client: Client): Promise<any[]> { + const all: any[] = []; + let cursor: string | undefined; + do { + const res = await client.listTools(cursor ? { cursor } : undefined); + all.push(...res.tools); + cursor = res.nextCursor; + } while (cursor); + return all; +} + +/** + * List a connected server's tools and convert each into an AgentTool. + * Tools are namespaced `<server>__<tool>` (sanitized for provider name rules) + * to avoid collisions. Exported so tests can exercise conversion over an + * in-memory transport without spawning. + */ +export async function buildToolsForConnection(conn: McpConnection): Promise<AgentTool<any>[]> { + const tools = await listAllTools(conn.client); + return tools.map((t) => + buildTool({ + name: sanitizeToolName(`${conn.name}__${t.name}`), + description: t.description || `MCP tool ${t.name} from ${conn.name}`, + // MCP inputSchema is a JSON-Schema object ({type, properties, required}). + // buildTypeboxSchema converts it recursively (integers, enums, typed + // arrays, nested objects); unknown shapes degrade to Type.Any. + parameters: (t.inputSchema as Record<string, any>) ?? {}, + execute: async (args: any, signal?: AbortSignal) => { + try { + // Forward the agent's abort signal so cancelling the turn cancels + // the in-flight MCP request. Call with the original (unsanitized) name. + const result = await conn.client.callTool( + { name: t.name, arguments: args ?? {} }, + undefined, + { signal }, + ); + return flattenToolResult(result); + } catch (err: any) { + // Protocol-level errors (e.g. -32602) throw; surface them as a + // string so the agent can read and recover instead of crashing. + return `Error: ${err?.message || err}`; + } + }, + // We can't know a remote tool's semantics — fail closed. + metadata: { isConcurrencySafe: false, isReadOnly: false, isDestructive: false }, + }), + ); +} + +/** Connect to a single server. Fail-soft: logs and returns null on any error. */ +async function connectServer(name: string, rawCfg: McpServerConfig): Promise<McpConnection | null> { + const cfg = interpolateEnv(rawCfg); + const timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS; + try { + const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); + + let transport: any; + if (cfg.type === "http") { + if (!cfg.url) throw new Error("http server requires a url"); + const { StreamableHTTPClientTransport } = await import( + "@modelcontextprotocol/sdk/client/streamableHttp.js" + ); + transport = new StreamableHTTPClientTransport(new URL(cfg.url), { + requestInit: cfg.headers ? { headers: cfg.headers } : undefined, + }); + } else if (cfg.type === "sse") { + if (!cfg.url) throw new Error("sse server requires a url"); + const { SSEClientTransport } = await import("@modelcontextprotocol/sdk/client/sse.js"); + transport = new SSEClientTransport(new URL(cfg.url), { + requestInit: cfg.headers ? { headers: cfg.headers } : undefined, + }); + } else { + const stdio = cfg as McpStdioServerConfig; + if (!stdio.command) throw new Error("stdio server requires a command"); + const { StdioClientTransport, getDefaultEnvironment } = await import( + "@modelcontextprotocol/sdk/client/stdio.js" + ); + transport = new StdioClientTransport({ + command: stdio.command, + args: stdio.args ?? [], + env: { ...getDefaultEnvironment(), ...(stdio.env ?? {}) }, + cwd: stdio.cwd, + }); + } + + const client = new Client({ name: "gitagent", version: PACKAGE_VERSION }, { capabilities: {} }); + try { + await withTimeout(client.connect(transport), timeoutMs, `[mcp:${name}] connect`); + } catch (connectErr) { + // connect() spawns the child process / opens the socket. If it fails or + // times out mid-handshake, close the transport so we don't leak the + // spawned process or connection. + try { await transport?.close?.(); } catch { /* best-effort */ } + throw connectErr; + } + + return { + name, + client, + close: async () => { + try { + await client.close(); + } catch { + /* best-effort */ + } + }, + }; + } catch (err: any) { + console.warn(`[mcp:${name}] failed to connect: ${err?.message || err} — skipping`); + return null; + } +} + +/** + * Connect to all configured MCP servers, register their tools, and return an + * idempotent cleanup. Servers connect in parallel and independently fail-soft — + * one bad server never blocks the others. Returns immediately with no work (and + * without loading the MCP SDK) when no servers are configured. + * + * @param servers merged server map (manifest + SDK options) + * @param existingToolNames running set of tool names already registered; used + * for collision detection and updated in place. + */ +export async function setupMcp( + servers: Record<string, McpServerConfig> | undefined, + existingToolNames: Set<string>, +): Promise<McpSetupResult> { + const entries = Object.entries(servers ?? {}); + if (entries.length === 0) { + return { tools: [], cleanup: async () => {} }; + } + + const settled = await Promise.allSettled( + entries.map(([name, cfg]) => connectServer(name, cfg)), + ); + const connections = settled + .map((s) => (s.status === "fulfilled" ? s.value : null)) + .filter((c): c is McpConnection => c !== null); + + const tools: AgentTool<any>[] = []; + for (const conn of connections) { + let built: AgentTool<any>[]; + try { + built = await withTimeout( + buildToolsForConnection(conn), + DEFAULT_TIMEOUT_MS, + `[mcp:${conn.name}] listTools`, + ); + } catch (err: any) { + console.warn(`[mcp:${conn.name}] failed to list tools: ${err?.message || err} — skipping`); + await conn.close(); + continue; + } + for (const tool of built) { + if (existingToolNames.has(tool.name)) { + console.warn(`[mcp:${conn.name}] tool "${tool.name}" collides with an existing tool — skipping`); + continue; + } + existingToolNames.add(tool.name); + tools.push(tool); + } + } + + let closed = false; + const cleanup = async () => { + if (closed) return; + closed = true; + await Promise.allSettled(connections.map((c) => c.close())); + }; + + return { tools, cleanup }; +} diff --git a/src/mcp/types.ts b/src/mcp/types.ts new file mode 100644 index 0000000..a609d93 --- /dev/null +++ b/src/mcp/types.ts @@ -0,0 +1,31 @@ +import type { AgentTool } from "@mariozechner/pi-agent-core"; + +/** A local MCP server launched as a child process, spoken to over stdio. */ +export interface McpStdioServerConfig { + type?: "stdio"; + command: string; + args?: string[]; + env?: Record<string, string>; + cwd?: string; + /** Connect + initial listTools timeout in ms. Default 30000. */ + timeoutMs?: number; +} + +/** A remote MCP server reached over Streamable HTTP (or legacy SSE). */ +export interface McpHttpServerConfig { + type: "http" | "sse"; + url: string; + headers?: Record<string, string>; + /** Connect + initial listTools timeout in ms. Default 30000. */ + timeoutMs?: number; +} + +export type McpServerConfig = McpStdioServerConfig | McpHttpServerConfig; + +/** Result of wiring up all configured MCP servers for a session. */ +export interface McpSetupResult { + /** Tools discovered across all servers, namespaced `<server>__<tool>`. */ + tools: AgentTool<any>[]; + /** Idempotent teardown — closes every transport/client. Safe to call repeatedly. */ + cleanup: () => Promise<void>; +} diff --git a/src/plugins.ts b/src/plugins.ts index 8a22899..5ab6171 100644 --- a/src/plugins.ts +++ b/src/plugins.ts @@ -4,6 +4,7 @@ import { execFileSync } from "child_process"; import { createRequire } from "module"; import { homedir } from "os"; import yaml from "js-yaml"; +import { interpolateEnvString } from "./env-utils.js"; import type { AgentTool } from "@mariozechner/pi-agent-core"; import type { PluginManifest, @@ -73,7 +74,7 @@ function resolvePluginConfig( let value = userConfig[key]; // Resolve ${ENV_VAR} syntax if (typeof value === "string") { - value = value.replace(/\$\{(\w+)\}/g, (_, envName) => process.env[envName] || ""); + value = interpolateEnvString(value); } resolved[key] = value; } else if (prop.env && process.env[prop.env]) { diff --git a/src/sdk-types.ts b/src/sdk-types.ts index 20dc060..5ab79ca 100644 --- a/src/sdk-types.ts +++ b/src/sdk-types.ts @@ -1,5 +1,6 @@ import type { AgentManifest } from "./loader.js"; import type { SessionCosts } from "./cost-tracker.js"; +import type { McpServerConfig } from "./mcp/types.js"; // ── Message types ────────────────────────────────────────────────────── @@ -145,6 +146,8 @@ export interface QueryOptions { maxTurns?: number; abortController?: AbortController; sessionId?: string; + /** MCP servers to connect to. Merged with manifest `mcp_servers` (these win on key collision). */ + mcpServers?: Record<string, McpServerConfig>; constraints?: { temperature?: number; maxTokens?: number; diff --git a/src/sdk.ts b/src/sdk.ts index dfa9a80..55b941c 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -9,6 +9,8 @@ import type { SandboxContext } from "./sandbox.js"; import { loadHooksConfig, runHooks, wrapToolWithHooks } from "./hooks.js"; import { loadDeclarativeTools } from "./tool-loader.js"; import { toAgentTool } from "./tool-utils.js"; +import { setupMcp } from "./mcp/manager.js"; +import type { McpSetupResult } from "./mcp/types.js"; import { wrapToolWithProgrammaticHooks } from "./sdk-hooks.js"; import { mergeHooksConfigs } from "./plugins.js"; import { initLocalSession } from "./session.js"; @@ -109,6 +111,7 @@ export function query(options: QueryOptions): Query { // Sandbox context (hoisted for cleanup in catch) let sandboxCtx: SandboxContext | undefined; + let mcpSetup: McpSetupResult | undefined; // Local session (hoisted for cleanup in catch) let localSession: LocalSession | undefined; @@ -206,6 +209,11 @@ export function query(options: QueryOptions): Query { } } + // MCP tools — merge manifest + SDK server configs (SDK wins on key collision) + const mcpServers = { ...loaded.manifest.mcp_servers, ...options.mcpServers }; + mcpSetup = await setupMcp(mcpServers, existingToolNames); + tools = [...tools, ...mcpSetup.tools]; + // SDK-provided tools if (options.tools) { const converted = options.tools.map(toAgentTool); @@ -550,6 +558,12 @@ export function query(options: QueryOptions): Query { // Ensure channel finishes even if no agent_end event channel.finish(); } finally { + // Tear down MCP servers on every exit path — success, hook-block + // early-return, abort, and error (this finally runs before the + // .catch() handler below). cleanup() is idempotent. + if (mcpSetup) { + try { await mcpSetup.cleanup(); } catch { /* best-effort */ } + } // Close the session span on every exit path — success, hook-block // early-return, and the .catch() handler below (rethrow so this // runs first). diff --git a/src/tool-loader.ts b/src/tool-loader.ts index 5c58f02..13ac32d 100644 --- a/src/tool-loader.ts +++ b/src/tool-loader.ts @@ -16,35 +16,67 @@ interface ToolDefinition { }; } -export function buildTypeboxSchema(schema: Record<string, any>): any { - // Convert a simplified JSON-schema-like object to Typebox properties +/** + * Recursively convert a JSON-Schema node into a TypeBox schema. Handles the + * full set of types tool authors and MCP servers use: string/number/integer/ + * boolean/null, enums (→ union of literals), typed arrays (real item type), + * nested objects (real properties + required), and union `type` arrays. Unknown + * or missing types degrade to `Type.Any()` so the call still goes through and + * the server validates the actual payload. + */ +function jsonSchemaToTypebox(def: any): any { + if (!def || typeof def !== "object") return Type.Any(); + const desc = def.description || ""; + const opts = desc ? { description: desc } : {}; + + // enum → union of literals (preserves the allowed values for the model) + if (Array.isArray(def.enum) && def.enum.length > 0) { + if (def.enum.length === 1) return Type.Literal(def.enum[0], opts); + return Type.Union(def.enum.map((v: any) => Type.Literal(v)), opts); + } + + // union type, e.g. ["string", "null"] + if (Array.isArray(def.type)) { + const variants = def.type.map((t: string) => jsonSchemaToTypebox({ ...def, type: t, description: undefined })); + return variants.length === 1 ? variants[0] : Type.Union(variants, opts); + } + + switch (def.type) { + case "string": + return Type.String(opts); + case "number": + return Type.Number(opts); + case "integer": + return Type.Integer(opts); + case "boolean": + return Type.Boolean(opts); + case "null": + return Type.Null(opts); + case "array": + return Type.Array(def.items ? jsonSchemaToTypebox(def.items) : Type.Any(), opts); + case "object": + return buildTypeboxSchema(def, opts); + default: + // No/unknown type — fall back to permissive Any. + return Type.Any(opts); + } +} + +/** + * Build a TypeBox object schema from a JSON-Schema-like object (with + * `properties` and `required`). Used for both declarative YAML tools and MCP + * tool input schemas. + */ +export function buildTypeboxSchema(schema: Record<string, any>, opts: Record<string, any> = {}): any { const properties: Record<string, any> = {}; if (schema.properties) { for (const [key, def] of Object.entries(schema.properties) as [string, any][]) { - const desc = def.description || ""; const required = schema.required?.includes(key) ?? false; - let prop; - switch (def.type) { - case "number": - prop = Type.Number({ description: desc }); - break; - case "boolean": - prop = Type.Boolean({ description: desc }); - break; - case "array": - prop = Type.Array(Type.Any(), { description: desc }); - break; - case "object": - prop = Type.Any({ description: desc }); - break; - default: - prop = Type.String({ description: desc }); - break; - } + const prop = jsonSchemaToTypebox(def); properties[key] = required ? prop : Type.Optional(prop); } } - return Type.Object(properties); + return Type.Object(properties, opts); } function createDeclarativeTool( diff --git a/test/mcp.test.ts b/test/mcp.test.ts new file mode 100644 index 0000000..c9efb17 --- /dev/null +++ b/test/mcp.test.ts @@ -0,0 +1,167 @@ +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { z } from "zod"; + +// Dynamic imports since the project is ESM and tests run against compiled dist. +let buildToolsForConnection: typeof import("../dist/mcp/manager.js").buildToolsForConnection; +let flattenToolResult: typeof import("../dist/mcp/manager.js").flattenToolResult; +let setupMcp: typeof import("../dist/mcp/manager.js").setupMcp; +let Client: typeof import("@modelcontextprotocol/sdk/client/index.js").Client; +let McpServer: typeof import("@modelcontextprotocol/sdk/server/mcp.js").McpServer; +let InMemoryTransport: typeof import("@modelcontextprotocol/sdk/inMemory.js").InMemoryTransport; + +before(async () => { + const mgr = await import("../dist/mcp/manager.js"); + buildToolsForConnection = mgr.buildToolsForConnection; + flattenToolResult = mgr.flattenToolResult; + setupMcp = mgr.setupMcp; + Client = (await import("@modelcontextprotocol/sdk/client/index.js")).Client; + McpServer = (await import("@modelcontextprotocol/sdk/server/mcp.js")).McpServer; + InMemoryTransport = (await import("@modelcontextprotocol/sdk/inMemory.js")).InMemoryTransport; +}); + +/** Stand up an in-memory MCP server exposing echo/boom/pic tools, return a connected Client. */ +async function connectInMemoryServer() { + const server = new McpServer({ name: "test-server", version: "1.0.0" }); + + server.registerTool( + "echo", + { description: "Echo a message", inputSchema: { msg: z.string() } }, + async ({ msg }) => ({ content: [{ type: "text", text: msg }] }), + ); + server.registerTool( + "boom", + { description: "Always errors", inputSchema: {} }, + async () => ({ content: [{ type: "text", text: "kaboom" }], isError: true }), + ); + server.registerTool( + "pic", + { description: "Returns an image", inputSchema: {} }, + async () => ({ content: [{ type: "image", data: "AAAA", mimeType: "image/png" }] }), + ); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "test-client", version: "1.0.0" }, { capabilities: {} }); + await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]); + return { client, server }; +} + +describe("flattenToolResult", () => { + it("joins text blocks", () => { + assert.equal(flattenToolResult({ content: [{ type: "text", text: "a" }, { type: "text", text: "b" }] }), "a\nb"); + }); + it("prefixes Error: when isError is true", () => { + assert.equal(flattenToolResult({ content: [{ type: "text", text: "bad" }], isError: true }), "Error: bad"); + }); + it("summarizes image blocks without inlining data", () => { + const out = flattenToolResult({ content: [{ type: "image", data: "AAAA", mimeType: "image/png" }] }); + assert.equal(out, "[image: image/png, data omitted]"); + assert.ok(!out.includes("AAAA")); + }); + it("handles embedded resource text and links", () => { + assert.equal(flattenToolResult({ content: [{ type: "resource", resource: { uri: "file://x", text: "hi" } }] }), "hi"); + assert.equal(flattenToolResult({ content: [{ type: "resource_link", uri: "file://y" }] }), "[resource_link: file://y]"); + }); + it("tolerates empty/malformed results", () => { + assert.equal(flattenToolResult({}), ""); + assert.equal(flattenToolResult(null), ""); + }); + it("falls back to structuredContent JSON when there are no text blocks", () => { + assert.equal( + flattenToolResult({ content: [], structuredContent: { count: 42 } }), + '{"count":42}', + ); + }); + it("prefers text content over structuredContent when both exist", () => { + assert.equal( + flattenToolResult({ content: [{ type: "text", text: "hi" }], structuredContent: { x: 1 } }), + "hi", + ); + }); +}); + +describe("buildToolsForConnection", () => { + it("namespaces tool names and wires execute through callTool", async () => { + const { client } = await connectInMemoryServer(); + const conn = { name: "srv", client, close: async () => { await client.close(); } }; + const tools = await buildToolsForConnection(conn as any); + + const names = tools.map((t) => t.name).sort(); + assert.deepEqual(names, ["srv__boom", "srv__echo", "srv__pic"]); + + const echo = tools.find((t) => t.name === "srv__echo")!; + const res = await echo.execute("call-1", { msg: "hello" }); + assert.equal(res, "hello"); + + const boom = tools.find((t) => t.name === "srv__boom")!; + const boomRes = await boom.execute("call-2", {}); + assert.match(boomRes, /^Error: /); + + const pic = tools.find((t) => t.name === "srv__pic")!; + const picRes = await pic.execute("call-3", {}); + assert.match(picRes, /\[image: image\/png/); + + await conn.close(); + }); +}); + +describe("buildToolsForConnection — pagination & name sanitization", () => { + it("follows nextCursor across pages and sanitizes/caps tool names", async () => { + // A mock client that paginates listTools across two pages. + const longName = "x".repeat(100); + const mockClient: any = { + async listTools(params: any) { + if (!params?.cursor) { + return { tools: [{ name: "do.it", inputSchema: {} }], nextCursor: "page2" }; + } + return { tools: [{ name: longName, inputSchema: {} }] }; // no nextCursor → end + }, + async callTool() { + return { content: [{ type: "text", text: "ok" }] }; + }, + }; + const tools = await buildToolsForConnection({ name: "srv", client: mockClient, close: async () => {} }); + + // both pages collected + assert.equal(tools.length, 2); + // invalid char "." sanitized to "_" + assert.equal(tools[0].name, "srv__do_it"); + // long name capped to 64 and only valid chars + assert.ok(tools[1].name.length <= 64); + assert.match(tools[1].name, /^[a-zA-Z0-9_-]+$/); + }); + + it("returns a clean 'Error:' string when callTool throws (protocol error)", async () => { + const mockClient: any = { + async listTools() { + return { tools: [{ name: "bad", inputSchema: {} }] }; + }, + async callTool() { + throw new Error("MCP error -32602: Invalid arguments"); + }, + }; + const tools = await buildToolsForConnection({ name: "srv", client: mockClient, close: async () => {} }); + const res = await tools[0].execute("c1", {}); + assert.match(res, /^Error: .*-32602/); + }); +}); + +describe("setupMcp", () => { + it("returns empty tools and a no-op cleanup when no servers configured", async () => { + const result = await setupMcp(undefined, new Set()); + assert.deepEqual(result.tools, []); + // cleanup is idempotent / safe to call repeatedly + await result.cleanup(); + await result.cleanup(); + }); + + it("fails soft on an unreachable server (bad command) without throwing", async () => { + const existing = new Set<string>(); + const result = await setupMcp( + { broken: { command: "definitely-not-a-real-binary-xyz", args: [], timeoutMs: 2000 } }, + existing, + ); + assert.deepEqual(result.tools, []); + await result.cleanup(); + }); +});