Skip to content
Merged
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
26 changes: 2 additions & 24 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,51 +19,29 @@ jobs:
timeout-minutes: 10

steps:
- name: Checkout mcp-memory
uses: actions/checkout@v7
with:
path: mcp-memory

# @agentage/memory-core is a local file: dependency until it publishes to npm,
# so build it as a sibling here (memory-core is a public repo, default token reads it).
- name: Checkout memory-core (engine dependency)
uses: actions/checkout@v7
with:
repository: agentage/memory-core
ref: master
path: memory-core
- name: Checkout code
uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: mcp-memory/package-lock.json

- name: Build engine dependency
working-directory: memory-core
run: npm ci && npm run build

- name: Install dependencies
working-directory: mcp-memory
run: npm ci

- name: Type check
working-directory: mcp-memory
run: npm run type-check

- name: Lint
working-directory: mcp-memory
run: npm run lint

- name: Format check
working-directory: mcp-memory
run: npm run format:check

- name: Test with coverage
working-directory: mcp-memory
run: npm run test:coverage

- name: Build
working-directory: mcp-memory
run: npm run build
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Publish Package

# Publishes @agentage/mcp-memory to npm. Manual (workflow_dispatch) or on a release
# Publishes @agentage/server-memory to npm. Manual (workflow_dispatch) or on a release
# commit to master; never on an ordinary push. Requires an NPM_TOKEN repo secret.
# (The first publish was bootstrapped manually; --provenance is safe now the pkg exists.)

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# @agentage/mcp-memory
# @agentage/server-memory

The **MCP server** for agentage Memory: exposes your local vaults
(`~/.agentage/vaults.json`, read through [`@agentage/memory-core`](https://github.com/agentage/memory-core))
Expand All @@ -14,18 +14,18 @@ All memory logic (backends, git, search, routing) lives in `@agentage/memory-cor
```bash
# one-time, offline: scaffold ~/.agentage + a starter vault
# (memory-core's `init`, also surfaced by the agentage CLI)
npx @agentage/mcp-memory # serves ~/.agentage/vaults.json over stdio
npx @agentage/server-memory # serves ~/.agentage/vaults.json over stdio
```

Point any stdio MCP client (Windsurf, Zed, Claude Desktop) at `npx @agentage/mcp-memory`.
Point any stdio MCP client (Windsurf, Zed, Claude Desktop) at `npx @agentage/server-memory`.

## Reused by the CLI daemon

The server builder is transport-agnostic, so the agentage CLI reuses the exact same
pieces and only swaps the transport:

```ts
import { createMemoryServer, loadLocalServer } from '@agentage/mcp-memory';
import { createMemoryServer, loadLocalServer } from '@agentage/server-memory';

// stdio bin: await (await loadLocalServer()).connect(new StdioServerTransport());
// CLI daemon: const server = createMemoryServer(registry, { scope: 'local' });
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{
"name": "@agentage/mcp-memory",
"name": "@agentage/server-memory",
"version": "0.0.1",
"description": "The MCP server for agentage Memory: exposes @agentage/memory-core vaults as the frozen 6 memory__* tools over stdio. The open, cross-vendor counterpart to @modelcontextprotocol/server-memory.",
"description": "The agentage Memory MCP server: exposes your local vaults as the frozen 6 memory__* tools over stdio. The open, cross-vendor counterpart to @modelcontextprotocol/server-memory.",
"type": "module",
"license": "UNLICENSED",
"repository": {
"type": "git",
"url": "git+https://github.com/agentage/server-memory.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
Expand All @@ -13,7 +17,7 @@
"access": "public"
},
"bin": {
"agentage-mcp-memory": "dist/bin/mcp-memory.js"
"agentage-server-memory": "dist/bin/server-memory.js"
},
"files": [
"dist"
Expand All @@ -37,7 +41,7 @@
"prepublishOnly": "npm run verify"
},
"dependencies": {
"@agentage/memory-core": "^0.0.1",
"@agentage/memory-core": "^0.1.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.4.3"
},
Expand Down
12 changes: 6 additions & 6 deletions src/bin/mcp-memory.ts → src/bin/server-memory.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/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.
// @agentage/server-memory - the stdio MCP keystone. `npx @agentage/server-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.

Expand All @@ -18,6 +18,6 @@ const main = async (): Promise<void> => {

main().catch((err: unknown) => {
const message = err instanceof Error ? err.message : String(err);
process.stderr.write(`[mcp-memory] fatal: ${message}\n`);
process.stderr.write(`[server-memory] fatal: ${message}\n`);
process.exit(1);
});
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// @agentage/mcp-memory public API - the MCP layer over @agentage/memory-core.
// @agentage/server-memory public API - the MCP layer over @agentage/memory-core.
export { MEMORY_TOOLS, type MemoryToolDef } from './server/memory-tools.schema.js';
export {
createMemoryServer,
Expand Down
7 changes: 5 additions & 2 deletions src/server/create-memory-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export const SERVER_VERSION = '0.0.1';
export interface CreateServerOptions {
scope: McpScope; // which vaults to surface: only those whose mcp includes this scope
version?: string;
readOnly?: boolean; // expose only search/read/list (hide write/edit/delete)
only?: string; // surface just this one vault id
}

const instructionsFor = (vaultIds: string[]): string => {
Expand All @@ -27,7 +29,8 @@ const instructionsFor = (vaultIds: string[]): string => {
// vaults (those whose mcp scope includes opts.scope). Transport-agnostic: connect
// it to a stdio or Streamable-HTTP transport. The vault router federates per-call.
export const createMemoryServer = (reg: VaultRegistry, opts: CreateServerOptions): McpServer => {
const surfaced = reg.surfaced(opts.scope);
const scoped = reg.surfaced(opts.scope);
const surfaced = opts.only ? scoped.filter((h) => h.id === opts.only) : scoped;
const fallbackDefault = reg.default();
const defaultHandle = surfaced.find((h) => h.id === fallbackDefault?.id) ?? surfaced[0];
const router = createRouter(surfaced, defaultHandle);
Expand All @@ -40,6 +43,6 @@ export const createMemoryServer = (reg: VaultRegistry, opts: CreateServerOptions
},
{ capabilities: { tools: {} }, instructions: instructionsFor(surfaced.map((h) => h.id)) }
);
registerTools(server, router);
registerTools(server, router, { readOnly: opts.readOnly });
return server;
};
39 changes: 32 additions & 7 deletions src/server/local-server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { createRegistry, loadConfig } from '@agentage/memory-core';
import {
createRegistry,
loadConfig,
type McpScope,
type VaultsConfig,
} from '@agentage/memory-core';
import { createMemoryServer } from './create-memory-server.js';

// The whole local stack in one call: read vaults.json -> registry (memory-core) ->
// a server exposing the local-scoped vaults via the 6 tools. The transport (stdio,
// HTTP) is the caller's choice. Used by the stdio bin and the daemon.
export const loadLocalServer = async (opts: { configDir?: string } = {}): Promise<McpServer> => {
const config = await loadConfig({ configDir: opts.configDir });
export interface LocalServerOptions {
configDir?: string;
}

const truthy = (v: string | undefined): boolean => v === '1' || v === 'true' || v === 'yes';

// Resolve config from the environment first (so `npx` works with no file):
// AGENTAGE_VAULTS_DIR -> autodiscover every subfolder of that dir as a vault;
// otherwise -> read ~/.agentage/vaults.json (or the zero-config default).
const resolveConfig = async (configDir?: string): Promise<VaultsConfig> => {
const vaultsDir = process.env.AGENTAGE_VAULTS_DIR;
if (vaultsDir) return { version: 1, vaultsDir, autodiscover: true, autoInit: true };
return loadConfig({ configDir });
};

// The whole local stack in one call: resolve config -> registry (memory-core) -> an
// MCP server over the surfaced vaults. Honors env knobs: AGENTAGE_VAULTS_DIR,
// AGENTAGE_VAULT (one vault), AGENTAGE_SCOPE (local|remote), AGENTAGE_READONLY.
export const loadLocalServer = async (opts: LocalServerOptions = {}): Promise<McpServer> => {
const config = await resolveConfig(opts.configDir);
const registry = await createRegistry(config);
return createMemoryServer(registry, { scope: 'local' });
const scope = (process.env.AGENTAGE_SCOPE as McpScope) || 'local';
return createMemoryServer(registry, {
scope,
readOnly: truthy(process.env.AGENTAGE_READONLY),
only: process.env.AGENTAGE_VAULT || undefined,
});
};
13 changes: 12 additions & 1 deletion src/server/register-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,18 @@ const guard = async (fn: () => Promise<CallToolResult>): Promise<CallToolResult>
}
};

export interface RegisterOptions {
readOnly?: boolean; // skip the mutating tools (write/edit/delete)
}

// Register the frozen 6-tool surface onto the federated router. write/edit return
// {path,updated} (the commit SHA is internal, stripped); errors surface as isError.
export const registerTools = (server: McpServer, router: Router): void => {
// With readOnly, only search/read/list are registered.
export const registerTools = (
server: McpServer,
router: Router,
opts: RegisterOptions = {}
): void => {
server.registerTool('memory__search', toolConfig('memory__search'), (args) =>
guard(async () => {
const query = args as unknown as SearchQuery;
Expand All @@ -81,6 +90,8 @@ export const registerTools = (server: McpServer, router: Router): void => {
})
);

if (opts.readOnly) return;

server.registerTool('memory__write', toolConfig('memory__write'), (args) =>
guard(async () => {
const input = args as unknown as WriteInput;
Expand Down
11 changes: 4 additions & 7 deletions test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ 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.
// then `npx @agentage/server-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 bin = join(repoRoot, 'dist/bin/server-memory.js');
const tmps: string[] = [];
const mk = (p: string) => {
const d = mkdtempSync(join(tmpdir(), p));
Expand All @@ -22,11 +22,8 @@ const mk = (p: string) => {

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',
});
// @agentage/memory-core resolves from npm (a normal dependency); just build this
// package so the spawned bin exists.
execFileSync('npm', ['run', 'build'], { cwd: repoRoot, stdio: 'ignore' });
}, 60_000);

Expand Down