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 @@
+
+
@@ -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",
+ });
+ },
+ );
+}