diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c05c624..7c0f93d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,54 @@ Product: https://apilium.com/us/products/mayros Download: https://mayros.apilium.com Docs: https://apilium.com/us/doc/mayros +## 0.1.15 (2026-03-12) + +MCP Server production-ready, Claude Desktop and Claude Code integration, documentation, and product page update. + +### MCP Server + +- 9 tools exposed via Model Context Protocol: memory (remember, recall, search, forget), budget, governance, cortex (query, store, stats) +- Cortex sidecar auto-starts on `mayros serve`, persistent storage at `~/.mayros/cortex-data/` +- Dual transport: `--stdio` for IDE/Claude Desktop, `--http` for remote clients +- Default port aligned to Mayros convention: 19100 + +### MCP Setup + +- `mayros mcp-setup` — one-command registration for Claude Code (stdio or HTTP) +- `mayros mcp-setup --desktop` — auto-configures Claude Desktop config file +- Resolves absolute paths to `node` and `mayros.mjs` for Claude Desktop compatibility +- Cross-platform config detection: macOS, Windows, Linux + +### Documentation + +- New: `tools/mcp-server.mdx` — architecture, 9 tools reference, setup guides, configuration +- New: `cli/mcp-setup.mdx` — CLI reference with options and platform-specific paths +- Updated: `cli/serve.mdx` — port 19100, tools table, Cortex sidecar section +- Updated: `README.md` — step-by-step MCP setup guides, usage examples + +### Product Page + +- New capability cards: Intelligent Model Routing (Q-learning) and Policy Enforcement (governance) +- Updated capabilities: HNSW vector search, Byzantine consensus, response caching, budget tracking +- Updated architecture layers: Q-learning routing, governance gates, MCP Server, WASM transforms +- Security layers expanded from 6 to 10 (governance gates, HMAC audit trail, trust tiers, rate limiting) +- Updated numbers: 67 extensions, 75+ CLI commands, 20 security layers +- FAQ updated with MCP server and HNSW references + +### Badges + +- MCP Compatible badge (shields.io) +- Works with Claude badge (Anthropic logo) + +### Infrastructure + +- Require AIngle Cortex >= 0.4.3 + +--- + ## 0.1.14 (2026-03-11) -Intelligent routing, multi-agent consensus, and execution safety. +Intelligent routing, multi-agent consensus, execution safety, code transforms, governance, dual-platform coordination, and MCP server enhancements. ### Eruberu — Adaptive Model Routing @@ -54,10 +99,50 @@ Intelligent routing, multi-agent consensus, and execution safety. - `buildFromPricingCatalog()`: construct router from token-economy pricing catalog - `routeWithBudget()`: budget-aware routing that filters by remaining spend +### Hayameru — WASM Code Transforms + +- Deterministic code transforms that bypass LLM for simple edits (0 tokens, sub-millisecond) +- Intent detector: keyword-based prompt classification with confidence scoring +- 5 transforms: var→const, remove console, sort imports, add semicolons, remove comments +- Path safety validation and atomic file writes +- Integrates via `before_agent_run` hook — short-circuits LLM when confidence is high +- Metrics tracking: token savings, transform counts, timing + +### Kimeru — Byzantine & Raft Consensus + +- Byzantine fault tolerance: HMAC-SHA256 signed votes, PBFT phases (pre-prepare → prepare → commit) +- Quorum math: 2f+1 agreement required, minimum 4 agents, auto-fallback to weighted +- Raft leader election: highest EMA score wins, majority follower confirmation +- Re-election support with agent exclusion + +### Osameru — Governance Control Plane + +- Policy compiler: parses MAYROS.md for ALLOW/DENY/REQUIRE-APPROVAL rules +- Enforcement gate: evaluates tool calls, agent starts, and content against policy bundle +- HMAC-signed append-only audit trail with hash chain integrity verification +- Trust tiers: 3-level system (new → established → trusted) based on EMA performance scores +- Configurable modes: enforce, warn, audit-only, off + +### Kakeru — Dual-Platform Bridge + +- Platform bridge interface for heterogeneous agent coordination +- Claude bridge: native subagent integration (always connected) +- Codex bridge: subprocess-based OpenAI Codex CLI integration with git branch isolation +- Coordinator: parallel task execution, file lock coordination, branch management + +### MCP Server Enhancements + +- 9 dedicated MCP tools: remember, recall, search, forget, budget, policy_check, cortex_query, cortex_store, memory_stats +- Auto-start Cortex sidecar when running `mayros serve` +- Legacy SSE transport (MCP spec 2024-11-05) for Claude Desktop compatibility +- `mayros mcp-setup` command for one-step registration in Claude Code +- Enhanced health endpoint with Cortex sidecar status + ### Infrastructure - 55 extensions synced at v0.1.14 -- 55 new tests across 7 test files (Q-Learning, task classification, routing, performance tracking, consensus, rate limiting, loop breaking) +- 112 Phase 2 tests across 16 test files (transforms, intent detection, Byzantine consensus, Raft election, policy compilation, audit trail, trust tiers, enforcement, platform coordination) +- 55 Phase 1 tests across 7 test files (Q-Learning, task classification, routing, performance tracking, consensus, rate limiting, loop breaking) - Auto-release workflow: GitHub Releases created automatically on version tags ## 0.1.13 (2026-03-08) diff --git a/Dockerfile b/Dockerfile index 176afa35..1541b33a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:25-bookworm@sha256:c4bfed36421c310d1fbb6dc51faf98065768fbc1c2c1ddd554813ecaa81bb2db +FROM node:25-bookworm@sha256:2e45682ea560ac050cca0fd1ff5e82457a717a98e95e30bbf93306833a31332c # Install Bun (required for build scripts) RUN curl -fsSL https://bun.sh/install | bash diff --git a/README.md b/README.md index 7f992861..cb5fc1c9 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ TypeScript 55 extensions macOS | Linux + MCP Compatible + Works with Claude

@@ -31,9 +33,9 @@ --- -**Mayros** is an open-source AI agent framework that runs on your own devices. It ships with an interactive **coding CLI** (`mayros code`), connects to **17 messaging channels** (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Teams, and more), speaks and listens on **macOS/iOS/Android**, and has a **knowledge graph** that remembers everything across sessions. All backed by a local-first Gateway and an 18-layer security architecture. +**Mayros** is an open-source AI agent framework that runs on your own devices. It ships with an interactive **coding CLI** (`mayros code`), connects to **17 messaging channels** (WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Teams, and more), speaks and listens on **macOS/iOS/Android**, and has a **knowledge graph** that remembers everything across sessions. All backed by a local-first Gateway and an 20-layer security architecture. -> **55 extensions · 9,200+ tests · 29 hooks · MCP support · Multi-model · Multi-agent** +> **55 extensions · 11,700+ tests · 29 hooks · MCP server & client · Multi-model · Multi-agent** ```bash npm install -g @apilium/mayros@latest @@ -50,11 +52,11 @@ mayros code # interactive coding CLI | 🧠 **Knowledge Graph** | AIngle Cortex — persistent memory across sessions, projects, and agents | Flat conversation history | | 🤖 **Multi-Agent** | Teams, workflows, mailbox, background tasks, git worktree isolation | Single agent | | 📱 **Multi-Channel** | 17 channels — WhatsApp, Telegram, Slack, Discord, Signal, iMessage, Teams, Matrix, WebChat, and more | Terminal only | -| 🔒 **Security** | 18 layers — WASM sandbox, bash scanner, interactive permissions, namespace isolation, rate limiter | Basic sandboxing | +| 🔒 **Security** | 20 layers — WASM sandbox, bash scanner, interactive permissions, namespace isolation, rate limiter | Basic sandboxing | | 🎙️ **Voice** | Always-on Voice Wake + Talk Mode on macOS, iOS, Android | None | | 🖥️ **IDE** | VSCode + JetBrains plugins with chat, plan, traces, KG | VSCode only | | 📊 **Observability** | Full trace system, decision graph, session fork/rewind | Basic logging | -| 🔌 **Extensions** | 55 plugin extensions, 29 hook types, MCP client (4 transports) | Limited plugins | +| 🔌 **Extensions** | 55 plugin extensions, 29 hook types, MCP server + client (4 transports) | Limited plugins | | 🗺️ **Plan Mode** | Cortex-backed semantic planning: explore → assert → approve → execute | Simple plan files | --- @@ -157,8 +159,10 @@ Full beginner guide: **[Getting started](https://apilium.com/en/doc/mayros/start │ ┌────────────┬───────────┼───────────┬────────────┐ │ │ │ │ │ - mayros code VSCode / Pi Agent macOS App iOS/Android - (TUI) JetBrains (RPC) (menu bar) Nodes + mayros code VSCode / Pi Agent macOS App MCP Server + (TUI) JetBrains (RPC) (menu bar) :19100 + Claude Desktop + Claude Code ``` The Gateway is the single control plane — every client, channel, tool, and event connects through it. @@ -205,16 +209,91 @@ CLI: `mayros kg search|explore|query|stats|triples|namespaces|export|import` --- +## MCP Server + +Mayros exposes its tools, resources, and prompts via the [Model Context Protocol](https://modelcontextprotocol.io). Any MCP client — Claude Desktop, Claude Code, VSCode, Cursor, JetBrains — can discover and use Mayros capabilities. + +### Connect with Claude Desktop + +```bash +# 1. Install Mayros +npm install -g @apilium/mayros@latest + +# 2. Register with Claude Desktop (auto-detects paths, writes config) +mayros mcp-setup --desktop + +# 3. Restart Claude Desktop — done +# The tools icon appears in the chat input +``` + +Then in Claude Desktop, just talk naturally: + +- _"Remember that our API uses JWT tokens with 24h expiry"_ → stores in semantic memory +- _"What do you know about our authentication?"_ → recalls from memory and knowledge graph +- _"Store in the graph: project:api depends_on express v5"_ → creates an RDF triple +- _"What's the memory status?"_ → shows STM/LTM/graph statistics + +### Connect with Claude Code + +```bash +# From your terminal (not inside a Claude Code session) +mayros mcp-setup +# or manually: +claude mcp add mayros -- mayros serve --stdio +``` + +### Connect with other MCP clients + +```bash +# Start the HTTP server +mayros serve --http +# → MCP endpoint: http://127.0.0.1:19100/mcp +# → Legacy SSE: http://127.0.0.1:19100/sse +# → Health check: http://127.0.0.1:19100/health +``` + +Point any MCP client to `http://127.0.0.1:19100/mcp` (Streamable HTTP) or `http://127.0.0.1:19100/sse` (legacy SSE for older clients). + +### Tools + +| Tool | Description | +| --------------------- | ----------------------------------------------------- | +| `mayros_remember` | Store information in persistent semantic memory | +| `mayros_recall` | Search memory by text, tags, or type | +| `mayros_search` | Vector similarity search over memory (HNSW) | +| `mayros_forget` | Delete a memory entry | +| `mayros_budget` | Check token usage and budget status | +| `mayros_policy_check` | Evaluate actions against governance policies | +| `mayros_cortex_query` | Query the knowledge graph by subject/predicate/object | +| `mayros_cortex_store` | Store RDF triples in the knowledge graph | +| `mayros_memory_stats` | STM/LTM/HNSW/graph statistics | + +--- + +## Intelligent Routing + +Adaptive routing that learns and improves over time. + +- **Eruberu** (Q-Learning model routing) — learns optimal provider/model per task type, budget level, and time slot +- **Miteru** (task-to-agent routing) — learns which agent handles each task type best via EMA scoring +- **Hayameru** (code transforms) — deterministic WASM transforms that bypass the LLM for simple edits (var→const, remove console, sort imports). 0 tokens, sub-millisecond + +CLI: `mayros routing status|strategy|reset` + +--- + ## Multi-Agent Mesh Agents that work together. Mayros supports coordinated multi-agent workflows with shared knowledge. - **Team manager** — Cortex-backed lifecycle: create, assign roles, merge results, disband - **Workflow orchestrator** — built-in workflows (code-review, research, refactor) + custom definitions +- **Kimeru consensus** — majority vote, weighted (EMA), LLM-arbitrated, Byzantine (PBFT with HMAC), Raft leader election - **Agent mailbox** — persistent inter-agent messaging (send/inbox/outbox/archive) - **Background task tracker** — long-running tasks with status and cancellation - **Git worktree isolation** — each agent works in its own worktree to avoid conflicts - **Session fork/rewind** — checkpoint-based exploration with rewind capability +- **Kakeru bridge** — dual-platform coordination (Claude + Codex CLI) with file lock coordination CLI: `mayros workflow run|list` · `mayros dashboard team|summary|agent` · `mayros tasks list|status|cancel|summary` · `mayros mailbox list|read|send|archive|stats` @@ -256,15 +335,21 @@ Both connect to `ws://127.0.0.1:18789`. | Category | Extension | Purpose | | ------------- | ------------------------- | ------------------------------------------------------------------------- | | Skills | `semantic-skills` | QuickJS WASM sandbox, 6 semantic tools, skill marketplace | -| Agents | `agent-mesh` | Teams, workflows, delegation, mailbox, background tasks | +| Agents | `agent-mesh` | Teams, workflows, consensus (majority/weighted/Byzantine/Raft), mailbox | | Memory | `memory-semantic` | Cortex integration, rules engine, agent memory, contextual awareness | | Observability | `semantic-observability` | Traces, decision graph, session fork/rewind | | Indexer | `code-indexer` | Codebase scanning + RDF mapping (incremental) | | Security | `bash-sandbox` | Command parsing, domain checker, blocklist, audit log | +| Governance | `osameru-governance` | Policy enforcement, HMAC audit trail, trust tiers | | Permissions | `interactive-permissions` | Runtime permission dialogs, intent classification, policy store | +| Routing | `eruberu` | Q-Learning model routing, budget-driven fallback, task classification | +| Transforms | `hayameru` | Deterministic code transforms that bypass LLM (0 tokens, sub-ms) | +| Rate Limit | `tomeru-guard` | Sliding window rate limiter, loop breaker, velocity circuit breaker | | Hooks | `llm-hooks` | Markdown-defined hook evaluation with safe condition parser | -| MCP | `mcp-client` | Model Context Protocol client (stdio, SSE, WebSocket, HTTP) | -| Economy | `token-economy` | Budget tracking, prompt cache optimization | +| MCP Server | `mcp-server` | 9 tools exposed via MCP (memory, budget, governance, graph) | +| MCP Client | `mcp-client` | Model Context Protocol client (stdio, SSE, WebSocket, HTTP) | +| Economy | `token-economy` | Budget tracking, response cache, prompt cache optimization | +| Bridge | `kakeru-bridge` | Dual-platform coordination (Claude + Codex CLI) | | Hub | `skill-hub` | Apilium Hub marketplace, Ed25519 signing, dependency audit | | IoT | `iot-bridge` | IoT node fleet management | | Channels | 17 plugins | Discord, Telegram, WhatsApp, Slack, Signal, iMessage, Teams, Matrix, etc. | @@ -284,9 +369,9 @@ Both connect to `ws://127.0.0.1:18789`. --- -## Security (18 layers) +## Security (20 layers) -Mayros takes security seriously. 18 layers of defense: +Mayros takes security seriously. 20 layers of defense: | Layer | Description | | --------------------------- | --------------------------------------------------------------- | @@ -307,6 +392,8 @@ Mayros takes security seriously. 18 layers of defense: | DM Pairing | Unknown senders get pairing code, not access | | Audit Logging | Skill name + operation tagged on all sandbox writes | | Docker Sandboxing | Per-session Docker containers for non-main sessions | +| Governance (Osameru) | Policy compilation, enforcement gates, HMAC audit trail | +| Rate Limit (Tomeru) | Sliding window, token bucket, loop breaking, velocity breaker | | Enterprise Managed Settings | Enforced config overrides with locked keys | --- diff --git a/docs/MCP-SERVE-PLAN.md b/docs/MCP-SERVE-PLAN.md new file mode 100644 index 00000000..2c7775b7 --- /dev/null +++ b/docs/MCP-SERVE-PLAN.md @@ -0,0 +1,1085 @@ +# Mayros MCP Serve — Implementation Plan + +Target: **v0.1.15** +Goal: `mayros serve` becomes the primary way Claude Code (and any MCP client) gets persistent memory, governance, and budget tracking. + +## Current State + +`mayros serve` **already exists** in `extensions/mcp-server/`. It: + +- Starts an MCP server (stdio or HTTP transport on port 3100) +- Auto-discovers tools from the plugin registry via `resolvePluginTools()` +- Exposes resources (agents, conventions, rules, graph stats) via Cortex +- Has protocol dispatcher, tool adapter (TypeBox → JSON Schema), CORS, auth +- MCP protocol version: 2025-03-26 + +### What's Missing + +| Gap | Impact | Status | +| ----------------------------------------------------------------- | ---------------------------------- | ------- | +| Cortex sidecar doesn't auto-start with `mayros serve` | Memory tools fail without Cortex | Missing | +| SSE transport for Claude Desktop `/sse` endpoint | Claude Desktop can't connect | Missing | +| Legacy HTTP+SSE transport (`/sse` endpoint) | Obsidian MCP pattern compatibility | Missing | +| Dedicated memory tools (simpler API than `semantic_memory_store`) | UX friction | Missing | +| Vector search tool | Core differentiator not exposed | Missing | +| `claude mcp add` auto-config | User must manually configure | Missing | +| Health check includes Cortex status | Can't diagnose issues | Partial | +| Tool naming convention (`mayros_*` prefix) | Tools mixed with internal names | Missing | +| Documentation for Claude Code users | No onboarding guide | Missing | + +--- + +## Architecture + +``` + ┌───────────────────────────┐ + │ Claude Code / IDE │ + │ (MCP Client) │ + └─────────┬─────────────────┘ + │ + ┌─────────────┼──────────────┐ + │ stdio │ HTTP POST │ SSE + │ │ /mcp │ /sse + ▼ ▼ ▼ + ┌──────────────────────────────────────┐ + │ McpServer │ + │ ┌──────────────────────────────┐ │ + │ │ McpProtocolDispatcher │ │ + │ │ (JSON-RPC 2.0) │ │ + │ └──────────┬───────────────────┘ │ + │ │ │ + │ ┌──────────▼───────────────────┐ │ + │ │ Tool Registry │ │ + │ │ │ │ + │ │ mayros_remember │ │ + │ │ mayros_recall │ │ + │ │ mayros_search │ │ + │ │ mayros_forget │ │ + │ │ mayros_budget │ │ + │ │ mayros_policy_check │ │ + │ │ mayros_cortex_query │ │ + │ │ mayros_cortex_store │ │ + │ │ + all existing plugin tools │ │ + │ └──────────┬───────────────────┘ │ + └─────────────┼────────────────────────┘ + │ + ┌────────▼────────┐ + │ Cortex Sidecar │ (auto-started) + │ :19090 │ + │ ┌────────────┐ │ + │ │ GraphDB │ │ + │ │ (Sled) │ │ + │ ├────────────┤ │ + │ │ Ineru │ │ + │ │ STM/LTM │ │ + │ │ HNSW │ │ + │ └────────────┘ │ + └─────────────────┘ +``` + +--- + +## Task 1: Auto-start Cortex with `mayros serve` + +### Problem + +Currently `mayros serve` collects tools and starts the MCP server, but does NOT start the Cortex sidecar. Memory tools that depend on Cortex will fail silently. + +### Solution + +In `extensions/mcp-server/index.ts`, the `serve` CLI action must start Cortex before collecting tools. + +### Files to modify + +#### `extensions/mcp-server/index.ts` — serve action (line 159) + +Before `const tools = await collectTools({})`, add: + +```typescript +// Auto-start Cortex sidecar for memory and graph tools +let sidecar: CortexSidecar | null = null; +try { + const { CortexSidecar } = await import("../memory-semantic/cortex-sidecar.js"); + const { resolveCortexConfig } = await import("../memory-semantic/cortex-config.js"); + const cortexCfg = resolveCortexConfig(api.config); + sidecar = new CortexSidecar(cortexCfg); + const started = await sidecar.start(); + if (started) { + api.logger.info("Cortex sidecar started for MCP server"); + } else { + api.logger.warn("Cortex sidecar failed to start — memory tools will be unavailable"); + } +} catch (err) { + api.logger.warn(`Cortex sidecar not available: ${String(err)}`); +} +``` + +And on shutdown (inside the SIGINT/SIGTERM handler): + +```typescript +process.on("SIGINT", () => { + void (async () => { + if (sidecar) await sidecar.stop(); + await server?.stop(); + resolve(); + })(); +}); +``` + +### Dependencies + +- `extensions/memory-semantic/cortex-sidecar.ts` — `CortexSidecar` class (already exists) +- `extensions/memory-semantic/cortex-config.ts` — `resolveCortexConfig()` (already exists) + +--- + +## Task 2: Dedicated MCP Memory Tools + +### Problem + +The existing `semantic_memory_store` tool is designed for internal agent use. Its API is complex (RDF triples, subjects, predicates). MCP clients need a simpler, more intuitive API. + +### Solution + +Register dedicated `mayros_*` prefixed tools in the MCP server plugin that wrap the existing Cortex client with a user-friendly API. + +### New file: `extensions/mcp-server/memory-tools.ts` + +```typescript +/** + * MCP-friendly memory tools. + * + * Wraps Cortex/Ineru APIs with a simple remember/recall/search interface + * designed for external MCP clients (Claude Code, Cursor, etc.). + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export type MemoryToolDeps = { + cortexBaseUrl: string; + namespace: string; +}; + +export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { + const { cortexBaseUrl, namespace } = deps; + + return [ + // ── mayros_remember ────────────────────────────────────────────── + { + name: "mayros_remember", + description: + "Store information in persistent semantic memory. " + + "Use this to remember facts, decisions, preferences, patterns, " + + "or any context that should persist across sessions.", + parameters: Type.Object({ + content: Type.String({ + description: "The information to remember (natural language)", + }), + category: Type.Optional( + Type.String({ + description: + 'Category: "fact", "decision", "preference", "pattern", "code", "architecture"', + }), + ), + tags: Type.Optional( + Type.Array(Type.String(), { + description: "Tags for easier recall (e.g., ['payments', 'api'])", + }), + ), + importance: Type.Optional( + Type.Number({ + description: "Importance 0.0-1.0 (default 0.7). Higher = kept longer in memory", + }), + ), + }), + execute: async (_id: string, params: Record) => { + const content = params.content as string; + const category = (params.category as string) ?? "general"; + const tags = (params.tags as string[]) ?? []; + const importance = (params.importance as number) ?? 0.7; + + // Store as RDF triple in Cortex + const subject = `${namespace}:memory:${Date.now()}`; + const triples = [ + { subject, predicate: `${namespace}:memory:content`, object: content }, + { subject, predicate: `${namespace}:memory:category`, object: category }, + { subject, predicate: `${namespace}:memory:importance`, object: String(importance) }, + ...tags.map((tag) => ({ + subject, + predicate: `${namespace}:memory:tag`, + object: tag, + })), + ]; + + // Store in Cortex graph + for (const triple of triples) { + await fetch(`${cortexBaseUrl}/api/v1/triples`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(triple), + }); + } + + // Also store in Ineru STM for vector search + await fetch(`${cortexBaseUrl}/api/v1/memory/remember`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + entry_type: category, + data: { content, tags }, + tags, + importance, + }), + }); + + return { + content: [ + { + type: "text" as const, + text: `Remembered: "${content.slice(0, 80)}${content.length > 80 ? "..." : ""}" [${category}]${tags.length > 0 ? ` #${tags.join(" #")}` : ""}`, + }, + ], + }; + }, + }, + + // ── mayros_recall ──────────────────────────────────────────────── + { + name: "mayros_recall", + description: + "Search persistent memory for previously stored information. " + + "Query by text (semantic match), tags, or category. " + + "Returns relevant memories from past sessions.", + parameters: Type.Object({ + query: Type.Optional(Type.String({ description: "Text to search for (semantic match)" })), + tags: Type.Optional(Type.Array(Type.String(), { description: "Filter by tags" })), + category: Type.Optional(Type.String({ description: "Filter by category" })), + limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })), + }), + execute: async (_id: string, params: Record) => { + const query = params.query as string | undefined; + const tags = params.tags as string[] | undefined; + const category = params.category as string | undefined; + const limit = (params.limit as number) ?? 10; + + // Query Ineru recall endpoint + const recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: query, + tags: tags ?? [], + entry_type: category, + limit, + }), + }); + + if (!recallRes.ok) { + // Fallback: query Cortex graph directly + const pattern: Record = {}; + if (query) pattern.object = query; + const graphRes = await fetch( + `${cortexBaseUrl}/api/v1/triples?predicate=${namespace}:memory:content&limit=${limit}`, + ); + const graphData = (await graphRes.json()) as { triples?: Array<{ object: string }> }; + const triples = graphData.triples ?? []; + + return { + content: [ + { + type: "text" as const, + text: + triples.length > 0 + ? triples.map((t, i) => `${i + 1}. ${t.object}`).join("\n") + : "No memories found.", + }, + ], + }; + } + + const memories = (await recallRes.json()) as Array<{ + id: string; + entry_type: string; + data: { content?: string }; + tags: string[]; + importance: number; + relevance: number; + source: string; + }>; + + if (memories.length === 0) { + return { + content: [{ type: "text" as const, text: "No memories found." }], + }; + } + + const formatted = memories + .map( + (m, i) => + `${i + 1}. [${m.entry_type}] ${m.data.content ?? JSON.stringify(m.data)}` + + (m.tags.length > 0 ? ` #${m.tags.join(" #")}` : "") + + ` (relevance: ${(m.relevance * 100).toFixed(0)}%, source: ${m.source})`, + ) + .join("\n"); + + return { + content: [{ type: "text" as const, text: formatted }], + }; + }, + }, + + // ── mayros_search ──────────────────────────────────────────────── + { + name: "mayros_search", + description: + "Vector similarity search over memory using HNSW index. " + + "Finds semantically similar memories even with different wording. " + + "Requires an embedding vector (or text for future auto-embedding).", + parameters: Type.Object({ + text: Type.String({ + description: "Text to search for. Will be matched against stored memories.", + }), + k: Type.Optional(Type.Number({ description: "Number of results (default 5)" })), + min_similarity: Type.Optional( + Type.Number({ description: "Minimum similarity 0.0-1.0 (default 0.3)" }), + ), + }), + execute: async (_id: string, params: Record) => { + const text = params.text as string; + const k = (params.k as number) ?? 5; + + // For now, fall back to Ineru recall with text matching + // TODO: Auto-embed text via LLM and call /api/v1/memory/search + const recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text, limit: k }), + }); + + if (!recallRes.ok) { + return { + content: [ + { + type: "text" as const, + text: "Vector search unavailable. Cortex may not be running.", + }, + ], + }; + } + + const results = (await recallRes.json()) as Array<{ + data: { content?: string }; + relevance: number; + entry_type: string; + tags: string[]; + }>; + + if (results.length === 0) { + return { + content: [{ type: "text" as const, text: "No similar memories found." }], + }; + } + + const formatted = results + .map( + (r, i) => + `${i + 1}. [${(r.relevance * 100).toFixed(0)}%] ${r.data.content ?? JSON.stringify(r.data)}` + + (r.tags.length > 0 ? ` #${r.tags.join(" #")}` : ""), + ) + .join("\n"); + + return { + content: [{ type: "text" as const, text: formatted }], + }; + }, + }, + + // ── mayros_forget ──────────────────────────────────────────────── + { + name: "mayros_forget", + description: "Delete a specific memory entry by ID.", + parameters: Type.Object({ + id: Type.String({ description: "Memory ID to delete" }), + }), + execute: async (_id: string, params: Record) => { + const memoryId = params.id as string; + const res = await fetch(`${cortexBaseUrl}/api/v1/memory/${memoryId}`, { + method: "DELETE", + }); + return { + content: [ + { + type: "text" as const, + text: res.ok + ? `Memory ${memoryId} forgotten.` + : `Failed to forget: ${res.statusText}`, + }, + ], + }; + }, + }, + ]; +} +``` + +### New file: `extensions/mcp-server/budget-tools.ts` + +```typescript +/** + * MCP-friendly budget/token economy tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export function createBudgetTools(): AdaptableTool[] { + return [ + { + name: "mayros_budget", + description: + "Check token usage and budget status. " + + "Shows session spend, daily spend, and remaining budget.", + parameters: Type.Object({}), + execute: async () => { + // Read budget state from disk + const budgetPath = `${process.env.HOME ?? "."}/.mayros/budget-state.json`; + try { + const { readFile } = await import("node:fs/promises"); + const data = JSON.parse(await readFile(budgetPath, "utf-8")) as { + sessionTokens?: number; + dailyTokens?: number; + monthlyTokens?: number; + sessionCostUsd?: number; + dailyCostUsd?: number; + monthlyCostUsd?: number; + sessionLimit?: number; + dailyLimit?: number; + }; + + const lines = [ + "Token Budget Status:", + ` Session: ${data.sessionTokens?.toLocaleString() ?? 0} tokens ($${(data.sessionCostUsd ?? 0).toFixed(4)})`, + ` Daily: ${data.dailyTokens?.toLocaleString() ?? 0} tokens ($${(data.dailyCostUsd ?? 0).toFixed(4)})`, + ` Monthly: ${data.monthlyTokens?.toLocaleString() ?? 0} tokens ($${(data.monthlyCostUsd ?? 0).toFixed(4)})`, + ]; + if (data.sessionLimit) { + lines.push(` Session limit: ${data.sessionLimit.toLocaleString()} tokens`); + } + if (data.dailyLimit) { + lines.push(` Daily limit: ${data.dailyLimit.toLocaleString()} tokens`); + } + + return { content: [{ type: "text" as const, text: lines.join("\n") }] }; + } catch { + return { + content: [{ type: "text" as const, text: "No budget data available yet." }], + }; + } + }, + }, + ]; +} +``` + +### New file: `extensions/mcp-server/governance-tools.ts` + +```typescript +/** + * MCP-friendly governance tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export function createGovernanceTools(): AdaptableTool[] { + return [ + { + name: "mayros_policy_check", + description: + "Check if an action is allowed by the project governance policies. " + + "Evaluates tool calls, file operations, and commands against MAYROS.md rules.", + parameters: Type.Object({ + action: Type.String({ + description: 'Action type: "tool_call", "file_write", "file_delete", "shell_command"', + }), + target: Type.String({ + description: "Target of the action (tool name, file path, or command)", + }), + details: Type.Optional(Type.String({ description: "Additional context about the action" })), + }), + execute: async (_id: string, params: Record) => { + const action = params.action as string; + const target = params.target as string; + + // Load policy rules from MAYROS.md if exists + const { readFile, access } = await import("node:fs/promises"); + const policyPath = `${process.cwd()}/MAYROS.md`; + + try { + await access(policyPath); + const content = await readFile(policyPath, "utf-8"); + + // Simple pattern matching against DENY/ALLOW rules + const denyPatterns: string[] = []; + const allowPatterns: string[] = []; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (trimmed.startsWith("- DENY:")) { + denyPatterns.push(trimmed.slice(7).trim()); + } else if (trimmed.startsWith("- ALLOW:")) { + allowPatterns.push(trimmed.slice(8).trim()); + } + } + + // Check deny rules + for (const pattern of denyPatterns) { + if (target.includes(pattern) || action.includes(pattern)) { + return { + content: [ + { + type: "text" as const, + text: `DENIED: "${target}" matches deny rule "${pattern}"`, + }, + ], + }; + } + } + + return { + content: [ + { + type: "text" as const, + text: `ALLOWED: "${action}" on "${target}" — no deny rules matched (${denyPatterns.length} rules checked)`, + }, + ], + }; + } catch { + return { + content: [ + { + type: "text" as const, + text: `ALLOWED (no policy): No MAYROS.md found at ${policyPath}. All actions permitted.`, + }, + ], + }; + } + }, + }, + ]; +} +``` + +### New file: `extensions/mcp-server/cortex-tools.ts` + +```typescript +/** + * MCP-friendly Cortex graph query tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export type CortexToolDeps = { + cortexBaseUrl: string; + namespace: string; +}; + +export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { + const { cortexBaseUrl, namespace } = deps; + + return [ + { + name: "mayros_cortex_query", + description: + "Query the semantic knowledge graph. " + + "Find triples by subject, predicate, or object pattern. " + + "Use this for structured knowledge retrieval.", + parameters: Type.Object({ + subject: Type.Optional( + Type.String({ description: "Subject pattern (e.g., 'project:api')" }), + ), + predicate: Type.Optional( + Type.String({ description: "Predicate pattern (e.g., 'uses_framework')" }), + ), + object: Type.Optional(Type.String({ description: "Object value to match" })), + limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })), + }), + execute: async (_id: string, params: Record) => { + const limit = (params.limit as number) ?? 20; + const queryParams = new URLSearchParams(); + if (params.subject) queryParams.set("subject", params.subject as string); + if (params.predicate) queryParams.set("predicate", params.predicate as string); + if (params.object) queryParams.set("object", params.object as string); + queryParams.set("limit", String(limit)); + + const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `Query failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + triples: Array<{ subject: string; predicate: string; object: unknown }>; + }; + if (!data.triples || data.triples.length === 0) { + return { content: [{ type: "text" as const, text: "No triples found." }] }; + } + + const formatted = data.triples + .map((t) => ` ${t.subject} → ${t.predicate} → ${JSON.stringify(t.object)}`) + .join("\n"); + + return { + content: [ + { type: "text" as const, text: `Found ${data.triples.length} triples:\n${formatted}` }, + ], + }; + }, + }, + + { + name: "mayros_cortex_store", + description: + "Store a fact in the semantic knowledge graph as an RDF triple. " + + "Use subject-predicate-object structure for structured knowledge.", + parameters: Type.Object({ + subject: Type.String({ description: "Subject (e.g., 'project:payments-api')" }), + predicate: Type.String({ description: "Predicate/relation (e.g., 'uses_framework')" }), + object: Type.String({ description: "Object/value (e.g., 'Express.js')" }), + }), + execute: async (_id: string, params: Record) => { + const res = await fetch(`${cortexBaseUrl}/api/v1/triples`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + subject: params.subject, + predicate: params.predicate, + object: params.object, + }), + }); + + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `Store failed: ${res.statusText}` }], + }; + } + + return { + content: [ + { + type: "text" as const, + text: `Stored: ${params.subject as string} → ${params.predicate as string} → ${params.object as string}`, + }, + ], + }; + }, + }, + + { + name: "mayros_memory_stats", + description: + "Get memory system statistics: STM entries, LTM entities, HNSW index size, graph triple count.", + parameters: Type.Object({}), + execute: async () => { + const results: string[] = []; + + // Ineru stats + try { + const memRes = await fetch(`${cortexBaseUrl}/api/v1/memory/stats`); + if (memRes.ok) { + const stats = (await memRes.json()) as { + stm_count: number; + stm_capacity: number; + ltm_entity_count: number; + ltm_link_count: number; + total_memory_bytes: number; + }; + results.push( + "Ineru Memory:", + ` STM: ${stats.stm_count} / ${stats.stm_capacity} entries`, + ` LTM: ${stats.ltm_entity_count} entities, ${stats.ltm_link_count} links`, + ` Size: ${(stats.total_memory_bytes / 1024).toFixed(1)} KB`, + ); + } + } catch { + /* Cortex unavailable */ + } + + // HNSW stats + try { + const idxRes = await fetch(`${cortexBaseUrl}/api/v1/memory/index/stats`); + if (idxRes.ok) { + const idx = (await idxRes.json()) as { + point_count: number; + dimensions: number; + memory_bytes: number; + }; + results.push( + "HNSW Vector Index:", + ` Points: ${idx.point_count}`, + ` Dimensions: ${idx.dimensions}`, + ` Size: ${(idx.memory_bytes / 1024).toFixed(1)} KB`, + ); + } + } catch { + /* */ + } + + // Graph stats + try { + const graphRes = await fetch(`${cortexBaseUrl}/api/v1/stats`); + if (graphRes.ok) { + const g = (await graphRes.json()) as { + triple_count: number; + subject_count: number; + predicate_count: number; + }; + results.push( + "Knowledge Graph:", + ` Triples: ${g.triple_count}`, + ` Subjects: ${g.subject_count}`, + ` Predicates: ${g.predicate_count}`, + ); + } + } catch { + /* */ + } + + return { + content: [ + { + type: "text" as const, + text: results.length > 0 ? results.join("\n") : "Cortex sidecar not running.", + }, + ], + }; + }, + }, + ]; +} +``` + +--- + +## Task 3: Wire MCP Tools into Server + +### Modify `extensions/mcp-server/index.ts` + +In the `serve` CLI action, after Cortex starts and before `server.start()`: + +```typescript +// Register dedicated MCP tools +const cortexPort = cortexCfg?.port ?? 19090; +const cortexBase = `http://127.0.0.1:${cortexPort}`; +const ns = serverCfg.agentNamespace || "mayros"; + +const { createMemoryTools } = await import("./memory-tools.js"); +const { createBudgetTools } = await import("./budget-tools.js"); +const { createGovernanceTools } = await import("./governance-tools.js"); +const { createCortexTools } = await import("./cortex-tools.js"); + +const mcpTools: AdaptableTool[] = [ + ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createBudgetTools(), + ...createGovernanceTools(), + ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns }), +]; + +// Combine with auto-discovered plugin tools +const allTools = [...mcpTools, ...tools]; +``` + +Pass `allTools` to `McpServerOptions` instead of `tools`. + +--- + +## Task 4: Legacy SSE Transport (Claude Desktop compatibility) + +### Problem + +Claude Desktop uses the legacy MCP "HTTP with SSE" transport (`/sse` endpoint + POST to returned URL). The current HTTP transport only supports Streamable HTTP (`POST /mcp`). + +The obsidian-claude-code-mcp plugin serves on port 22360 with `/sse` for this reason. + +### Modify `extensions/mcp-server/transport-http.ts` + +Add `/sse` endpoint handling alongside existing `/mcp`: + +```typescript +// In handleRequest(), add before the "Not found" fallback: + +// Legacy SSE transport (Claude Desktop compatibility) +if (url === "/sse" && method === "GET") { + this.handleLegacySse(req, res); + return; +} +``` + +The legacy SSE transport: + +1. Client does `GET /sse` → server returns SSE stream with `endpoint` event +2. `endpoint` event contains URL for client to POST JSON-RPC requests to +3. Server responses come back through the SSE stream + +```typescript +private handleLegacySse(req: IncomingMessage, res: ServerResponse): void { + const sessionId = crypto.randomUUID(); + const postUrl = `/mcp/session/${sessionId}`; + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + + // Send endpoint URL + res.write(`event: endpoint\ndata: ${postUrl}\n\n`); + + // Store SSE connection for this session + this.sseSessions.set(sessionId, res); + + // Keep alive + const keepAlive = setInterval(() => { + if (res.destroyed) { clearInterval(keepAlive); return; } + res.write(": ping\n\n"); + }, 15_000); + + res.on("close", () => { + clearInterval(keepAlive); + this.sseSessions.delete(sessionId); + }); +} +``` + +And handle `POST /mcp/session/:id`: + +```typescript +if (url.startsWith("/mcp/session/") && method === "POST") { + const sessionId = url.split("/mcp/session/")[1]; + const sseRes = this.sseSessions.get(sessionId); + if (!sseRes) { + res.writeHead(404); + res.end(); + return; + } + + const body = await readBody(req); + const response = await this.dispatcher.handleMessage(body); + + // Send response through SSE stream + if (response) { + sseRes.write(`event: message\ndata: ${response}\n\n`); + } + + // Ack the POST + res.writeHead(202); + res.end(); + return; +} +``` + +Add field to class: + +```typescript +private sseSessions = new Map(); +``` + +--- + +## Task 5: `claude mcp add` Auto-Configuration + +### New file: `extensions/mcp-server/setup-claude.ts` + +```typescript +/** + * Auto-configure Claude Code to use Mayros MCP server. + * + * Writes the MCP server config to Claude Code's settings. + */ + +export async function setupClaudeCodeMcp(opts: { port: number; host: string }): Promise { + const { execSync } = await import("node:child_process"); + + try { + // Use claude CLI to add MCP server + execSync( + `claude mcp add mayros -- mayros serve --http --port ${opts.port} --host ${opts.host}`, + { stdio: "inherit" }, + ); + console.log("Mayros MCP server registered with Claude Code."); + } catch { + // Fallback: show manual instructions + console.log("\nTo connect Mayros to Claude Code, run:\n"); + console.log( + ` claude mcp add mayros -- mayros serve --http --port ${opts.port} --host ${opts.host}\n`, + ); + } +} +``` + +### Add CLI subcommand in `extensions/mcp-server/index.ts` + +```typescript +const setup = program + .command("mcp-setup") + .description("Register Mayros as an MCP server in Claude Code") + .option("--port ", "HTTP port (default: 3100)", parseInt) + .action(async (opts: { port?: number }) => { + const { setupClaudeCodeMcp } = await import("./setup-claude.js"); + await setupClaudeCodeMcp({ port: opts.port ?? 3100, host: "127.0.0.1" }); + }); +``` + +--- + +## Task 6: Enhanced Health Check + +### Modify `extensions/mcp-server/transport-http.ts` + +Change health endpoint to include Cortex status: + +```typescript +if (url === "/health" && method === "GET") { + // Check Cortex health + let cortexHealthy = false; + try { + const cortexRes = await fetch("http://127.0.0.1:19090/health"); + cortexHealthy = cortexRes.ok; + } catch { + /* */ + } + + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + status: "ok", + transport: "streamable-http", + cortex: cortexHealthy ? "healthy" : "unavailable", + tools: this.dispatcher.toolCount(), + }), + ); + return; +} +``` + +--- + +## Files Summary + +### New Files + +| File | Purpose | LOC est. | +| ------------------------------------------- | ------------------------------------------------------------- | -------- | +| `extensions/mcp-server/memory-tools.ts` | mayros_remember, mayros_recall, mayros_search, mayros_forget | ~220 | +| `extensions/mcp-server/budget-tools.ts` | mayros_budget | ~60 | +| `extensions/mcp-server/governance-tools.ts` | mayros_policy_check | ~80 | +| `extensions/mcp-server/cortex-tools.ts` | mayros_cortex_query, mayros_cortex_store, mayros_memory_stats | ~150 | +| `extensions/mcp-server/setup-claude.ts` | `mayros mcp-setup` auto-config | ~30 | + +### Modified Files + +| File | Change | +| ----------------------------------------- | ------------------------------------------------------ | +| `extensions/mcp-server/index.ts` | Auto-start Cortex, wire MCP tools, add `mcp-setup` CLI | +| `extensions/mcp-server/transport-http.ts` | Add `/sse` legacy transport, enhanced `/health` | + +### Tools Exposed via MCP + +| Tool | Category | Description | +| --------------------------- | ----------- | ------------------------------------ | +| `mayros_remember` | Memory | Store information persistently | +| `mayros_recall` | Memory | Search memory by text/tags/category | +| `mayros_search` | Memory | Vector similarity search | +| `mayros_forget` | Memory | Delete a memory entry | +| `mayros_budget` | Economy | Token usage and budget status | +| `mayros_policy_check` | Governance | Check action against MAYROS.md rules | +| `mayros_cortex_query` | Knowledge | Query semantic graph | +| `mayros_cortex_store` | Knowledge | Store fact in semantic graph | +| `mayros_memory_stats` | Diagnostics | Memory and index statistics | +| + all existing plugin tools | Various | Auto-discovered from plugin registry | + +--- + +## User Experience + +### Installation (2 commands) + +```bash +npm install -g mayros +claude mcp add mayros -- mayros serve --http +``` + +### Or with auto-setup + +```bash +npm install -g mayros +mayros mcp-setup +``` + +### What Claude Code sees + +After connection, Claude Code has 9+ new tools available: + +``` +Connected to MCP server: Mayros (9 tools) + mayros_remember — Store information in persistent semantic memory + mayros_recall — Search persistent memory + mayros_search — Vector similarity search over memory + mayros_forget — Delete a memory entry + mayros_budget — Check token usage and budget + mayros_policy_check — Check governance policies + mayros_cortex_query — Query semantic knowledge graph + mayros_cortex_store — Store fact in knowledge graph + mayros_memory_stats — Memory system statistics +``` + +### Example session + +``` +User: "Remember that the payments API uses Stripe with rate limit 100/min" + +Claude Code → mayros_remember( + content: "The payments API uses Stripe with rate limit of 100 requests per minute", + category: "architecture", + tags: ["payments", "stripe", "api", "rate-limit"] +) + +[3 days later, new session] + +User: "What do we know about the payments module?" + +Claude Code → mayros_recall( + query: "payments module", + limit: 5 +) + +→ 1. [architecture] The payments API uses Stripe with rate limit of 100/min #payments #stripe (relevance: 95%, source: LongTerm) +``` + +--- + +## Implementation Sequence + +``` +Task 1 — Auto-start Cortex (~30 lines in index.ts) +Task 2 — Memory tools (memory-tools.ts, ~220 LOC) +Task 3 — Wire tools + budget + governance + cortex tools (~100 LOC new files + ~20 lines in index.ts) +Task 4 — Legacy SSE transport (~80 lines in transport-http.ts) +Task 5 — claude mcp add setup (~30 LOC + CLI command) +Task 6 — Enhanced health check (~15 lines) + +Total: ~6 files new/modified, ~550 LOC net +``` + +--- + +## Constraints + +- No new npm dependencies (uses Node built-in `http`, `fs`, `crypto`) +- Backward compatible: `mayros serve --stdio` unchanged +- Cortex failure is non-fatal: server starts, memory tools return "unavailable" +- Tool names prefixed `mayros_*` to avoid collision with user's MCP tools +- MCP protocol: 2025-03-26 (Streamable HTTP) + 2024-11-05 (legacy SSE) dual support +- Auth token optional (disabled by default for local use) diff --git a/extensions/agent-mesh/package.json b/extensions/agent-mesh/package.json index edf16b45..0b47db85 100644 --- a/extensions/agent-mesh/package.json +++ b/extensions/agent-mesh/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-agent-mesh", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros multi-agent coordination mesh with shared namespaces, delegation, and knowledge fusion", "type": "module", diff --git a/extensions/analytics/package.json b/extensions/analytics/package.json index 9da66ca7..38eb4556 100644 --- a/extensions/analytics/package.json +++ b/extensions/analytics/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-analytics", - "version": "0.1.14", + "version": "0.1.15", "private": true, "type": "module", "main": "index.ts", diff --git a/extensions/bash-sandbox/package.json b/extensions/bash-sandbox/package.json index 00b90b67..46984981 100644 --- a/extensions/bash-sandbox/package.json +++ b/extensions/bash-sandbox/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-bash-sandbox", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Bash command sandbox with domain allowlist, command blocklist, and dangerous pattern detection", "type": "module", diff --git a/extensions/bluebubbles/package.json b/extensions/bluebubbles/package.json index f27ab618..4522f219 100644 --- a/extensions/bluebubbles/package.json +++ b/extensions/bluebubbles/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-bluebubbles", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros BlueBubbles channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/ci-plugin/package.json b/extensions/ci-plugin/package.json index f6904fd4..6fbed50d 100644 --- a/extensions/ci-plugin/package.json +++ b/extensions/ci-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-ci-plugin", - "version": "0.1.14", + "version": "0.1.15", "description": "CI/CD pipeline integration for Mayros — GitHub Actions and GitLab CI providers", "type": "module", "dependencies": { diff --git a/extensions/code-indexer/package.json b/extensions/code-indexer/package.json index b42e83b6..634b3de3 100644 --- a/extensions/code-indexer/package.json +++ b/extensions/code-indexer/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-code-indexer", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros code indexer plugin — regex-based codebase scanning with RDF triple storage in Cortex", "type": "module", diff --git a/extensions/code-tools/package.json b/extensions/code-tools/package.json index e19725aa..0ed9e9e7 100644 --- a/extensions/code-tools/package.json +++ b/extensions/code-tools/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-code-tools", - "version": "0.1.14", + "version": "0.1.15", "private": true, "type": "module", "dependencies": { diff --git a/extensions/copilot-proxy/package.json b/extensions/copilot-proxy/package.json index f47e4acd..5f6a49aa 100644 --- a/extensions/copilot-proxy/package.json +++ b/extensions/copilot-proxy/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-copilot-proxy", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Copilot Proxy provider plugin", "type": "module", diff --git a/extensions/cortex-sync/package.json b/extensions/cortex-sync/package.json index ba871593..7c121084 100644 --- a/extensions/cortex-sync/package.json +++ b/extensions/cortex-sync/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-cortex-sync", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Cortex DAG synchronization — peer discovery, delta sync, and cross-device knowledge replication", "type": "module", diff --git a/extensions/diagnostics-otel/package.json b/extensions/diagnostics-otel/package.json index 324907c5..18c8bfaf 100644 --- a/extensions/diagnostics-otel/package.json +++ b/extensions/diagnostics-otel/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-diagnostics-otel", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros diagnostics OpenTelemetry exporter", "license": "MIT", "type": "module", diff --git a/extensions/discord/package.json b/extensions/discord/package.json index 97203a0a..e305becc 100644 --- a/extensions/discord/package.json +++ b/extensions/discord/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-discord", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Discord channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/eruberu/package.json b/extensions/eruberu/package.json index 84e67662..5bb5948c 100644 --- a/extensions/eruberu/package.json +++ b/extensions/eruberu/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-eruberu", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros intelligent model routing plugin — Q-Learning adaptive provider/model selection", "type": "module", diff --git a/extensions/feishu/package.json b/extensions/feishu/package.json index c476e984..a33d1380 100644 --- a/extensions/feishu/package.json +++ b/extensions/feishu/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-feishu", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Feishu/Lark channel plugin (community maintained by @m1heng)", "license": "MIT", "type": "module", diff --git a/extensions/google-antigravity-auth/package.json b/extensions/google-antigravity-auth/package.json index 8dd32134..7b6434c6 100644 --- a/extensions/google-antigravity-auth/package.json +++ b/extensions/google-antigravity-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-google-antigravity-auth", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Google Antigravity OAuth provider plugin", "type": "module", diff --git a/extensions/google-gemini-cli-auth/package.json b/extensions/google-gemini-cli-auth/package.json index 5e157c37..05b5fab1 100644 --- a/extensions/google-gemini-cli-auth/package.json +++ b/extensions/google-gemini-cli-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-google-gemini-cli-auth", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Gemini CLI OAuth provider plugin", "type": "module", diff --git a/extensions/googlechat/package.json b/extensions/googlechat/package.json index 256b637e..a47839b0 100644 --- a/extensions/googlechat/package.json +++ b/extensions/googlechat/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-googlechat", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Google Chat channel plugin", "type": "module", diff --git a/extensions/hayameru/package.json b/extensions/hayameru/package.json index 0f4c661e..685188a5 100644 --- a/extensions/hayameru/package.json +++ b/extensions/hayameru/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-hayameru", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros deterministic code transforms — bypass LLM for simple edits", "type": "module", diff --git a/extensions/imessage/package.json b/extensions/imessage/package.json index f0fc04b5..d12acea7 100644 --- a/extensions/imessage/package.json +++ b/extensions/imessage/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-imessage", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros iMessage channel plugin", "type": "module", diff --git a/extensions/interactive-permissions/package.json b/extensions/interactive-permissions/package.json index b4cd56ef..44b4c62a 100644 --- a/extensions/interactive-permissions/package.json +++ b/extensions/interactive-permissions/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-interactive-permissions", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Runtime permission dialogs, bash intent classification, policy persistence, and audit trail", "type": "module", diff --git a/extensions/iot-bridge/package.json b/extensions/iot-bridge/package.json index 6952230f..65e3dd1a 100644 --- a/extensions/iot-bridge/package.json +++ b/extensions/iot-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-iot-bridge", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "IoT Bridge — connect MAYROS agents to aingle_minimal IoT nodes via REST", "type": "module", diff --git a/extensions/irc/package.json b/extensions/irc/package.json index f0b023c1..9bcea023 100644 --- a/extensions/irc/package.json +++ b/extensions/irc/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-irc", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros IRC channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/kakeru-bridge/package.json b/extensions/kakeru-bridge/package.json index dc398a29..776dba9c 100644 --- a/extensions/kakeru-bridge/package.json +++ b/extensions/kakeru-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-kakeru-bridge", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros dual-platform coordination bridge — Claude Code + Codex CLI", "type": "module", diff --git a/extensions/line/package.json b/extensions/line/package.json index 798fa55e..26401ba4 100644 --- a/extensions/line/package.json +++ b/extensions/line/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-line", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros LINE channel plugin", "type": "module", diff --git a/extensions/llm-hooks/package.json b/extensions/llm-hooks/package.json index 361c0d9a..789a1efc 100644 --- a/extensions/llm-hooks/package.json +++ b/extensions/llm-hooks/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-llm-hooks", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Markdown-defined hooks evaluated by LLM for policy enforcement", "type": "module", diff --git a/extensions/llm-task/package.json b/extensions/llm-task/package.json index 5ea4aa52..63c55848 100644 --- a/extensions/llm-task/package.json +++ b/extensions/llm-task/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-llm-task", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros JSON-only LLM task plugin", "type": "module", diff --git a/extensions/lobster/package.json b/extensions/lobster/package.json index 514a78de..e234e31a 100644 --- a/extensions/lobster/package.json +++ b/extensions/lobster/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-lobster", - "version": "0.1.14", + "version": "0.1.15", "description": "Lobster workflow tool plugin (typed pipelines + resumable approvals)", "license": "MIT", "type": "module", diff --git a/extensions/lsp-bridge/package.json b/extensions/lsp-bridge/package.json index c1043317..50135860 100644 --- a/extensions/lsp-bridge/package.json +++ b/extensions/lsp-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-lsp-bridge", - "version": "0.1.14", + "version": "0.1.15", "description": "Cortex-backed language server bridge for Mayros — hover, diagnostics, go-to-definition", "type": "module", "dependencies": { diff --git a/extensions/matrix/package.json b/extensions/matrix/package.json index b9b802ab..57055e9b 100644 --- a/extensions/matrix/package.json +++ b/extensions/matrix/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-matrix", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Matrix channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/mattermost/package.json b/extensions/mattermost/package.json index 3a4b965e..76d8e266 100644 --- a/extensions/mattermost/package.json +++ b/extensions/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mattermost", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Mattermost channel plugin", "type": "module", diff --git a/extensions/mcp-client/package.json b/extensions/mcp-client/package.json index e3c55a5a..9c701dca 100644 --- a/extensions/mcp-client/package.json +++ b/extensions/mcp-client/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mcp-client", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "MCP server client with multi-transport support and Cortex tool registry", "type": "module", diff --git a/extensions/mcp-server/budget-tools.ts b/extensions/mcp-server/budget-tools.ts new file mode 100644 index 00000000..b248712c --- /dev/null +++ b/extensions/mcp-server/budget-tools.ts @@ -0,0 +1,53 @@ +/** + * MCP-friendly budget/token economy tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export function createBudgetTools(): AdaptableTool[] { + return [ + { + name: "mayros_budget", + description: + "Check token usage and budget status. " + + "Shows session spend, daily spend, and remaining budget.", + parameters: Type.Object({}), + execute: async () => { + const budgetPath = `${process.env.HOME ?? "."}/.mayros/budget-state.json`; + try { + const { readFile } = await import("node:fs/promises"); + const data = JSON.parse(await readFile(budgetPath, "utf-8")) as { + sessionTokens?: number; + dailyTokens?: number; + monthlyTokens?: number; + sessionCostUsd?: number; + dailyCostUsd?: number; + monthlyCostUsd?: number; + sessionLimit?: number; + dailyLimit?: number; + }; + + const lines = [ + "Token Budget Status:", + ` Session: ${data.sessionTokens?.toLocaleString() ?? 0} tokens ($${(data.sessionCostUsd ?? 0).toFixed(4)})`, + ` Daily: ${data.dailyTokens?.toLocaleString() ?? 0} tokens ($${(data.dailyCostUsd ?? 0).toFixed(4)})`, + ` Monthly: ${data.monthlyTokens?.toLocaleString() ?? 0} tokens ($${(data.monthlyCostUsd ?? 0).toFixed(4)})`, + ]; + if (data.sessionLimit) { + lines.push(` Session limit: ${data.sessionLimit.toLocaleString()} tokens`); + } + if (data.dailyLimit) { + lines.push(` Daily limit: ${data.dailyLimit.toLocaleString()} tokens`); + } + + return { content: [{ type: "text" as const, text: lines.join("\n") }] }; + } catch { + return { + content: [{ type: "text" as const, text: "No budget data available yet." }], + }; + } + }, + }, + ]; +} diff --git a/extensions/mcp-server/config.test.ts b/extensions/mcp-server/config.test.ts index 98831656..a56f328f 100644 --- a/extensions/mcp-server/config.test.ts +++ b/extensions/mcp-server/config.test.ts @@ -6,7 +6,7 @@ describe("mcpServerConfigSchema", () => { it("parses minimal config with defaults", () => { const cfg = mcpServerConfigSchema.parse({}); expect(cfg.transport).toBe("stdio"); - expect(cfg.port).toBe(3100); + expect(cfg.port).toBe(19100); expect(cfg.host).toBe("127.0.0.1"); expect(cfg.serverName).toBe("mayros"); expect(cfg.serverVersion).toBe("0.1.0"); @@ -62,7 +62,7 @@ describe("mcpServerConfigSchema", () => { it("parses null/undefined as defaults", () => { const cfg = mcpServerConfigSchema.parse(null); expect(cfg.transport).toBe("stdio"); - expect(cfg.port).toBe(3100); + expect(cfg.port).toBe(19100); }); // 7 diff --git a/extensions/mcp-server/config.ts b/extensions/mcp-server/config.ts index 9abfa952..d16e2819 100644 --- a/extensions/mcp-server/config.ts +++ b/extensions/mcp-server/config.ts @@ -50,7 +50,7 @@ export type McpServerConfig = { const DEFAULT_NAMESPACE = "mayros"; const DEFAULT_TRANSPORT: McpServerTransportMode = "stdio"; -const DEFAULT_PORT = 3100; +const DEFAULT_PORT = 19100; const DEFAULT_HOST = "127.0.0.1"; const DEFAULT_SERVER_NAME = "mayros"; const DEFAULT_SERVER_VERSION = "0.1.0"; diff --git a/extensions/mcp-server/cortex-tools.ts b/extensions/mcp-server/cortex-tools.ts new file mode 100644 index 00000000..30041fee --- /dev/null +++ b/extensions/mcp-server/cortex-tools.ts @@ -0,0 +1,193 @@ +/** + * MCP-friendly Cortex graph query tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export type CortexToolDeps = { + cortexBaseUrl: string; + namespace: string; +}; + +export function createCortexTools(deps: CortexToolDeps): AdaptableTool[] { + const { cortexBaseUrl } = deps; + + return [ + { + name: "mayros_cortex_query", + description: + "Query the semantic knowledge graph. " + + "Find triples by subject, predicate, or object pattern. " + + "Use this for structured knowledge retrieval.", + parameters: Type.Object({ + subject: Type.Optional( + Type.String({ description: "Subject pattern (e.g., 'project:api')" }), + ), + predicate: Type.Optional( + Type.String({ description: "Predicate pattern (e.g., 'uses_framework')" }), + ), + object: Type.Optional(Type.String({ description: "Object value to match" })), + limit: Type.Optional(Type.Number({ description: "Max results (default 20)" })), + }), + execute: async (_id: string, params: Record) => { + const limit = (params.limit as number) ?? 20; + const queryParams = new URLSearchParams(); + if (params.subject) queryParams.set("subject", params.subject as string); + if (params.predicate) queryParams.set("predicate", params.predicate as string); + if (params.object) queryParams.set("object", params.object as string); + queryParams.set("limit", String(limit)); + + const res = await fetch(`${cortexBaseUrl}/api/v1/triples?${queryParams}`); + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `Query failed: ${res.statusText}` }], + }; + } + + const data = (await res.json()) as { + triples: Array<{ subject: string; predicate: string; object: unknown }>; + }; + if (!data.triples || data.triples.length === 0) { + return { content: [{ type: "text" as const, text: "No triples found." }] }; + } + + const formatted = data.triples + .map((t) => ` ${t.subject} -> ${t.predicate} -> ${JSON.stringify(t.object)}`) + .join("\n"); + + return { + content: [ + { + type: "text" as const, + text: `Found ${data.triples.length} triples:\n${formatted}`, + }, + ], + }; + }, + }, + + { + name: "mayros_cortex_store", + description: + "Store a fact in the semantic knowledge graph as an RDF triple. " + + "Use subject-predicate-object structure for structured knowledge.", + parameters: Type.Object({ + subject: Type.String({ description: "Subject (e.g., 'project:payments-api')" }), + predicate: Type.String({ + description: "Predicate/relation (e.g., 'uses_framework')", + }), + object: Type.String({ description: "Object/value (e.g., 'Express.js')" }), + }), + execute: async (_id: string, params: Record) => { + const res = await fetch(`${cortexBaseUrl}/api/v1/triples`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + subject: params.subject, + predicate: params.predicate, + object: params.object, + }), + }); + + if (!res.ok) { + return { + content: [{ type: "text" as const, text: `Store failed: ${res.statusText}` }], + }; + } + + return { + content: [ + { + type: "text" as const, + text: `Stored: ${params.subject as string} -> ${params.predicate as string} -> ${params.object as string}`, + }, + ], + }; + }, + }, + + { + name: "mayros_memory_stats", + description: + "Get memory system statistics: STM entries, LTM entities, HNSW index size, graph triple count.", + parameters: Type.Object({}), + execute: async () => { + const results: string[] = []; + + // Ineru stats + try { + const memRes = await fetch(`${cortexBaseUrl}/api/v1/memory/stats`); + if (memRes.ok) { + const stats = (await memRes.json()) as { + stm_count: number; + stm_capacity: number; + ltm_entity_count: number; + ltm_link_count: number; + total_memory_bytes: number; + }; + results.push( + "Ineru Memory:", + ` STM: ${stats.stm_count} / ${stats.stm_capacity} entries`, + ` LTM: ${stats.ltm_entity_count} entities, ${stats.ltm_link_count} links`, + ` Size: ${(stats.total_memory_bytes / 1024).toFixed(1)} KB`, + ); + } + } catch { + /* Cortex unavailable */ + } + + // HNSW stats + try { + const idxRes = await fetch(`${cortexBaseUrl}/api/v1/memory/index/stats`); + if (idxRes.ok) { + const idx = (await idxRes.json()) as { + point_count: number; + dimensions: number; + memory_bytes: number; + }; + results.push( + "HNSW Vector Index:", + ` Points: ${idx.point_count}`, + ` Dimensions: ${idx.dimensions}`, + ` Size: ${(idx.memory_bytes / 1024).toFixed(1)} KB`, + ); + } + } catch { + /* */ + } + + // Graph stats + try { + const graphRes = await fetch(`${cortexBaseUrl}/api/v1/stats`); + if (graphRes.ok) { + const stats = (await graphRes.json()) as { + graph: { + triple_count: number; + subject_count: number; + predicate_count: number; + }; + }; + results.push( + "Knowledge Graph:", + ` Triples: ${stats.graph.triple_count}`, + ` Subjects: ${stats.graph.subject_count}`, + ` Predicates: ${stats.graph.predicate_count}`, + ); + } + } catch { + /* */ + } + + return { + content: [ + { + type: "text" as const, + text: results.length > 0 ? results.join("\n") : "Cortex sidecar not running.", + }, + ], + }; + }, + }, + ]; +} diff --git a/extensions/mcp-server/governance-tools.ts b/extensions/mcp-server/governance-tools.ts new file mode 100644 index 00000000..c7dc8892 --- /dev/null +++ b/extensions/mcp-server/governance-tools.ts @@ -0,0 +1,79 @@ +/** + * MCP-friendly governance tools. + */ + +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export function createGovernanceTools(): AdaptableTool[] { + return [ + { + name: "mayros_policy_check", + description: + "Check if an action is allowed by the project governance policies. " + + "Evaluates tool calls, file operations, and commands against MAYROS.md rules.", + parameters: Type.Object({ + action: Type.String({ + description: 'Action type: "tool_call", "file_write", "file_delete", "shell_command"', + }), + target: Type.String({ + description: "Target of the action (tool name, file path, or command)", + }), + details: Type.Optional(Type.String({ description: "Additional context about the action" })), + }), + execute: async (_id: string, params: Record) => { + const action = params.action as string; + const target = params.target as string; + + const { readFile, access } = await import("node:fs/promises"); + const policyPath = `${process.cwd()}/MAYROS.md`; + + try { + await access(policyPath); + const content = await readFile(policyPath, "utf-8"); + + // Pattern matching against DENY/ALLOW rules + const denyPatterns: string[] = []; + for (const line of content.split("\n")) { + const trimmed = line.trim(); + if (trimmed.startsWith("- DENY:")) { + denyPatterns.push(trimmed.slice(7).trim()); + } + } + + // Check deny rules + for (const pattern of denyPatterns) { + if (target.includes(pattern) || action.includes(pattern)) { + return { + content: [ + { + type: "text" as const, + text: `DENIED: "${target}" matches deny rule "${pattern}"`, + }, + ], + }; + } + } + + return { + content: [ + { + type: "text" as const, + text: `ALLOWED: "${action}" on "${target}" — no deny rules matched (${denyPatterns.length} rules checked)`, + }, + ], + }; + } catch { + return { + content: [ + { + type: "text" as const, + text: `ALLOWED (no policy): No MAYROS.md found at ${policyPath}. All actions permitted.`, + }, + ], + }; + } + }, + }, + ]; +} diff --git a/extensions/mcp-server/index.ts b/extensions/mcp-server/index.ts index 744dd1c3..f54030d3 100644 --- a/extensions/mcp-server/index.ts +++ b/extensions/mcp-server/index.ts @@ -17,12 +17,7 @@ import type { MayrosPluginApi, MayrosPluginToolContext } from "@apilium/mayros"; import { mcpServerConfigSchema, type McpServerConfig } from "./config.js"; import { McpServer, type McpServerOptions } from "./server.js"; import type { AdaptableTool } from "./tool-adapter.js"; -import type { - ResourceDataSources, - AgentInfo, - ConventionInfo, - RuleInfo, -} from "./resource-provider.js"; +import type { ResourceDataSources, AgentInfo } from "./resource-provider.js"; import type { PromptDataSources } from "./prompt-provider.js"; // ============================================================================ @@ -47,9 +42,7 @@ const mcpServerPlugin = { // at module load time. resolvePluginTools discovers all registered // plugin tools for the given context and returns AnyAgentTool[]. const { resolvePluginTools } = (await import("../../src/plugins/tools.js")) as { - resolvePluginTools: (params: { - context: MayrosPluginToolContext; - }) => Array<{ + resolvePluginTools: (params: { context: MayrosPluginToolContext }) => Array<{ name: string; label?: string; description?: string; @@ -154,7 +147,7 @@ const mcpServerPlugin = { serve .option("--stdio", "Use stdio transport (for IDE integration)") .option("--http", "Use HTTP transport (for remote clients)") - .option("--port ", "HTTP port (default: 3100)", parseInt) + .option("--port ", "HTTP port (default: 19100)", parseInt) .option("--host ", "HTTP host (default: 127.0.0.1)") .action(async (opts: { stdio?: boolean; http?: boolean; port?: number; host?: string }) => { const transport = opts.stdio ? "stdio" : opts.http ? "http" : cfg.transport; @@ -168,10 +161,53 @@ const mcpServerPlugin = { host, }; - const tools = await collectTools({}); + // Auto-start Cortex sidecar for memory and graph tools + let sidecar: { stop: () => Promise } | null = null; + try { + const { CortexSidecar } = (await import("../memory-semantic/cortex-sidecar.js")) as { + CortexSidecar: new (cfg: unknown) => { + start: () => Promise; + stop: () => Promise; + }; + }; + const instance = new CortexSidecar(serverCfg.cortex); + const started = await instance.start(); + if (started) { + sidecar = instance; + api.logger.info("Cortex sidecar started for MCP server"); + } else { + api.logger.warn("Cortex sidecar failed to start — memory tools will be unavailable"); + } + } catch (err) { + api.logger.warn(`Cortex sidecar not available: ${String(err)}`); + } + + // Collect auto-discovered plugin tools + const pluginTools = await collectTools({}); + + // Register dedicated MCP tools + const cortexPort = cfg.cortex?.port ?? 19090; + const cortexBase = `http://127.0.0.1:${cortexPort}`; + const ns = serverCfg.agentNamespace || "mayros"; + + const { createMemoryTools } = await import("./memory-tools.js"); + const { createBudgetTools } = await import("./budget-tools.js"); + const { createGovernanceTools } = await import("./governance-tools.js"); + const { createCortexTools } = await import("./cortex-tools.js"); + + const mcpTools: AdaptableTool[] = [ + ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createBudgetTools(), + ...createGovernanceTools(), + ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ]; + + // Combine: dedicated MCP tools first, then auto-discovered plugin tools + const allTools = [...mcpTools, ...pluginTools]; + const serverOpts: McpServerOptions = { config: serverCfg, - tools, + tools: allTools, resourceSources, promptSources, logger: { @@ -184,6 +220,16 @@ const mcpServerPlugin = { server = new McpServer(serverOpts); await server.start(); + // Register shutdown handler for sidecar cleanup + const shutdown = () => { + void (async () => { + if (sidecar) await sidecar.stop(); + await server?.stop(); + })(); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + if (transport !== "stdio") { const status = server.status(); api.logger.info( @@ -191,15 +237,38 @@ const mcpServerPlugin = { ); // Keep process alive for HTTP mode await new Promise((resolve) => { - process.on("SIGINT", () => { - void server?.stop().then(resolve); - }); - process.on("SIGTERM", () => { - void server?.stop().then(resolve); - }); + process.on("SIGINT", resolve); + process.on("SIGTERM", resolve); }); } }); + + // mcp-setup command + program + .command("mcp-setup") + .description("Register Mayros as an MCP server in Claude (Code or Desktop)") + .option("--desktop", "Configure Claude Desktop (writes config file)") + .option("--stdio", "Use stdio transport (default)") + .option("--http", "Use HTTP transport (connect to pre-running server)") + .option("--port ", "HTTP port (default: 19100)", parseInt) + .option("--host ", "HTTP host (default: 127.0.0.1)") + .action( + async (opts: { + desktop?: boolean; + stdio?: boolean; + http?: boolean; + port?: number; + host?: string; + }) => { + const { setupClaudeCodeMcp } = await import("./setup-claude.js"); + await setupClaudeCodeMcp({ + port: opts.port ?? cfg.port, + host: opts.host ?? cfg.host, + transport: opts.http ? "http" : "stdio", + target: opts.desktop ? "desktop" : "code", + }); + }, + ); }); // ── Register service lifecycle ────────────────────────────────── @@ -247,7 +316,7 @@ const mcpServerPlugin = { }); return res.matches.map((m) => ({ id: m.subject.split(":").pop() ?? "", - text: String(m.object), + text: typeof m.object === "string" ? m.object : JSON.stringify(m.object), category: "general", source: "cortex", confidence: 1, @@ -291,7 +360,8 @@ const mcpServerPlugin = { if (!match) return null; return { id, - text: String(match.object), + text: + typeof match.object === "string" ? match.object : JSON.stringify(match.object), category: "general", source: "cortex", confidence: 1, @@ -311,7 +381,7 @@ const mcpServerPlugin = { }); return res.matches.map((m) => ({ id: m.subject.split(":").pop() ?? "", - content: String(m.object), + content: typeof m.object === "string" ? m.object : JSON.stringify(m.object), scope: "global", priority: 0, source: "cortex", @@ -332,7 +402,8 @@ const mcpServerPlugin = { if (!match) return null; return { id, - content: String(match.object), + content: + typeof match.object === "string" ? match.object : JSON.stringify(match.object), scope: "global", priority: 0, source: "cortex", @@ -354,7 +425,7 @@ const mcpServerPlugin = { predicate: `${ns}:rule:content`, }); return res.matches.map((m) => ({ - content: String(m.object), + content: typeof m.object === "string" ? m.object : JSON.stringify(m.object), scope, priority: 0, })); diff --git a/extensions/mcp-server/memory-tools.ts b/extensions/mcp-server/memory-tools.ts new file mode 100644 index 00000000..ecf8f76b --- /dev/null +++ b/extensions/mcp-server/memory-tools.ts @@ -0,0 +1,273 @@ +/** + * MCP-friendly memory tools. + * + * Wraps Cortex/Ineru APIs with a simple remember/recall/search/forget interface + * designed for external MCP clients (Claude Code, Cursor, etc.). + */ + +import { randomBytes } from "node:crypto"; +import { Type } from "@sinclair/typebox"; +import type { AdaptableTool } from "./tool-adapter.js"; + +export type MemoryToolDeps = { + cortexBaseUrl: string; + namespace: string; +}; + +export function createMemoryTools(deps: MemoryToolDeps): AdaptableTool[] { + const { cortexBaseUrl, namespace } = deps; + + return [ + // ── mayros_remember ────────────────────────────────────────────── + { + name: "mayros_remember", + description: + "Store information in persistent semantic memory. " + + "Use this to remember facts, decisions, preferences, patterns, " + + "or any context that should persist across sessions.", + parameters: Type.Object({ + content: Type.String({ + description: "The information to remember (natural language)", + }), + category: Type.Optional( + Type.String({ + description: + 'Category: "fact", "decision", "preference", "pattern", "code", "architecture"', + }), + ), + tags: Type.Optional( + Type.Array(Type.String(), { + description: "Tags for easier recall (e.g., ['payments', 'api'])", + }), + ), + importance: Type.Optional( + Type.Number({ + description: "Importance 0.0-1.0 (default 0.7). Higher = kept longer in memory", + }), + ), + }), + execute: async (_id: string, params: Record) => { + const content = params.content as string; + const category = (params.category as string) ?? "general"; + const tags = (params.tags as string[]) ?? []; + const importance = (params.importance as number) ?? 0.7; + + // Store as RDF triple in Cortex (timestamp + random suffix to avoid collisions) + const subject = `${namespace}:memory:${Date.now()}-${randomBytes(4).toString("hex")}`; + const triples = [ + { subject, predicate: `${namespace}:memory:content`, object: content }, + { subject, predicate: `${namespace}:memory:category`, object: category }, + { subject, predicate: `${namespace}:memory:importance`, object: String(importance) }, + ...tags.map((tag) => ({ + subject, + predicate: `${namespace}:memory:tag`, + object: tag, + })), + ]; + + // Store in Cortex graph + for (const triple of triples) { + await fetch(`${cortexBaseUrl}/api/v1/triples`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(triple), + }); + } + + // Also store in Ineru STM for vector search + await fetch(`${cortexBaseUrl}/api/v1/memory/remember`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + entry_type: category, + data: { content, tags }, + tags, + importance, + }), + }); + + return { + content: [ + { + type: "text" as const, + text: `Remembered: "${content.slice(0, 80)}${content.length > 80 ? "..." : ""}" [${category}]${tags.length > 0 ? ` #${tags.join(" #")}` : ""}`, + }, + ], + }; + }, + }, + + // ── mayros_recall ──────────────────────────────────────────────── + { + name: "mayros_recall", + description: + "Search persistent memory for previously stored information. " + + "Query by text (semantic match), tags, or category. " + + "Returns relevant memories from past sessions.", + parameters: Type.Object({ + query: Type.Optional(Type.String({ description: "Text to search for (semantic match)" })), + tags: Type.Optional(Type.Array(Type.String(), { description: "Filter by tags" })), + category: Type.Optional(Type.String({ description: "Filter by category" })), + limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })), + }), + execute: async (_id: string, params: Record) => { + const query = params.query as string | undefined; + const tags = params.tags as string[] | undefined; + const category = params.category as string | undefined; + const limit = (params.limit as number) ?? 10; + + // Query Ineru recall endpoint + const recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + text: query, + tags: tags ?? [], + entry_type: category, + limit, + }), + }); + + if (!recallRes.ok) { + // Fallback: query Cortex graph directly + const graphRes = await fetch( + `${cortexBaseUrl}/api/v1/triples?predicate=${encodeURIComponent(`${namespace}:memory:content`)}&limit=${limit}`, + ); + const graphData = (await graphRes.json()) as { + triples?: Array<{ object: string }>; + }; + const triples = graphData.triples ?? []; + + return { + content: [ + { + type: "text" as const, + text: + triples.length > 0 + ? triples.map((t, i) => `${i + 1}. ${t.object}`).join("\n") + : "No memories found.", + }, + ], + }; + } + + const memories = (await recallRes.json()) as Array<{ + id: string; + entry_type: string; + data: { content?: string }; + tags: string[]; + importance: number; + relevance: number; + source: string; + }>; + + if (memories.length === 0) { + return { + content: [{ type: "text" as const, text: "No memories found." }], + }; + } + + const formatted = memories + .map( + (m, i) => + `${i + 1}. [${m.entry_type}] ${m.data.content ?? JSON.stringify(m.data)}` + + (m.tags.length > 0 ? ` #${m.tags.join(" #")}` : "") + + ` (relevance: ${(m.relevance * 100).toFixed(0)}%, source: ${m.source})`, + ) + .join("\n"); + + return { + content: [{ type: "text" as const, text: formatted }], + }; + }, + }, + + // ── mayros_search ──────────────────────────────────────────────── + { + name: "mayros_search", + description: + "Vector similarity search over memory. " + + "Finds semantically similar memories even with different wording.", + parameters: Type.Object({ + text: Type.String({ + description: "Text to search for. Will be matched against stored memories.", + }), + k: Type.Optional(Type.Number({ description: "Number of results (default 5)" })), + min_similarity: Type.Optional( + Type.Number({ description: "Minimum similarity 0.0-1.0 (default 0.3)" }), + ), + }), + execute: async (_id: string, params: Record) => { + const text = params.text as string; + const k = (params.k as number) ?? 5; + + const recallRes = await fetch(`${cortexBaseUrl}/api/v1/memory/recall`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text, limit: k }), + }); + + if (!recallRes.ok) { + return { + content: [ + { + type: "text" as const, + text: "Vector search unavailable. Cortex may not be running.", + }, + ], + }; + } + + const results = (await recallRes.json()) as Array<{ + data: { content?: string }; + relevance: number; + entry_type: string; + tags: string[]; + }>; + + if (results.length === 0) { + return { + content: [{ type: "text" as const, text: "No similar memories found." }], + }; + } + + const formatted = results + .map( + (r, i) => + `${i + 1}. [${(r.relevance * 100).toFixed(0)}%] ${r.data.content ?? JSON.stringify(r.data)}` + + (r.tags.length > 0 ? ` #${r.tags.join(" #")}` : ""), + ) + .join("\n"); + + return { + content: [{ type: "text" as const, text: formatted }], + }; + }, + }, + + // ── mayros_forget ──────────────────────────────────────────────── + { + name: "mayros_forget", + description: "Delete a specific memory entry by ID.", + parameters: Type.Object({ + id: Type.String({ description: "Memory ID to delete" }), + }), + execute: async (_id: string, params: Record) => { + const memoryId = params.id as string; + const res = await fetch(`${cortexBaseUrl}/api/v1/memory/${encodeURIComponent(memoryId)}`, { + method: "DELETE", + }); + return { + content: [ + { + type: "text" as const, + text: res.ok + ? `Memory ${memoryId} forgotten.` + : `Failed to forget: ${res.statusText}`, + }, + ], + }; + }, + }, + ]; +} diff --git a/extensions/mcp-server/package.json b/extensions/mcp-server/package.json index 98458e20..8f0a00e9 100644 --- a/extensions/mcp-server/package.json +++ b/extensions/mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-mcp-server", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "MCP server exposing Mayros tools, Cortex resources, and workflow prompts via Model Context Protocol", "type": "module", diff --git a/extensions/mcp-server/server.ts b/extensions/mcp-server/server.ts index 21c08e99..5e715b02 100644 --- a/extensions/mcp-server/server.ts +++ b/extensions/mcp-server/server.ts @@ -181,12 +181,14 @@ export class McpServer { } private async startHttp(): Promise { + const cortexHealthUrl = `http://${this.config.cortex.host}:${this.config.cortex.port}/api/v1/health`; this.httpTransport = new McpHttpTransport({ dispatcher: this.dispatcher, port: this.config.port, host: this.config.host, authToken: this.config.auth.token, allowedOrigins: this.config.auth.allowedOrigins, + cortexHealthUrl, onError: (err) => { this.logger.error(`[mcp-server:http] ${err.message}`); }, diff --git a/extensions/mcp-server/setup-claude.ts b/extensions/mcp-server/setup-claude.ts new file mode 100644 index 00000000..3b714cd0 --- /dev/null +++ b/extensions/mcp-server/setup-claude.ts @@ -0,0 +1,164 @@ +/** + * Auto-configure Claude to use Mayros MCP server. + * + * Targets: + * --desktop → Claude Desktop (writes claude_desktop_config.json) + * (default) → Claude Code CLI (`claude mcp add`) + * + * Resolves absolute paths to node and mayros.mjs so Claude Desktop + * can find the binary regardless of shell PATH. + */ + +import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { execSync } from "node:child_process"; +import { homedir, platform } from "node:os"; + +// ── Types ────────────────────────────────────────────────────────── + +export type SetupTarget = "code" | "desktop"; + +export type SetupClaudeOpts = { + port: number; + host: string; + transport?: "stdio" | "http"; + target?: SetupTarget; +}; + +// ── Public API ───────────────────────────────────────────────────── + +export async function setupClaudeCodeMcp(opts: SetupClaudeOpts): Promise { + const target = opts.target ?? "code"; + + if (target === "desktop") { + setupDesktop(); + } else { + setupCode(opts); + } +} + +// ── Claude Code ──────────────────────────────────────────────────── + +function setupCode(opts: SetupClaudeOpts): void { + const transport = opts.transport ?? "stdio"; + + try { + if (transport === "stdio") { + execSync("claude mcp add mayros -- mayros serve --stdio", { stdio: "inherit" }); + } else { + const url = `http://${opts.host}:${opts.port}/mcp`; + execSync(`claude mcp add mayros -s http --url ${url}`, { stdio: "inherit" }); + } + console.log("Mayros registered with Claude Code."); + } catch { + console.log("\nTo connect Mayros to Claude Code manually:\n"); + console.log(" claude mcp add mayros -- mayros serve --stdio\n"); + } +} + +// ── Claude Desktop ───────────────────────────────────────────────── + +function setupDesktop(): void { + const configPath = getDesktopConfigPath(); + if (!configPath) { + console.error("Could not determine Claude Desktop config path for this platform."); + return; + } + + const nodePath = resolveNodePath(); + const mayrosPath = resolveMayrosEntryPath(); + + if (!nodePath || !mayrosPath) { + console.error("Could not resolve paths to node or mayros."); + console.log("\nManual setup — add to", configPath, ":\n"); + printManualDesktopConfig(); + return; + } + + // Read existing config or create new + let config: Record = {}; + if (existsSync(configPath)) { + try { + config = JSON.parse(readFileSync(configPath, "utf-8")) as Record; + } catch { + // Corrupt file — start fresh but preserve what we can + } + } + + // Merge mcpServers + const mcpServers = (config.mcpServers ?? {}) as Record; + mcpServers.mayros = { + command: nodePath, + args: [mayrosPath, "serve", "--stdio"], + }; + config.mcpServers = mcpServers; + + // Write + const dir = dirname(configPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8"); + + console.log(`Mayros registered in Claude Desktop config.`); + console.log(` Config: ${configPath}`); + console.log(` Node: ${nodePath}`); + console.log(` Entry: ${mayrosPath}`); + console.log(`\nRestart Claude Desktop to activate.`); +} + +// ── Helpers ──────────────────────────────────────────────────────── + +function getDesktopConfigPath(): string | null { + const home = homedir(); + const os = platform(); + + if (os === "darwin") { + return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"); + } + if (os === "win32") { + return join(home, "AppData", "Roaming", "Claude", "claude_desktop_config.json"); + } + if (os === "linux") { + return join(home, ".config", "Claude", "claude_desktop_config.json"); + } + return null; +} + +function resolveNodePath(): string | null { + try { + return execSync("which node", { encoding: "utf-8" }).trim(); + } catch { + // Fallback common paths + const candidates = ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"]; + return candidates.find((p) => existsSync(p)) ?? null; + } +} + +function resolveMayrosEntryPath(): string | null { + // 1. Check global npm install + try { + const globalDir = execSync("npm root -g", { encoding: "utf-8" }).trim(); + const globalEntry = join(globalDir, "@apilium", "mayros", "mayros.mjs"); + if (existsSync(globalEntry)) return globalEntry; + } catch { + // npm not available + } + + // 2. Check relative to this file (running from source) + const localEntry = join(dirname(dirname(__dirname)), "mayros.mjs"); + if (existsSync(localEntry)) return localEntry; + + return null; +} + +function printManualDesktopConfig(): void { + console.log(`{ + "mcpServers": { + "mayros": { + "command": "/path/to/node", + "args": ["/path/to/mayros.mjs", "serve", "--stdio"] + } + } +}`); +} diff --git a/extensions/mcp-server/transport-http.ts b/extensions/mcp-server/transport-http.ts index b4ac4c35..b05cea81 100644 --- a/extensions/mcp-server/transport-http.ts +++ b/extensions/mcp-server/transport-http.ts @@ -9,6 +9,7 @@ */ import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http"; +import { randomUUID } from "node:crypto"; import type { McpProtocolDispatcher } from "./protocol.js"; // ============================================================================ @@ -21,6 +22,7 @@ export type HttpTransportOptions = { host: string; authToken?: string; allowedOrigins: string[]; + cortexHealthUrl?: string; onError?: (err: Error) => void; onRequest?: (method: string, path: string) => void; }; @@ -35,9 +37,11 @@ export class McpHttpTransport { private readonly host: string; private readonly authToken?: string; private readonly allowedOrigins: string[]; + private readonly cortexHealthUrl: string; private readonly onError?: (err: Error) => void; private readonly onRequest?: (method: string, path: string) => void; private server: Server | null = null; + private sseSessions = new Map(); constructor(options: HttpTransportOptions) { this.dispatcher = options.dispatcher; @@ -45,6 +49,7 @@ export class McpHttpTransport { this.host = options.host; this.authToken = options.authToken; this.allowedOrigins = options.allowedOrigins; + this.cortexHealthUrl = options.cortexHealthUrl ?? "http://127.0.0.1:19090/api/v1/health"; this.onError = options.onError; this.onRequest = options.onRequest; } @@ -69,6 +74,12 @@ export class McpHttpTransport { /** Stop the HTTP server. */ async stop(): Promise { + // Close all active SSE sessions + for (const [id, res] of this.sseSessions) { + if (!res.destroyed) res.end(); + this.sseSessions.delete(id); + } + return new Promise((resolve) => { if (!this.server) { resolve(); @@ -119,10 +130,24 @@ export class McpHttpTransport { this.setCorsHeaders(req, res); - // Health check + // Health check (enhanced with Cortex status) if (url === "/health" && method === "GET") { + let cortexHealthy = false; + try { + const cortexRes = await fetch(this.cortexHealthUrl); + cortexHealthy = cortexRes.ok; + } catch { + /* Cortex not available */ + } + res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ status: "ok", transport: "streamable-http" })); + res.end( + JSON.stringify({ + status: "ok", + transport: "streamable-http", + cortex: cortexHealthy ? "healthy" : "unavailable", + }), + ); return; } @@ -138,6 +163,18 @@ export class McpHttpTransport { return; } + // Legacy SSE transport (Claude Desktop compatibility — MCP spec 2024-11-05) + if (url === "/sse" && method === "GET") { + this.handleLegacySse(res); + return; + } + + // Legacy SSE session POST endpoint + if (url.startsWith("/mcp/session/") && method === "POST") { + await this.handleLegacySsePost(url, req, res); + return; + } + // Not found res.writeHead(404, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Not found" })); @@ -197,6 +234,81 @@ export class McpHttpTransport { }); } + private handleLegacySse(res: ServerResponse): void { + const sessionId = randomUUID(); + const postUrl = `/mcp/session/${sessionId}`; + + res.writeHead(200, { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }); + + // Send endpoint URL per legacy MCP SSE spec + res.write(`event: endpoint\ndata: ${postUrl}\n\n`); + + // Store SSE connection for this session + this.sseSessions.set(sessionId, res); + + // Keep alive + const keepAlive = setInterval(() => { + if (res.destroyed) { + clearInterval(keepAlive); + return; + } + res.write(": ping\n\n"); + }, 15_000); + + res.on("close", () => { + clearInterval(keepAlive); + this.sseSessions.delete(sessionId); + }); + } + + private async handleLegacySsePost( + url: string, + req: IncomingMessage, + res: ServerResponse, + ): Promise { + const sessionId = url.split("/mcp/session/")[1]; + if (!sessionId) { + res.writeHead(400); + res.end(); + return; + } + + const sseRes = this.sseSessions.get(sessionId); + if (!sseRes || sseRes.destroyed) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "SSE session not found" })); + return; + } + + try { + const body = await readBody(req); + const response = await this.dispatcher.handleMessage(body); + + // Send response through the SSE stream + if (response) { + sseRes.write(`event: message\ndata: ${response}\n\n`); + } + + // Acknowledge the POST + res.writeHead(202); + res.end(); + } catch (err) { + this.onError?.(err instanceof Error ? err : new Error(String(err))); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + jsonrpc: "2.0", + id: null, + error: { code: -32603, message: "Internal server error" }, + }), + ); + } + } + private setCorsHeaders(req: IncomingMessage, res: ServerResponse): void { const origin = req.headers.origin ?? "*"; const allowed = diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json index 61cc82b1..0bfab2e6 100644 --- a/extensions/memory-core/package.json +++ b/extensions/memory-core/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-core", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros core memory search plugin", "type": "module", diff --git a/extensions/memory-lancedb/package.json b/extensions/memory-lancedb/package.json index 1ca17dfe..6fa2bf12 100644 --- a/extensions/memory-lancedb/package.json +++ b/extensions/memory-lancedb/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-lancedb", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros LanceDB-backed long-term memory plugin with auto-recall/capture", "type": "module", diff --git a/extensions/memory-semantic/package.json b/extensions/memory-semantic/package.json index c06476ab..033e232b 100644 --- a/extensions/memory-semantic/package.json +++ b/extensions/memory-semantic/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-memory-semantic", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros semantic memory plugin via AIngle Cortex sidecar (RDF triples, identity graph, Ineru STM/LTM)", "type": "module", diff --git a/extensions/minimax-portal-auth/package.json b/extensions/minimax-portal-auth/package.json index 7f7831dd..00b172f1 100644 --- a/extensions/minimax-portal-auth/package.json +++ b/extensions/minimax-portal-auth/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-minimax-portal-auth", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros MiniMax Portal OAuth provider plugin", "type": "module", diff --git a/extensions/msteams/package.json b/extensions/msteams/package.json index 041a86a1..506f8541 100644 --- a/extensions/msteams/package.json +++ b/extensions/msteams/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-msteams", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Microsoft Teams channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/nextcloud-talk/package.json b/extensions/nextcloud-talk/package.json index 21e006c5..4f480743 100644 --- a/extensions/nextcloud-talk/package.json +++ b/extensions/nextcloud-talk/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-nextcloud-talk", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Nextcloud Talk channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/nostr/package.json b/extensions/nostr/package.json index a31ab236..f2702af7 100644 --- a/extensions/nostr/package.json +++ b/extensions/nostr/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-nostr", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Nostr channel plugin for NIP-04 encrypted DMs", "license": "MIT", "type": "module", diff --git a/extensions/open-prose/package.json b/extensions/open-prose/package.json index 3f53b96d..b40e78ff 100644 --- a/extensions/open-prose/package.json +++ b/extensions/open-prose/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-open-prose", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "OpenProse VM skill pack plugin (slash command + telemetry).", "type": "module", diff --git a/extensions/osameru-governance/package.json b/extensions/osameru-governance/package.json index d0f72d0c..796ac6e0 100644 --- a/extensions/osameru-governance/package.json +++ b/extensions/osameru-governance/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-osameru-governance", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros governance control plane — policy enforcement, HMAC audit trail, trust tiers", "type": "module", diff --git a/extensions/semantic-observability/package.json b/extensions/semantic-observability/package.json index ef3cbaac..6079a77a 100644 --- a/extensions/semantic-observability/package.json +++ b/extensions/semantic-observability/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-semantic-observability", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros semantic observability plugin — structured tracing of agent decisions as RDF events", "type": "module", diff --git a/extensions/semantic-skills/package.json b/extensions/semantic-skills/package.json index 0308525d..b6ce33e0 100644 --- a/extensions/semantic-skills/package.json +++ b/extensions/semantic-skills/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-semantic-skills", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros semantic skills plugin — graph-aware skills with PoL assertions, ZK proofs, and permission gating", "type": "module", diff --git a/extensions/shared/cortex-version.ts b/extensions/shared/cortex-version.ts index 32160d8f..69acf541 100644 --- a/extensions/shared/cortex-version.ts +++ b/extensions/shared/cortex-version.ts @@ -5,4 +5,4 @@ * features or API changes. `mayros update` and the sidecar startup * check will compare the installed binary against this value. */ -export const REQUIRED_CORTEX_VERSION = "0.4.2"; +export const REQUIRED_CORTEX_VERSION = "0.4.3"; diff --git a/extensions/signal/package.json b/extensions/signal/package.json index 0c682071..e8961a5b 100644 --- a/extensions/signal/package.json +++ b/extensions/signal/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-signal", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Signal channel plugin", "type": "module", diff --git a/extensions/skill-hub/package.json b/extensions/skill-hub/package.json index 4aa24f2b..b71d0912 100644 --- a/extensions/skill-hub/package.json +++ b/extensions/skill-hub/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-skill-hub", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Apilium Hub marketplace — publish, install, sign, and verify semantic skills", "type": "module", diff --git a/extensions/slack/package.json b/extensions/slack/package.json index 9e48e87e..6dfacec4 100644 --- a/extensions/slack/package.json +++ b/extensions/slack/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-slack", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Slack channel plugin", "type": "module", diff --git a/extensions/telegram/package.json b/extensions/telegram/package.json index 45fb95b7..194750c7 100644 --- a/extensions/telegram/package.json +++ b/extensions/telegram/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-telegram", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Telegram channel plugin", "type": "module", diff --git a/extensions/tlon/package.json b/extensions/tlon/package.json index 12fdce64..db6f1e64 100644 --- a/extensions/tlon/package.json +++ b/extensions/tlon/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-tlon", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Tlon/Urbit channel plugin", "type": "module", diff --git a/extensions/token-economy/package.json b/extensions/token-economy/package.json index 2b923168..53159b0d 100644 --- a/extensions/token-economy/package.json +++ b/extensions/token-economy/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-token-economy", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros token economy plugin — per-session cost tracking, configurable budgets with soft-stop, and prompt-level memoization", "type": "module", diff --git a/extensions/tomeru-guard/package.json b/extensions/tomeru-guard/package.json index 282f4c64..cf2b5b8b 100644 --- a/extensions/tomeru-guard/package.json +++ b/extensions/tomeru-guard/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-tomeru-guard", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros rate limiting and loop breaking plugin — prevents runaway tool execution", "type": "module", diff --git a/extensions/twitch/package.json b/extensions/twitch/package.json index 6d908ce1..ae6e702b 100644 --- a/extensions/twitch/package.json +++ b/extensions/twitch/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-twitch", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros Twitch channel plugin", "type": "module", diff --git a/extensions/voice-call/package.json b/extensions/voice-call/package.json index c0c4320f..946ab81f 100644 --- a/extensions/voice-call/package.json +++ b/extensions/voice-call/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-voice-call", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros voice-call plugin", "license": "MIT", "type": "module", diff --git a/extensions/whatsapp/package.json b/extensions/whatsapp/package.json index 028f6bec..851165e5 100644 --- a/extensions/whatsapp/package.json +++ b/extensions/whatsapp/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-whatsapp", - "version": "0.1.14", + "version": "0.1.15", "private": true, "description": "Mayros WhatsApp channel plugin", "type": "module", diff --git a/extensions/zalo/package.json b/extensions/zalo/package.json index 665394db..2d61aba1 100644 --- a/extensions/zalo/package.json +++ b/extensions/zalo/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalo", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Zalo channel plugin", "license": "MIT", "type": "module", diff --git a/extensions/zalouser/package.json b/extensions/zalouser/package.json index 1cf17e9f..c2228990 100644 --- a/extensions/zalouser/package.json +++ b/extensions/zalouser/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros-zalouser", - "version": "0.1.14", + "version": "0.1.15", "description": "Mayros Zalo Personal Account plugin via zca-cli", "license": "MIT", "type": "module", diff --git a/package.json b/package.json index 9a331f72..6f619329 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apilium/mayros", - "version": "0.1.14", + "version": "0.1.15", "description": "Multi-channel AI agent framework with knowledge graph, MCP support, and coding CLI", "keywords": [ "agent", diff --git a/src/cli/program/register.subclis.ts b/src/cli/program/register.subclis.ts index b3213e19..56092e97 100644 --- a/src/cli/program/register.subclis.ts +++ b/src/cli/program/register.subclis.ts @@ -440,6 +440,15 @@ const entries: SubCliEntry[] = [ mod.registerServeCli(program); }, }, + { + name: "mcp-setup", + description: "Register Mayros as an MCP server in Claude (Code or Desktop)", + hasSubcommands: false, + register: async (program) => { + const mod = await import("../serve-cli.js"); + mod.registerMcpSetupCli(program); + }, + }, { name: "search", description: "Search conversation history across sessions", diff --git a/src/cli/serve-cli.ts b/src/cli/serve-cli.ts index 2443df4b..c675b3e8 100644 --- a/src/cli/serve-cli.ts +++ b/src/cli/serve-cli.ts @@ -19,22 +19,58 @@ export function registerServeCli(program: Command): void { .description("Start MCP server to expose Mayros tools, resources, and prompts") .option("--stdio", "Use stdio transport (for IDE integration)") .option("--http", "Use HTTP transport (for remote clients)") - .option("--port ", "HTTP port (default: 3100)", parseInt) + .option("--port ", "HTTP port (default: 19100)", parseInt) .option("--host ", "HTTP host (default: 127.0.0.1)") .action(async (opts: { stdio?: boolean; http?: boolean; port?: number; host?: string }) => { const { McpServer } = await import("../../extensions/mcp-server/server.js"); const { mcpServerConfigSchema } = await import("../../extensions/mcp-server/config.js"); const transport = opts.stdio ? ("stdio" as const) : ("http" as const); - const port = opts.port ?? 3100; - const host = opts.host ?? "127.0.0.1"; const config = mcpServerConfigSchema.parse({ transport, - port, - host, + ...(opts.port != null && { port: opts.port }), + ...(opts.host != null && { host: opts.host }), }); + // Auto-start Cortex sidecar + let sidecar: { stop: () => Promise } | null = null; + try { + const { CortexSidecar } = + (await import("../../extensions/memory-semantic/cortex-sidecar.js")) as { + CortexSidecar: new (cfg: unknown) => { + start: () => Promise; + stop: () => Promise; + }; + }; + const instance = new CortexSidecar(config.cortex); + const started = await instance.start(); + if (started) { + sidecar = instance; + process.stderr.write("Cortex sidecar started\n"); + } + } catch { + // Cortex sidecar not available + } + + // Load dedicated MCP tools + const cortexPort = config.cortex?.port ?? 19090; + const cortexBase = `http://127.0.0.1:${cortexPort}`; + const ns = config.agentNamespace || "mayros"; + + const { createMemoryTools } = await import("../../extensions/mcp-server/memory-tools.js"); + const { createBudgetTools } = await import("../../extensions/mcp-server/budget-tools.js"); + const { createGovernanceTools } = + await import("../../extensions/mcp-server/governance-tools.js"); + const { createCortexTools } = await import("../../extensions/mcp-server/cortex-tools.js"); + + const tools = [ + ...createMemoryTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ...createBudgetTools(), + ...createGovernanceTools(), + ...createCortexTools({ cortexBaseUrl: cortexBase, namespace: ns }), + ]; + // Discover agents let agentInfos: Array<{ id: string; @@ -64,7 +100,7 @@ export function registerServeCli(program: Command): void { const server = new McpServer({ config, - tools: [], + tools, resourceSources: { listAgents: () => agentInfos, getAgent: (id) => agentInfos.find((a) => a.id === id) ?? null, @@ -93,6 +129,16 @@ export function registerServeCli(program: Command): void { await server.start(); + // Shutdown handler: stop server + sidecar + const shutdown = () => { + void (async () => { + if (sidecar) await sidecar.stop(); + await server.stop(); + })(); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + if (transport !== "stdio") { const status = server.status(); process.stderr.write( @@ -102,13 +148,37 @@ export function registerServeCli(program: Command): void { ); await new Promise((resolve) => { - process.on("SIGINT", () => { - void server.stop().then(resolve); - }); - process.on("SIGTERM", () => { - void server.stop().then(resolve); - }); + process.on("SIGINT", resolve); + process.on("SIGTERM", resolve); }); } }); } + +export function registerMcpSetupCli(program: Command): void { + program + .command("mcp-setup") + .description("Register Mayros as an MCP server in Claude (Code or Desktop)") + .option("--desktop", "Configure Claude Desktop (writes config file)") + .option("--stdio", "Use stdio transport (default)") + .option("--http", "Use HTTP transport (connect to pre-running server)") + .option("--port ", "HTTP port (default: 19100)", parseInt) + .option("--host ", "HTTP host (default: 127.0.0.1)") + .action( + async (opts: { + desktop?: boolean; + stdio?: boolean; + http?: boolean; + port?: number; + host?: string; + }) => { + const { setupClaudeCodeMcp } = await import("../../extensions/mcp-server/setup-claude.js"); + await setupClaudeCodeMcp({ + port: opts.port ?? 19100, + host: opts.host ?? "127.0.0.1", + transport: opts.http ? "http" : "stdio", + target: opts.desktop ? "desktop" : "code", + }); + }, + ); +}