Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/mex-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "mex-mcp",
"version": "0.1.0",
"description": "MCP server for mex-agent — exposes check/log/timeline/heartbeat/read-file as native MCP tools",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"bin": {
"mex-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"start": "node dist/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.0",
"mex-agent": "workspace:*",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.4.0",
"typescript": "^5.7.0"
}
}
21 changes: 21 additions & 0 deletions packages/mex-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerCheckTool } from "./tools/check.js";
import { registerLogTool } from "./tools/log.js";
import { registerTimelineTool } from "./tools/timeline.js";
import { registerHeartbeatTool } from "./tools/heartbeat.js";
import { registerReadFileTool } from "./tools/read-file.js";

const server = new McpServer({
name: "mex-mcp",
version: "0.1.0",
});

registerCheckTool(server);
registerLogTool(server);
registerTimelineTool(server);
registerHeartbeatTool(server);
registerReadFileTool(server);

const transport = new StdioServerTransport();
await server.connect(transport);
32 changes: 32 additions & 0 deletions packages/mex-mcp/src/tools/check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { findConfig, runDriftCheck } from "mex-agent";

export function registerCheckTool(server: McpServer) {
server.tool(
"mex_check",
"Run a drift check on the mex scaffold. Returns a DriftReport with a numeric score, issues list, and file count.",
{
projectRoot: z
.string()
.optional()
.describe("Absolute path to the project root. Defaults to cwd."),
},
async ({ projectRoot }) => {
const root = projectRoot ?? process.cwd();
const config = await findConfig(root);
if (!config) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "No mex config found", projectRoot: root }),
},
],
};
}
const report = await runDriftCheck(config);
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
}
);
}
32 changes: 32 additions & 0 deletions packages/mex-mcp/src/tools/heartbeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { findConfig, checkHeartbeat } from "mex-agent";

export function registerHeartbeatTool(server: McpServer) {
server.tool(
"mex_heartbeat",
"Check the mex scaffold heartbeat. Returns ok status, stale files with age in days, and memory cleanup status.",
{
projectRoot: z
.string()
.optional()
.describe("Absolute path to the project root. Defaults to cwd."),
},
async ({ projectRoot }) => {
const root = projectRoot ?? process.cwd();
const config = await findConfig(root);
if (!config) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "No mex config found", projectRoot: root }),
},
],
};
}
const result = checkHeartbeat(config, new Date());
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
);
}
51 changes: 51 additions & 0 deletions packages/mex-mcp/src/tools/log.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { findConfig, appendEvent, readEvents, EVENT_KINDS } from "mex-agent";
import type { EventKind } from "mex-agent";

export function registerLogTool(server: McpServer) {
server.tool(
"mex_log",
`Append an agent event to the mex log, or read recent events. Valid kinds: ${EVENT_KINDS.join(", ")}.`,
{
projectRoot: z
.string()
.optional()
.describe("Absolute path to the project root. Defaults to cwd."),
action: z.enum(["read", "write"]).default("read"),
kind: z
.string()
.optional()
.describe(`Event kind for write (one of: ${EVENT_KINDS.join(", ")}).`),
summary: z.string().optional().describe("Human-readable event summary for write."),
limit: z.number().int().positive().optional().default(20),
},
async ({ projectRoot, action, kind, summary, limit }) => {
const root = projectRoot ?? process.cwd();
const config = await findConfig(root);
if (!config) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "No mex config found", projectRoot: root }),
},
],
};
}
if (action === "write") {
if (!kind || !summary) {
return {
content: [
{ type: "text", text: JSON.stringify({ error: "kind and summary are required for write" }) },
],
};
}
await appendEvent(config, { kind: kind as EventKind, summary });
return { content: [{ type: "text", text: JSON.stringify({ ok: true, kind, summary }) }] };
}
const events = await readEvents(config, { limit });
return { content: [{ type: "text", text: JSON.stringify(events, null, 2) }] };
}
);
}
51 changes: 51 additions & 0 deletions packages/mex-mcp/src/tools/read-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { z } from "zod";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { findConfig } from "mex-agent";

export function registerReadFileTool(server: McpServer) {
server.tool(
"mex_read_file",
"Read a file from the mex scaffold directory (.mex/). Path is relative to .mex/ (e.g. 'AGENTS.md', 'context/stack.md').",
{
projectRoot: z
.string()
.optional()
.describe("Absolute path to the project root. Defaults to cwd."),
file: z
.string()
.describe("Path to the scaffold file relative to .mex/ (e.g. 'AGENTS.md', 'context/stack.md')."),
},
async ({ projectRoot, file }) => {
const root = projectRoot ?? process.cwd();
const config = await findConfig(root);
if (!config) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "No mex config found", projectRoot: root }),
},
],
};
}
const fullPath = join(config.scaffoldRoot, file);
if (!existsSync(fullPath)) {
return {
content: [
{
type: "text",
text: JSON.stringify({
error: `File not found: ${file}`,
scaffoldRoot: config.scaffoldRoot,
}),
},
],
};
}
const content = readFileSync(fullPath, "utf-8");
return { content: [{ type: "text", text: content }] };
}
);
}
49 changes: 49 additions & 0 deletions packages/mex-mcp/src/tools/timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { findConfig, readEvents } from "mex-agent";

export function registerTimelineTool(server: McpServer) {
server.tool(
"mex_timeline",
"Read the mex event timeline, optionally filtered by kind or date range. Good for understanding what an agent did and when.",
{
projectRoot: z
.string()
.optional()
.describe("Absolute path to the project root. Defaults to cwd."),
kind: z
.string()
.optional()
.describe("Filter by event kind (e.g. 'session_start', 'checkpoint', 'note')."),
since: z
.string()
.optional()
.describe("ISO 8601 timestamp — return only events at or after this time."),
limit: z.number().int().positive().optional().default(50),
},
async ({ projectRoot, kind, since, limit }) => {
const root = projectRoot ?? process.cwd();
const config = await findConfig(root);
if (!config) {
return {
content: [
{
type: "text",
text: JSON.stringify({ error: "No mex config found", projectRoot: root }),
},
],
};
}
// over-fetch to allow client-side filtering without multiple round-trips
let events = await readEvents(config, { limit: limit * 4 });
if (kind) events = events.filter((e) => e.kind === kind);
if (since) {
const sinceMs = new Date(since).getTime();
events = events.filter((e) => new Date(e.timestamp).getTime() >= sinceMs);
}
return {
content: [{ type: "text", text: JSON.stringify(events.slice(0, limit), null, 2) }],
};
}
);
}
9 changes: 9 additions & 0 deletions packages/mex-mcp/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
13 changes: 13 additions & 0 deletions packages/mex-mcp/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: { index: "src/index.ts" },
format: ["esm"],
target: "node20",
outDir: "dist",
clean: true,
splitting: false,
sourcemap: true,
dts: true,
banner: { js: "#!/usr/bin/env node" },
});