@trylucy/agent-memory 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +205 -2
  3. package/contracts/agent-manifest.json +27 -0
  4. package/contracts/agent-memory-api.contract.json +199 -0
  5. package/contracts/hashes.json +23 -0
  6. package/package.json +84 -4
  7. package/schemas/canonical-recall/response.schema.json +56 -0
  8. package/schemas/v1/common.schema.json +190 -0
  9. package/schemas/v1/contract-document.schema.json +277 -0
  10. package/schemas/v1/create-proposal-request.schema.json +42 -0
  11. package/schemas/v1/error.schema.json +90 -0
  12. package/schemas/v1/memory-version-state.schema.json +39 -0
  13. package/schemas/v1/memory.schema.json +47 -0
  14. package/schemas/v1/proposal-status.schema.json +277 -0
  15. package/schemas/v1/public-surface.schema.json +26 -0
  16. package/schemas/v1/recall-request.schema.json +45 -0
  17. package/schemas/v1/recall-response.schema.json +60 -0
  18. package/schemas/v1/receipt.schema.json +67 -0
  19. package/schemas/v1/replace-proposal-request.schema.json +46 -0
  20. package/schemas/v1/replace-transition.schema.json +19 -0
  21. package/schemas/v1/scope.schema.json +10 -0
  22. package/scripts/lib/am01.mjs +159 -0
  23. package/src/client/canonical-validation.mjs +37 -0
  24. package/src/client/client.mjs +151 -0
  25. package/src/client/errors.mjs +101 -0
  26. package/src/client/http-transport.d.ts +12 -0
  27. package/src/client/http-transport.mjs +212 -0
  28. package/src/client/index.d.ts +243 -0
  29. package/src/client/index.mjs +38 -0
  30. package/src/client/serialization.mjs +53 -0
  31. package/src/client/validation.mjs +174 -0
  32. package/src/clients/claude-desktop.json +13 -0
  33. package/src/clients/claude.json +13 -0
  34. package/src/clients/codex.toml +8 -0
  35. package/src/clients/cursor.json +12 -0
  36. package/src/clients/generic.mcp.json +12 -0
  37. package/src/clients/openclaw.json +14 -0
  38. package/src/clients/vscode.json +13 -0
  39. package/src/integrations/hermes-advisory-lock.mjs +140 -0
  40. package/src/integrations/hermes-artifact.mjs +551 -0
  41. package/src/integrations/hermes-filesystem.mjs +112 -0
  42. package/src/integrations/hermes.d.ts +106 -0
  43. package/src/integrations/hermes.mjs +425 -0
  44. package/src/mcp/bin.mjs +4 -0
  45. package/src/mcp/config.mjs +60 -0
  46. package/src/mcp/index.d.ts +27 -0
  47. package/src/mcp/lifecycle.mjs +50 -0
  48. package/src/mcp/schemas.mjs +98 -0
  49. package/src/mcp/server.mjs +163 -0
  50. package/src/mcp/stdio.d.ts +28 -0
  51. package/src/mcp/stdio.mjs +59 -0
  52. package/src/onboarding/usage.mjs +44 -0
  53. package/src/projection/memory-md.mjs +319 -0
  54. package/src/provider/index.d.ts +9 -0
  55. package/src/provider/index.mjs +1 -0
  56. package/src/provider/memory-first.mjs +95 -0
  57. package/src/routing/destination.mjs +42 -0
  58. package/src/safety/secrets.mjs +31 -0
@@ -0,0 +1,60 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { createLucyHttpTransport } from '../client/http-transport.mjs';
4
+ import { McpLifecycleError, runLifecycleStage } from './lifecycle.mjs';
5
+
6
+ const PUBLIC_PERMISSIONS = new Set(['memory:read', 'memory:propose']);
7
+ const configurationError = () => new McpLifecycleError('CONFIGURATION_ERROR');
8
+
9
+ export function loadLucyMemoryMcpConfig(environment = process.env) {
10
+ const token = environment.LUCY_MEMORY_TOKEN;
11
+ if (typeof token !== 'string' || token.length === 0 || /[\r\n\0]/.test(token)) throw configurationError();
12
+ const permissions = String(environment.LUCY_MEMORY_PERMISSIONS ?? '')
13
+ .split(',')
14
+ .map((value) => value.trim())
15
+ .filter(Boolean);
16
+ if (permissions.length === 0 || new Set(permissions).size !== permissions.length
17
+ || permissions.some((permission) => !PUBLIC_PERMISSIONS.has(permission))) throw configurationError();
18
+ const timeoutMs = Number(environment.LUCY_MEMORY_TIMEOUT_MS ?? 10_000);
19
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000) throw configurationError();
20
+ const insecureLoopbackSetting = environment.LUCY_MEMORY_ALLOW_INSECURE_LOOPBACK;
21
+ if (insecureLoopbackSetting !== undefined && insecureLoopbackSetting !== 'true' && insecureLoopbackSetting !== 'false') throw configurationError();
22
+ const allowInsecureLoopback = insecureLoopbackSetting === 'true';
23
+ const baseUrl = environment.LUCY_MEMORY_BASE_URL;
24
+ const transportModule = environment.LUCY_MEMORY_TRANSPORT_MODULE;
25
+ const hasBase = typeof baseUrl === 'string' && baseUrl.length > 0;
26
+ const hasModule = typeof transportModule === 'string' && transportModule.length > 0;
27
+ if (hasBase === hasModule) throw configurationError();
28
+ if (hasBase && /[\r\n\0]/.test(baseUrl)) throw configurationError();
29
+ if (hasModule && (/[\r\n\0]/.test(transportModule) || (!transportModule.startsWith('file:') && !isAbsolute(transportModule)))) {
30
+ throw configurationError();
31
+ }
32
+ return Object.freeze({
33
+ token,
34
+ permissions: Object.freeze(permissions),
35
+ timeoutMs,
36
+ allowInsecureLoopback,
37
+ ...(hasBase ? { baseUrl } : { transportModule }),
38
+ });
39
+ }
40
+
41
+ export async function loadAgentMemoryTransport(moduleSpecifier) {
42
+ return runLifecycleStage('TRANSPORT_LOAD_ERROR', async () => {
43
+ const url = moduleSpecifier.startsWith('file:') ? new URL(moduleSpecifier) : pathToFileURL(moduleSpecifier);
44
+ if (url.protocol !== 'file:') throw new Error('unsupported protocol');
45
+ const loaded = await import(url.href);
46
+ const factory = loaded.createTransport ?? loaded.default;
47
+ if (typeof factory !== 'function') throw new Error('missing transport factory');
48
+ const transport = await factory();
49
+ if (typeof transport !== 'function') throw new Error('invalid transport factory result');
50
+ return transport;
51
+ });
52
+ }
53
+
54
+ export async function resolveLucyMemoryTransport(config) {
55
+ if (config.baseUrl) return createLucyHttpTransport({
56
+ baseUrl: config.baseUrl,
57
+ allowInsecureLoopback: config.allowInsecureLoopback,
58
+ });
59
+ return loadAgentMemoryTransport(config.transportModule);
60
+ }
@@ -0,0 +1,27 @@
1
+ import type { Server } from '@modelcontextprotocol/server';
2
+ import type { AgentMemoryClient } from '../client/index.js';
3
+
4
+ export type LucyMemoryToolName = 'memory_recall' | 'memory_propose' | 'memory_proposal_status';
5
+ export interface McpSafeError {
6
+ code: string;
7
+ message: string;
8
+ retryable: boolean;
9
+ status: number | null;
10
+ request_id?: `req_${string}`;
11
+ }
12
+ export interface LucyMemoryToolDefinition {
13
+ readonly name: LucyMemoryToolName;
14
+ readonly description: string;
15
+ readonly annotations: Readonly<Record<string, boolean>>;
16
+ readonly inputSchema: Readonly<Record<string, unknown>>;
17
+ readonly outputSchema: Readonly<Record<string, unknown>>;
18
+ }
19
+ export interface LucyMemoryMcpServerOptions {
20
+ client: AgentMemoryClient;
21
+ name?: string;
22
+ version?: string;
23
+ }
24
+ export const LUCY_MEMORY_TOOL_NAMES: readonly LucyMemoryToolName[];
25
+ export const LUCY_MEMORY_TOOLS: readonly LucyMemoryToolDefinition[];
26
+ export function safeMcpError(error: unknown, operation?: LucyMemoryToolName): McpSafeError;
27
+ export function createLucyMemoryMcpServer(options: LucyMemoryMcpServerOptions): Promise<Server>;
@@ -0,0 +1,50 @@
1
+ const SAFE_MESSAGES = Object.freeze({
2
+ CONFIGURATION_ERROR: 'Lucy Memory MCP configuration is invalid.',
3
+ TRANSPORT_LOAD_ERROR: 'Lucy Memory MCP transport could not be loaded.',
4
+ STARTUP_ERROR: 'Lucy Memory MCP could not start.',
5
+ SHUTDOWN_ERROR: 'Lucy Memory MCP could not shut down cleanly.',
6
+ });
7
+
8
+ export class McpLifecycleError extends Error {
9
+ constructor(code) {
10
+ super(SAFE_MESSAGES[code] ?? SAFE_MESSAGES.STARTUP_ERROR);
11
+ this.name = 'McpLifecycleError';
12
+ this.code = Object.hasOwn(SAFE_MESSAGES, code) ? code : 'STARTUP_ERROR';
13
+ }
14
+ }
15
+
16
+ export function safeLifecycleDiagnostic(error, fallback = 'STARTUP_ERROR') {
17
+ const fallbackCode = Object.hasOwn(SAFE_MESSAGES, fallback) ? fallback : 'STARTUP_ERROR';
18
+ const code = error instanceof McpLifecycleError && Object.hasOwn(SAFE_MESSAGES, error.code) ? error.code : fallbackCode;
19
+ return `${code}: ${SAFE_MESSAGES[code]}\n`;
20
+ }
21
+
22
+ export async function runLifecycleStage(code, action) {
23
+ try {
24
+ return await action();
25
+ } catch {
26
+ throw new McpLifecycleError(code);
27
+ }
28
+ }
29
+
30
+ export function createShutdownController({ close, writeDiagnostic, setExitCode }) {
31
+ if (typeof close !== 'function' || typeof writeDiagnostic !== 'function' || typeof setExitCode !== 'function') {
32
+ throw new TypeError('Shutdown controller dependencies must be functions.');
33
+ }
34
+ let pending;
35
+ return function shutdown() {
36
+ if (pending) return pending;
37
+ pending = (async () => {
38
+ try {
39
+ await close();
40
+ setExitCode(0);
41
+ return true;
42
+ } catch {
43
+ writeDiagnostic(safeLifecycleDiagnostic(new McpLifecycleError('SHUTDOWN_ERROR'), 'SHUTDOWN_ERROR'));
44
+ setExitCode(1);
45
+ return false;
46
+ }
47
+ })();
48
+ return pending;
49
+ };
50
+ }
@@ -0,0 +1,98 @@
1
+ import { readFileSync, readdirSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+
4
+ const BASE = 'https://schemas.trylucy.ai/agent-memory/v1/';
5
+ const SCHEMA_DIRECTORY = fileURLToPath(new URL('../../schemas/v1/', import.meta.url));
6
+ const CANONICAL_RECALL_OUTPUT = JSON.parse(readFileSync(new URL('../../schemas/canonical-recall/response.schema.json', import.meta.url), 'utf8'));
7
+ const documents = new Map();
8
+ for (const name of readdirSync(SCHEMA_DIRECTORY).filter((value) => value.endsWith('.schema.json')).sort()) {
9
+ const document = JSON.parse(readFileSync(new URL(`../../schemas/v1/${name}`, import.meta.url), 'utf8'));
10
+ if (document.$id !== `${BASE}${name}`) throw new Error(`Frozen schema identity mismatch: ${name}`);
11
+ documents.set(name.replace('.schema.json', ''), document);
12
+ }
13
+
14
+ function referenceTarget(reference, currentName) {
15
+ if (reference.startsWith('#')) return currentName;
16
+ if (!reference.startsWith(BASE)) throw new Error(`External frozen schema reference is not allowed: ${reference}`);
17
+ const relative = reference.slice(BASE.length);
18
+ const hash = relative.indexOf('#');
19
+ return (hash < 0 ? relative : relative.slice(0, hash)).replace('.schema.json', '');
20
+ }
21
+ function rewriteReference(reference, currentName) {
22
+ if (reference.startsWith('#')) return `#/$defs/${currentName}${reference.slice(1)}`;
23
+ const target = referenceTarget(reference, currentName);
24
+ if (!documents.has(target)) throw new Error(`Frozen schema reference not found: ${reference}`);
25
+ const hash = reference.indexOf('#');
26
+ return `#/$defs/${target}${hash < 0 ? '' : reference.slice(hash + 1)}`;
27
+ }
28
+ function transform(value, currentName) {
29
+ if (Array.isArray(value)) return value.map((item) => transform(item, currentName));
30
+ if (!value || typeof value !== 'object') return value;
31
+ const output = {};
32
+ for (const [key, child] of Object.entries(value)) {
33
+ if (key === '$id' || key === '$schema') continue;
34
+ output[key] = key === '$ref' ? rewriteReference(child, currentName) : transform(child, currentName);
35
+ }
36
+ return output;
37
+ }
38
+ function collectReferences(value, currentName, found) {
39
+ if (Array.isArray(value)) { for (const item of value) collectReferences(item, currentName, found); return; }
40
+ if (!value || typeof value !== 'object') return;
41
+ for (const [key, child] of Object.entries(value)) {
42
+ if (key === '$ref') {
43
+ const target = referenceTarget(child, currentName);
44
+ if (!found.has(target)) {
45
+ found.add(target);
46
+ collectReferences(documents.get(target), target, found);
47
+ }
48
+ } else collectReferences(child, currentName, found);
49
+ }
50
+ }
51
+ function definitionsFor(rootNames) {
52
+ const names = new Set(rootNames);
53
+ for (const name of rootNames) collectReferences(documents.get(name), name, names);
54
+ return Object.fromEntries([...names].sort().map((name) => [name, transform(documents.get(name), name)]));
55
+ }
56
+ function bundled(name) {
57
+ const definitions = definitionsFor([name]);
58
+ return { ...structuredClone(definitions[name]), $defs: definitions };
59
+ }
60
+ function deepFreeze(value) {
61
+ if (value && typeof value === 'object' && !Object.isFrozen(value)) {
62
+ Object.freeze(value);
63
+ for (const child of Object.values(value)) deepFreeze(child);
64
+ }
65
+ return value;
66
+ }
67
+
68
+ const proposalDefinitions = definitionsFor(['create-proposal-request', 'replace-proposal-request']);
69
+ const proposalBranch = (name) => {
70
+ const schema = structuredClone(proposalDefinitions[name]);
71
+ delete schema.$defs;
72
+ return schema;
73
+ };
74
+ const statusDefinitions = definitionsFor(['common']);
75
+ export const MCP_SCHEMAS = deepFreeze({
76
+ recallInput: bundled('recall-request'),
77
+ proposalInput: {
78
+ type: 'object',
79
+ oneOf: [proposalBranch('create-proposal-request'), proposalBranch('replace-proposal-request')],
80
+ $defs: proposalDefinitions,
81
+ },
82
+ proposalStatusInput: {
83
+ type: 'object',
84
+ additionalProperties: false,
85
+ required: ['proposal_id'],
86
+ properties: { proposal_id: { $ref: '#/$defs/common/$defs/proposalId' } },
87
+ $defs: statusDefinitions,
88
+ },
89
+ recallOutput: bundled('recall-response'),
90
+ canonicalRecallOutput: CANONICAL_RECALL_OUTPUT,
91
+ proposalOutput: bundled('proposal-status'),
92
+ });
93
+
94
+ export function containsExternalSchemaReference(value) {
95
+ if (Array.isArray(value)) return value.some(containsExternalSchemaReference);
96
+ if (!value || typeof value !== 'object') return false;
97
+ return Object.entries(value).some(([key, child]) => (key === '$ref' && !String(child).startsWith('#')) || containsExternalSchemaReference(child));
98
+ }
@@ -0,0 +1,163 @@
1
+ import { ProtocolError, ProtocolErrorCode, Server } from '@modelcontextprotocol/server';
2
+ import { types as utilTypes } from 'node:util';
3
+ import { AgentMemoryClient, mappedErrorRequestId, safePublicErrorRequestId } from '../client/client.mjs';
4
+ import {
5
+ AgentMemoryError,
6
+ AuthorizationFailureError,
7
+ ValidationFailureError,
8
+ } from '../client/errors.mjs';
9
+ import { stableSerialize } from '../client/serialization.mjs';
10
+ import { MCP_SCHEMAS } from './schemas.mjs';
11
+ import { usageSkillText } from '../onboarding/usage.mjs';
12
+
13
+ export const LUCY_MEMORY_TOOL_NAMES = Object.freeze([
14
+ 'memory_recall',
15
+ 'memory_propose',
16
+ 'memory_proposal_status',
17
+ ]);
18
+
19
+ const SAFE_ERROR_MESSAGES = Object.freeze({
20
+ authentication_failure: 'Lucy authentication failed.',
21
+ authorization_failure: 'Lucy authorization failed.',
22
+ invalid_scope: 'The memory scope is invalid.',
23
+ invalid_key: 'The memory key is invalid.',
24
+ invalid_kind: 'The memory kind is invalid.',
25
+ validation_failure: 'The memory request is invalid.',
26
+ proposal_not_found: 'The proposal was not found or is not visible.',
27
+ memory_not_found: 'The memory was not found or is not visible.',
28
+ version_conflict: 'The expected memory version does not match.',
29
+ lifecycle_conflict: 'The memory lifecycle does not permit this operation.',
30
+ rate_limit: 'Lucy rate limited the request.',
31
+ upstream_unavailable: 'Lucy is temporarily unavailable.',
32
+ timeout: 'The Lucy request timed out.',
33
+ malformed_response: 'Lucy returned a malformed response.',
34
+ proposal_contract_incompatible: 'Lucy returned a proposal response incompatible with the frozen AM-01 contract. Ask the Lucy service operator to restore AM-01 compatibility, then retry.',
35
+ internal_lucy_error: 'Lucy returned an internal error.',
36
+ insufficient_credits: 'The workspace does not have enough Lucy credits for this Agent Memory operation.',
37
+ billing_unavailable: 'Lucy billing is temporarily unavailable.',
38
+ });
39
+ const SAFE_ERROR_CODES = new Set(Object.keys(SAFE_ERROR_MESSAGES));
40
+ const RETRYABLE_ERROR_CODES = new Set(['rate_limit', 'upstream_unavailable', 'timeout', 'billing_unavailable']);
41
+ const INTERNAL_MCP_ERROR = Object.freeze({
42
+ code: 'internal_mcp_error',
43
+ message: 'Lucy Memory MCP could not complete the request.',
44
+ retryable: false,
45
+ status: null,
46
+ });
47
+ const CANONICAL_CLIENT_OPERATIONS = Object.freeze({
48
+ memory_recall: AgentMemoryClient.prototype.recallCanonical,
49
+ memory_propose: AgentMemoryClient.prototype.propose,
50
+ memory_proposal_status: AgentMemoryClient.prototype.proposalStatus,
51
+ });
52
+ const AUTHORIZATION_FAILURE_PROTOTYPE = AuthorizationFailureError.prototype;
53
+ const VALIDATION_FAILURE_PROTOTYPE = ValidationFailureError.prototype;
54
+ const AGENT_MEMORY_CLIENT_PROTOTYPE = AgentMemoryClient.prototype;
55
+ const BOUNDARY_TYPE_ERROR = TypeError;
56
+ const GET_PROTOTYPE_OF = Object.getPrototypeOf;
57
+ const IS_FROZEN = Object.isFrozen;
58
+ const IS_PROXY = utilTypes.isProxy;
59
+ const REFLECT_APPLY = Reflect.apply;
60
+
61
+ export const LUCY_MEMORY_TOOLS = Object.freeze([
62
+ Object.freeze({
63
+ name: 'memory_recall',
64
+ description: 'Recall active approved Lucy memory only. Use for durable preferences, identity, project decisions, or explicit remember/recall. Do not use for arithmetic, weather, news, or one-shot drafting.',
65
+ annotations: Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }),
66
+ inputSchema: MCP_SCHEMAS.recallInput,
67
+ outputSchema: MCP_SCHEMAS.canonicalRecallOutput,
68
+ }),
69
+ Object.freeze({
70
+ name: 'memory_propose',
71
+ description: 'Propose a durable Lucy memory write. Success means pending, not saved. Do not send secrets. Humans approve on Lucy unless they enabled auto memory.',
72
+ annotations: Object.freeze({ readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true }),
73
+ inputSchema: MCP_SCHEMAS.proposalInput,
74
+ outputSchema: MCP_SCHEMAS.proposalOutput,
75
+ }),
76
+ Object.freeze({
77
+ name: 'memory_proposal_status',
78
+ description: 'Read whether a Lucy proposal is pending, approved, rejected, expired, or superseded. Pending memory is not recallable.',
79
+ annotations: Object.freeze({ readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }),
80
+ inputSchema: MCP_SCHEMAS.proposalStatusInput,
81
+ outputSchema: MCP_SCHEMAS.proposalOutput,
82
+ }),
83
+ ]);
84
+
85
+ function ownDataValue(value, key) {
86
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
87
+ return descriptor && Object.hasOwn(descriptor, 'value') ? descriptor.value : undefined;
88
+ }
89
+
90
+ export function safeMcpError(error, operation) {
91
+ try {
92
+ if (!(error instanceof AgentMemoryError)) return { ...INTERNAL_MCP_ERROR };
93
+ const code = ownDataValue(error, 'code');
94
+ if (!SAFE_ERROR_CODES.has(code)) return { ...INTERNAL_MCP_ERROR };
95
+ const candidateStatus = ownDataValue(error, 'status');
96
+ const status = Number.isInteger(candidateStatus) && candidateStatus >= 100 && candidateStatus <= 599
97
+ ? candidateStatus
98
+ : null;
99
+ const requestId = safePublicErrorRequestId(mappedErrorRequestId(error));
100
+ return {
101
+ code,
102
+ message: SAFE_ERROR_MESSAGES[code],
103
+ retryable: operation === 'memory_propose' && RETRYABLE_ERROR_CODES.has(code),
104
+ status,
105
+ ...(requestId === null ? {} : { request_id: requestId }),
106
+ };
107
+ } catch {
108
+ return { ...INTERNAL_MCP_ERROR };
109
+ }
110
+ }
111
+
112
+ async function requireClient(client) {
113
+ try {
114
+ if (IS_PROXY(client)
115
+ || GET_PROTOTYPE_OF(client) !== AGENT_MEMORY_CLIENT_PROTOTYPE
116
+ || !IS_FROZEN(client)) {
117
+ throw new BOUNDARY_TYPE_ERROR('client must be an exact non-proxy AgentMemoryClient.');
118
+ }
119
+ await REFLECT_APPLY(CANONICAL_CLIENT_OPERATIONS.memory_recall, client, ['{']);
120
+ throw new BOUNDARY_TYPE_ERROR('client must be an exact non-proxy AgentMemoryClient.');
121
+ } catch (error) {
122
+ const errorPrototype = IS_PROXY(error) ? null : GET_PROTOTYPE_OF(error);
123
+ if (errorPrototype === AUTHORIZATION_FAILURE_PROTOTYPE
124
+ || errorPrototype === VALIDATION_FAILURE_PROTOTYPE) return client;
125
+ throw new BOUNDARY_TYPE_ERROR('client must be an exact non-proxy AgentMemoryClient.');
126
+ }
127
+ }
128
+
129
+ export async function createLucyMemoryMcpServer({ client, name = 'lucy-memory-mcp', version = '0.1.2' } = {}) {
130
+ const memoryClient = await requireClient(client);
131
+ const server = new Server(
132
+ { name, version },
133
+ { capabilities: { tools: {} }, instructions: usageSkillText() },
134
+ );
135
+
136
+ server.setRequestHandler('tools/list', async () => ({ tools: LUCY_MEMORY_TOOLS }));
137
+ server.setRequestHandler('tools/call', async ({ params }) => {
138
+ const argumentsJson = stableSerialize(params.arguments ?? {});
139
+ try {
140
+ const operation = CANONICAL_CLIENT_OPERATIONS[params.name];
141
+ if (operation === undefined) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Unknown tool: ${params.name}`);
142
+ const result = await REFLECT_APPLY(operation, memoryClient, [argumentsJson]);
143
+ // Keep billing correlation in tool text for acceptance/agents. Strip it from
144
+ // structuredContent so frozen AM-01 outputSchema (additionalProperties:false) still validates.
145
+ const { operation_id: _billingOperationId, ...am01Structured } = result && typeof result === 'object' && !Array.isArray(result)
146
+ ? result
147
+ : { ...(result ?? {}) };
148
+ return {
149
+ content: [{ type: 'text', text: stableSerialize(result) }],
150
+ structuredContent: am01Structured,
151
+ };
152
+ } catch (error) {
153
+ if (error instanceof ProtocolError) throw error;
154
+ const payload = safeMcpError(error, params.name);
155
+ return {
156
+ isError: true,
157
+ content: [{ type: 'text', text: stableSerialize(payload) }],
158
+ };
159
+ }
160
+ });
161
+
162
+ return server;
163
+ }
@@ -0,0 +1,28 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ import type { AgentMemoryClient } from '../client/index.js';
3
+
4
+ export interface LucyMemoryStdioRuntime {
5
+ readonly close: () => Promise<void>;
6
+ }
7
+ export interface LucyMemoryStdioOptions {
8
+ client: AgentMemoryClient;
9
+ stdin?: Readable;
10
+ stdout?: Writable;
11
+ maxBufferSize?: number;
12
+ }
13
+ export function modulePathFromUrl(url: URL | string): string;
14
+ export function startLucyMemoryMcpStdio(options: LucyMemoryStdioOptions): Promise<LucyMemoryStdioRuntime>;
15
+ export function runLucyMemoryMcpStdio(options?: {
16
+ environment?: Record<string, string | undefined>;
17
+ stdin?: Readable;
18
+ stdout?: Writable;
19
+ }): Promise<LucyMemoryStdioRuntime>;
20
+ export function main(options?: {
21
+ environment?: Record<string, string | undefined>;
22
+ stdin?: Readable;
23
+ stdout?: Writable;
24
+ stderr?: Writable;
25
+ exit?: (code: number) => void;
26
+ signals?: { once(event: 'SIGINT' | 'SIGTERM', listener: () => void): unknown };
27
+ runtimeFactory?: typeof runLucyMemoryMcpStdio;
28
+ }): Promise<(LucyMemoryStdioRuntime & { readonly shutdown: () => Promise<boolean> }) | null>;
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport, serveStdio } from '@modelcontextprotocol/server/stdio';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { AgentMemoryClient } from '../client/client.mjs';
5
+ import { loadLucyMemoryMcpConfig, resolveLucyMemoryTransport } from './config.mjs';
6
+ import { createShutdownController, runLifecycleStage, safeLifecycleDiagnostic } from './lifecycle.mjs';
7
+ import { createLucyMemoryMcpServer } from './server.mjs';
8
+
9
+ export function modulePathFromUrl(url) {
10
+ return fileURLToPath(url);
11
+ }
12
+
13
+ export async function startLucyMemoryMcpStdio({ client, stdin = process.stdin, stdout = process.stdout, maxBufferSize = 1_048_576 } = {}) {
14
+ const transport = new StdioServerTransport(stdin, stdout, { maxBufferSize });
15
+ const handle = serveStdio(
16
+ () => createLucyMemoryMcpServer({ client }),
17
+ { transport, legacy: 'serve' },
18
+ );
19
+ return Object.freeze({ close: async () => handle.close() });
20
+ }
21
+
22
+ export async function runLucyMemoryMcpStdio({ environment = process.env, stdin = process.stdin, stdout = process.stdout } = {}) {
23
+ const config = await runLifecycleStage('CONFIGURATION_ERROR', () => loadLucyMemoryMcpConfig(environment));
24
+ const transport = await runLifecycleStage('TRANSPORT_LOAD_ERROR', () => resolveLucyMemoryTransport(config));
25
+ const client = new AgentMemoryClient({
26
+ transport,
27
+ token: config.token,
28
+ permissions: config.permissions,
29
+ timeoutMs: config.timeoutMs,
30
+ });
31
+ return runLifecycleStage('STARTUP_ERROR', () => startLucyMemoryMcpStdio({ client, stdin, stdout }));
32
+ }
33
+
34
+ export async function main({ environment = process.env, stdin = process.stdin, stdout = process.stdout, stderr = process.stderr,
35
+ exit = (code) => process.exit(code), signals = process, runtimeFactory = runLucyMemoryMcpStdio } = {}) {
36
+ let runtime;
37
+ try {
38
+ runtime = await runtimeFactory({ environment, stdin, stdout });
39
+ } catch (error) {
40
+ stderr.write(safeLifecycleDiagnostic(error));
41
+ exit(1);
42
+ return null;
43
+ }
44
+ let shutdownExitCode = 1;
45
+ const shutdown = createShutdownController({
46
+ close: runtime.close,
47
+ writeDiagnostic: (diagnostic) => stderr.write(diagnostic),
48
+ setExitCode: (code) => { shutdownExitCode = code; },
49
+ });
50
+ let signalHandled = false;
51
+ const onSignal = () => {
52
+ if (signalHandled) return;
53
+ signalHandled = true;
54
+ shutdown().finally(() => exit(shutdownExitCode));
55
+ };
56
+ signals.once('SIGINT', onSignal);
57
+ signals.once('SIGTERM', onSignal);
58
+ return Object.freeze({ ...runtime, shutdown });
59
+ }
@@ -0,0 +1,44 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const manifest = JSON.parse(readFileSync(join(dirname(fileURLToPath(import.meta.url)), '../../contracts/agent-manifest.json'), 'utf8'));
6
+
7
+ export const LUCY_USAGE_SKILL = `# Lucy governed memory
8
+
9
+ ${manifest.sentence}
10
+
11
+ Use exactly these tools:
12
+
13
+ - memory_recall: active approved Lucy memory only
14
+ - memory_propose: request a durable write; this is not a save
15
+ - memory_proposal_status: inspect pending/approved/rejected
16
+
17
+ Do not invent a fourth tool. Do not approve, deny, forget, or restore through MCP.
18
+
19
+ Recall when a durable preference, identity fact, project decision, or explicit remember/recall request matters.
20
+ Do not recall for arithmetic, weather, news, or one-shot drafting.
21
+
22
+ Search recall practically:
23
+
24
+ - Scope is an object, never a string: user \`{ "kind": "user" }\`, workspace \`{ "kind": "workspace" }\`, project \`{ "kind": "project", "id": "<project-id>" }\`.
25
+ - Minimal valid request: \`{ "query": "database", "scope": { "kind": "user" } }\`.
26
+ - Begin with concise literal, distinctive substrings rather than a long paraphrase.
27
+ - Try spelling and spacing variants.
28
+ - Use known keys, kinds, and scopes when available.
29
+ - Broaden or narrow the query and filters iteratively.
30
+ - Do not conclude that memory is absent after one empty result.
31
+ - Distinguish a successful recall with zero matches from authentication, authorization, or service errors.
32
+
33
+ Propose only durable non-secret facts. Pending is not recallable. Wait for Lucy/human approval unless the human enabled auto memory on Lucy.
34
+
35
+ Local notes are not Lucy authority. If they conflict, Lucy wins. Never dual-write.
36
+ `;
37
+
38
+ export function usageSkillText() {
39
+ return LUCY_USAGE_SKILL;
40
+ }
41
+
42
+ export function publicManifest() {
43
+ return manifest;
44
+ }