From 8f6d9420901b22647332ffd8a454938bcd5af153 Mon Sep 17 00:00:00 2001 From: vreshch Date: Sat, 20 Jun 2026 13:49:54 +0200 Subject: [PATCH] feat: stdio bin (agentage-mcp-memory) + out-of-the-box e2e (init -> serve) --- package.json | 3 ++ src/bin/mcp-memory.ts | 23 +++++++++++++ test/e2e.test.ts | 76 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) create mode 100644 src/bin/mcp-memory.ts create mode 100644 test/e2e.test.ts diff --git a/package.json b/package.json index fe21a1d..a576abb 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,9 @@ "exports": { ".": "./dist/index.js" }, + "bin": { + "agentage-mcp-memory": "dist/bin/mcp-memory.js" + }, "files": [ "dist" ], diff --git a/src/bin/mcp-memory.ts b/src/bin/mcp-memory.ts new file mode 100644 index 0000000..41e1e34 --- /dev/null +++ b/src/bin/mcp-memory.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +// @agentage/mcp-memory - the stdio MCP keystone. `npx @agentage/mcp-memory` exposes +// the user's local vaults (~/.agentage/vaults.json, read via @agentage/memory-core) +// as the 6 memory__* tools over stdio, for stdio-only clients (Windsurf, Zed) and as +// the published npm artifact. Zero memory logic - it binds the memory-core server to +// a StdioServerTransport. +// +// stdout is the JSON-RPC wire; all diagnostics MUST go to stderr. + +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { loadLocalServer } from '../server/local-server.js'; + +const main = async (): Promise => { + const server = await loadLocalServer(); + await server.connect(new StdioServerTransport()); + // The transport keeps the process alive until the client disconnects (stdin EOF). +}; + +main().catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err); + process.stderr.write(`[mcp-memory] fatal: ${message}\n`); + process.exit(1); +}); diff --git a/test/e2e.test.ts b/test/e2e.test.ts new file mode 100644 index 0000000..2ecc3d8 --- /dev/null +++ b/test/e2e.test.ts @@ -0,0 +1,76 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { init } from '@agentage/memory-core'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { call } from './fixtures/mcp.js'; + +// The whole point: it works out of the box. `init` scaffolds ~/.agentage + a vault, +// then `npx @agentage/mcp-memory` (here: the built bin) serves it - nothing else. +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); +const bin = join(repoRoot, 'dist/bin/mcp-memory.js'); +const tmps: string[] = []; +const mk = (p: string) => { + const d = mkdtempSync(join(tmpdir(), p)); + tmps.push(d); + return d; +}; + +describe('e2e: init, then the stdio server just works', () => { + beforeAll(() => { + // build memory-core (the file: dep) + this package so the spawned bin can run. + execFileSync('npm', ['run', 'build'], { + cwd: join(repoRoot, '../memory-core'), + stdio: 'ignore', + }); + execFileSync('npm', ['run', 'build'], { cwd: repoRoot, stdio: 'ignore' }); + }, 60_000); + + afterAll(() => { + while (tmps.length) rmSync(tmps.pop()!, { recursive: true, force: true }); + }); + + it('init scaffolds, the bin serves the vault, and the 6 tools round-trip', async () => { + const configDir = mk('mcp-e2e-cfg-'); + const vaultPath = mk('mcp-e2e-vault-'); + + // 1. one call to set everything up - offline. + const setup = await init({ configDir, vaultName: 'work', vaultPath }); + expect(setup.createdConfig).toBe(true); + + // 2. spawn the published-style bin against that config; nothing else configured. + const transport = new StdioClientTransport({ + command: process.execPath, + args: [bin], + env: { ...(process.env as Record), AGENTAGE_CONFIG_DIR: configDir }, + }); + const client = new Client({ name: 'e2e', version: '0' }); + await client.connect(transport); + try { + expect((await client.listTools()).tools).toHaveLength(6); + + const w = await call(client, 'memory__write', { + path: 'hello.md', + body: 'first memory, mentions pkce', + }); + expect(w.isError).toBe(false); + + const r = await call(client, 'memory__read', { path: 'hello.md' }); + expect((r.structured as { body: string }).body).toBe('first memory, mentions pkce'); + + const s = await call(client, 'memory__search', { query: 'pkce' }); + expect((s.structured as { results: Array<{ path: string }> }).results[0].path).toBe( + 'hello.md' + ); + + const l = await call(client, 'memory__list', {}); + expect((l.structured as { files: number }).files).toBe(1); + } finally { + await client.close(); + } + }, 30_000); +});