@dreamforgestudiollc/emet 0.0.0-stage → 0.2.4

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 (73) hide show
  1. package/.env.example +136 -0
  2. package/CHANGELOG.md +296 -0
  3. package/CHARTER.md +125 -0
  4. package/LICENSE +22 -0
  5. package/README.md +783 -2
  6. package/docs/AUTHORSHIP.md +26 -0
  7. package/docs/BENCHMARKS.md +261 -0
  8. package/docs/BOOTSTRAP.md +127 -0
  9. package/docs/BOTS.md +79 -0
  10. package/docs/COVERAGE.md +64 -0
  11. package/docs/DEPLOY.md +246 -0
  12. package/docs/DISCIPLINES.md +29 -0
  13. package/docs/GUARDRAILS-AND-MONORAILS.md +107 -0
  14. package/docs/GUIDED-SETUP.md +296 -0
  15. package/docs/PERMISSIONS.md +118 -0
  16. package/docs/ROADMAP.md +59 -0
  17. package/docs/SETUP.md +259 -0
  18. package/guides/NEURODIVERGENT_SUPPORT.md +278 -0
  19. package/package.json +70 -4
  20. package/server.json +39 -0
  21. package/src/aleph.js +395 -0
  22. package/src/annotations.js +135 -0
  23. package/src/approve-throttle.js +54 -0
  24. package/src/atlas-role.js +58 -0
  25. package/src/conformance.js +688 -0
  26. package/src/content_analyzer.js +450 -0
  27. package/src/database.js +811 -0
  28. package/src/disciplines.js +26 -0
  29. package/src/dispatch.js +279 -0
  30. package/src/env.js +52 -0
  31. package/src/floor.js +187 -0
  32. package/src/gaps.js +476 -0
  33. package/src/guides.js +48 -0
  34. package/src/http-remote.js +269 -0
  35. package/src/index.js +390 -0
  36. package/src/invites.js +167 -0
  37. package/src/layers.js +449 -0
  38. package/src/lessons-to-checks.js +366 -0
  39. package/src/logger.js +299 -0
  40. package/src/metadata.js +61 -0
  41. package/src/module-path.js +32 -0
  42. package/src/mongo-connect.js +48 -0
  43. package/src/named-items.js +208 -0
  44. package/src/oauth-provider.js +838 -0
  45. package/src/persons.js +116 -0
  46. package/src/provenance.js +257 -0
  47. package/src/redact.js +231 -0
  48. package/src/request-log.js +124 -0
  49. package/src/scopes.js +97 -0
  50. package/src/security-headers.js +26 -0
  51. package/src/session-graph.js +945 -0
  52. package/src/session.js +1097 -0
  53. package/src/setup.js +1313 -0
  54. package/src/startup-pages.js +354 -0
  55. package/src/startup-payload.js +126 -0
  56. package/src/tools.js +4271 -0
  57. package/src/transcript-session.js +106 -0
  58. package/src/validation.js +1163 -0
  59. package/src/version.js +2 -0
  60. package/templates/BOARD.md +72 -0
  61. package/templates/BOTHANDOFF.md +57 -0
  62. package/templates/CHARACTER.md +30 -0
  63. package/templates/HANDOFF.md +63 -0
  64. package/templates/IDENTITY.md +29 -0
  65. package/templates/INDEX.md +82 -0
  66. package/templates/INSTALL.md +29 -0
  67. package/templates/PROCEDURE.md +39 -0
  68. package/templates/ROSTER.md +43 -0
  69. package/templates/SEED.md +65 -0
  70. package/templates/STAGING.md +37 -0
  71. package/templates/STANDARD.md +99 -0
  72. package/templates/THREAD.md +43 -0
  73. package/templates/USER.md +29 -0
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * EMET remote MCP server — PRODUCTION entry (OAuth-protected).
4
+ * ------------------------------------------------------------------
5
+ * Streamable HTTP transport + full OAuth 2.1 (via the MCP SDK's mcpAuthRouter)
6
+ * guarding /mcp with bearer auth. Shares tools.js and database.js with the
7
+ * stdio entry point - one codebase, two transports.
8
+ *
9
+ * Required env for deploy:
10
+ * MONGODB_URI Atlas connection string (scoped user recommended)
11
+ * PUBLIC_URL the public https base URL of this host (issuer)
12
+ * EMET_OAUTH_PASSPHRASE the consent passphrase (set by the user)
13
+ * Optional:
14
+ * PORT (default 8849), HOST (default 0.0.0.0), EMET_SOURCE_TAG (e.g. "mobile"),
15
+ * The server trusts exactly one proxy hop (Railway). TRUST_PROXY is not read.
16
+ * EMET_CIMD (default on; set 0 to stop advertising Client ID Metadata Document
17
+ * support and fall back to Dynamic Client Registration only — a rollback that
18
+ * needs no redeploy; see oauth-provider.js)
19
+ *
20
+ * Configuration comes from the environment, or from a .env file in the repo root.
21
+ * PUBLIC_URL defaults to http://127.0.0.1:PORT for local development.
22
+ */
23
+ import './env.js';
24
+ import fs from 'fs';
25
+ import { setting } from './env.js';
26
+ import path from 'path';
27
+ import { fileURLToPath } from 'url';
28
+ import express from 'express';
29
+
30
+ import { mcpAuthRouter, createOAuthMetadata, getOAuthProtectedResourceMetadataUrl } from '@modelcontextprotocol/sdk/server/auth/router.js';
31
+ import { redirectUriMatches } from '@modelcontextprotocol/sdk/server/auth/handlers/authorize.js';
32
+ import { requireBearerAuth } from '@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js';
33
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
34
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
35
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
36
+ import { EMET_VERSION } from './version.js';
37
+
38
+
39
+ const db = await import('./database.js');
40
+ const t = await import('./tools.js');
41
+ const { dispatchTool } = await import('./dispatch.js');
42
+ const { MongoOAuthProvider, withCimdMetadata } = await import('./oauth-provider.js');
43
+ const { requestLogger } = await import('./request-log.js');
44
+ const { securityHeaders } = await import('./security-headers.js');
45
+ const { ApproveThrottle } = await import('./approve-throttle.js');
46
+ const { StructuredLogger, AuditOperation } = await import('./logger.js');
47
+
48
+ const PORT = Number(process.env.PORT) || 8849;
49
+ const HOST = process.env.HOST || '0.0.0.0';
50
+ const PUBLIC_URL = (process.env.PUBLIC_URL || `http://127.0.0.1:${PORT}`).replace(/\/+$/, '');
51
+ const PASSPHRASE = setting('OAUTH_PASSPHRASE') || '';
52
+ // One hop only. A larger TRUST_PROXY would let a client spoof req.ip.
53
+ const TRUST_PROXY = 1;
54
+ const RESOURCE_URL = new URL(PUBLIC_URL + '/mcp');
55
+ const PRM_URL = getOAuthProtectedResourceMetadataUrl(RESOURCE_URL);
56
+
57
+ // This used to be a stub whose debug/info/warn/audit were empty functions, with
58
+ // AuditOperation a Proxy returning its own key names. Every audit call on this
59
+ // transport - and this is the transport a hosted install runs - was discarded,
60
+ // so the server reported an audit trail it did not keep. Both transports now
61
+ // construct the same recorder. Output goes to stderr, which the host captures.
62
+ const logger = new StructuredLogger({
63
+ serviceName: 'emet',
64
+ version: EMET_VERSION,
65
+ minLevel: process.env.LOG_LEVEL || (process.env.DEBUG === 'true' ? 'debug' : 'info'),
66
+ jsonOutput: process.env.LOG_FORMAT !== 'text',
67
+ auditEnabled: true,
68
+ auditLogPath: setting('AUDIT_LOG') || null
69
+ });
70
+
71
+ const dbManager = new db.EmetDatabase(logger);
72
+ const rateLimiter = new t.RateLimiter(logger);
73
+
74
+ // Over HTTP the source tag is the one bound at approval (auth.extra.sourceTag).
75
+ // Stdio still resolves the caller's tag inside saveMemory. A token with no
76
+ // bound tag may name a registered tag until it is refreshed; that refresh is
77
+ // refused so the next consent binds a tag.
78
+
79
+ function buildServer() {
80
+ const server = new Server({ name: 'emet', version: EMET_VERSION }, { capabilities: { tools: {} } });
81
+ server.setRequestHandler(ListToolsRequestSchema, async (_request, extra) => {
82
+ const { scopesAllow } = await import('./scopes.js');
83
+ const auth = extra && extra.authInfo;
84
+ const scopes = (auth && auth.scopes) || [];
85
+ return { tools: t.TOOLS.filter((tool) => scopesAllow(scopes, tool.name, { http: true, authenticated: Boolean(auth), args: null })) };
86
+ });
87
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
88
+ const { name, arguments: args = {} } = request.params;
89
+ const { actorStore, personFromAuth } = await import('./persons.js');
90
+ const auth = extra && extra.authInfo;
91
+ const clientId = (auth && auth.clientId) || 'http:visitor';
92
+ const actor = {
93
+ personId: personFromAuth(extra),
94
+ clientId,
95
+ scopes: (auth && auth.scopes) || [],
96
+ transport: 'http',
97
+ sourceTag: (auth && auth.extra && auth.extra.sourceTag) || null,
98
+ authenticated: Boolean(auth)
99
+ };
100
+ const bucket = t.rateBucket(actor);
101
+ const rl = rateLimiter.checkLimit(name, bucket);
102
+ if (!rl.allowed) return actorStore.run(actor, () => t.handleError(new t.RateLimitError(rl.reason, rl.retryAfterMs), name, logger));
103
+ rateLimiter.recordRequest(name, bucket);
104
+ // Every tool call leaves one audit entry (H6). callTool never throws: it
105
+ // returns either a success response or handleError's isError response.
106
+ const t0 = Date.now();
107
+ // Parameter NAMES, never values (the owner, 2026-09-27, code audit 1.6).
108
+ const paramNames = Object.keys(args || {}).sort();
109
+ const result = await actorStore.run(actor, () => callTool(name, args));
110
+ logger.audit(AuditOperation.TOOL_CALL, { tool: name, success: result.isError !== true, durationMs: Date.now() - t0, params: paramNames });
111
+ return result;
112
+ });
113
+ return server;
114
+ }
115
+
116
+ async function callTool(name, args) {
117
+ // One dispatch table for both transports - src/dispatch.js (audit 2026-09-27).
118
+ return dispatchTool(name, args, { dbManager, logger, AuditOperation });
119
+ }
120
+
121
+ const provider = new MongoOAuthProvider(() => dbManager.db, { passphrase: PASSPHRASE, issuerUrl: PUBLIC_URL });
122
+ // Failed passphrase attempts per client address (audit finding H4, 2026-09-19).
123
+ // The tool-call limiter sits behind bearer auth; this is the one gate in front of it.
124
+ const approveThrottle = new ApproveThrottle();
125
+
126
+ const app = express();
127
+ app.set('trust proxy', TRUST_PROXY);
128
+
129
+ // On every response, including /mcp, the OAuth metadata routes, errors, and
130
+ // the consent page. The consent page still sets its own content security policy.
131
+ app.use(securityHeaders);
132
+
133
+ // One redacted line per OAuth/MCP request and per 4xx/5xx anywhere (H4,
134
+ // 2026-09-07). EMET_REQUEST_LOG=0 silences it. See request-log.js.
135
+ app.use(requestLogger({ enabled: process.env.EMET_REQUEST_LOG !== '0' }));
136
+
137
+ app.get('/health', (_req, res) => res.json({ ok: true, service: 'emet', tools: t.TOOLS.length, cimd: provider.cimdEnabled }));
138
+
139
+ // OAuth 2.1 authorization server + protected-resource metadata:
140
+ // /.well-known/oauth-authorization-server, /.well-known/oauth-protected-resource/mcp,
141
+ // /register, /authorize, /token, /revoke
142
+ const AUTH_OPTIONS = {
143
+ provider,
144
+ issuerUrl: new URL(PUBLIC_URL),
145
+ resourceServerUrl: RESOURCE_URL,
146
+ scopesSupported: ['emet.read', 'emet.write'],
147
+ resourceName: 'EMET Memory',
148
+ };
149
+
150
+ // Authorization-server metadata, served AHEAD of the SDK router so it can carry
151
+ // `client_id_metadata_document_supported` (SDK 1.30.0 builds this object
152
+ // internally with no option to extend it). Everything else in the document is
153
+ // the SDK's own; only the CIMD flag is added, and DCR stays advertised.
154
+ const AS_METADATA = withCimdMetadata(createOAuthMetadata(AUTH_OPTIONS), provider.cimdEnabled);
155
+ app.get('/.well-known/oauth-authorization-server', (_req, res) => {
156
+ res.setHeader('Cache-Control', 'no-store');
157
+ res.json(AS_METADATA);
158
+ });
159
+
160
+ app.use(mcpAuthRouter(AUTH_OPTIONS));
161
+
162
+ // Passphrase approval -> mint authorization code -> redirect back to the client
163
+ app.post('/oauth/approve', express.urlencoded({ extended: false }), async (req, res) => {
164
+ try {
165
+ const { client_id, redirect_uri, code_challenge, state, scopes, resource, passphrase, invite_code } = req.body || {};
166
+ const throttle = approveThrottle.check(req.ip);
167
+ if (!throttle.allowed) {
168
+ res.locals.reason = 'throttled';
169
+ res.status(429).setHeader('Retry-After', String(throttle.retryAfterS));
170
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
171
+ return res.end('<p style="font-family:system-ui;background:#12141a;color:#e8e6e0;padding:24px">Too many attempts. Try again later.</p>');
172
+ }
173
+ let inviteSubject = null;
174
+ let inviteScopes = null;
175
+ let inviteDoor = null;
176
+ let sourceTag = null;
177
+ if (invite_code && String(invite_code).trim()) {
178
+ await dbManager.getConnection('working');
179
+ const { redeemInvite } = await import('./invites.js');
180
+ const redeemed = await redeemInvite(dbManager.db, invite_code);
181
+ if (!redeemed.ok) {
182
+ approveThrottle.recordFailure(req.ip);
183
+ res.locals.reason = 'bad_invite';
184
+ res.status(401).setHeader('Content-Type', 'text/html; charset=utf-8');
185
+ return res.end('<p style="font-family:system-ui;background:#12141a;color:#e8e6e0;padding:24px">That invite code is not valid. Go back and try again.</p>');
186
+ }
187
+ inviteSubject = redeemed.person_id;
188
+ inviteDoor = redeemed.door === 'member' ? 'member' : 'guest';
189
+ sourceTag = redeemed.source_tag || null;
190
+ // An empty scope list is owner access; a guest must never get it
191
+ // (security audit 2026-09-29).
192
+ const { guestScopesOrNull } = await import('./scopes.js');
193
+ inviteScopes = guestScopesOrNull(redeemed.scopes);
194
+ if (!inviteScopes) {
195
+ res.locals.reason = 'invite_without_scopes';
196
+ res.status(401).setHeader('Content-Type', 'text/html; charset=utf-8');
197
+ return res.end('<p style="font-family:system-ui;background:#12141a;color:#e8e6e0;padding:24px">That invite grants no access. Ask for a new one.</p>');
198
+ }
199
+ } else if (!provider.checkPassphrase(passphrase || '')) {
200
+ approveThrottle.recordFailure(req.ip);
201
+ res.locals.reason = 'bad_passphrase';
202
+ res.status(401).setHeader('Content-Type', 'text/html; charset=utf-8');
203
+ return res.end('<p style="font-family:system-ui;background:#12141a;color:#e8e6e0;padding:24px">Incorrect passphrase. Go back and try again.</p>');
204
+ } else {
205
+ const requested = String((req.body && req.body.source) || '').trim();
206
+ const { registeredSourceTags } = await import('./setup.js');
207
+ const registry = registeredSourceTags();
208
+ if (registry.length && !registry.includes(requested)) {
209
+ res.locals.reason = 'bad_source';
210
+ return res.status(400).end('source tag is not registered');
211
+ }
212
+ sourceTag = requested || null;
213
+ }
214
+ const client = await provider.clientsStore.getClient(client_id);
215
+ if (!client) { res.locals.reason = 'unknown_client'; return res.status(400).end('unknown client'); }
216
+ // Same rule the SDK's /authorize applies: exact match, except that loopback
217
+ // redirects (localhost / 127.0.0.1 / [::1]) match with the port ignored, per
218
+ // RFC 8252 §7.3 — Claude Code binds an ephemeral port on every run.
219
+ if (!(client.redirect_uris || []).some((registered) => redirectUriMatches(redirect_uri, registered))) {
220
+ res.locals.reason = 'redirect_uri_not_registered';
221
+ return res.status(400).end('redirect_uri not registered');
222
+ }
223
+ approveThrottle.recordSuccess(req.ip);
224
+ res.locals.reason = client.cimd ? 'approved_cimd' : 'approved_dcr';
225
+ const requestedScopes = inviteScopes || (scopes ? String(scopes).split(' ').filter(Boolean) : []);
226
+ // A passphrase approval with no requested scopes is the owner. Store that
227
+ // as the full-access alias so an empty HTTP scope list never means owner.
228
+ const grantedScopes = inviteSubject ? requestedScopes : (requestedScopes.length ? requestedScopes : ['emet']);
229
+ const code = await provider.createAuthorizationCode({
230
+ client_id, redirect_uri, code_challenge,
231
+ scopes: grantedScopes,
232
+ resource: resource || null,
233
+ subject: inviteSubject || null,
234
+ // The door rides on the token: 'member' for EMET Member Access, 'invite' for EMET Guest Access.
235
+ issued_via: inviteSubject ? (inviteDoor === 'member' ? 'member' : 'invite') : null,
236
+ source_tag: sourceTag,
237
+ });
238
+ const u = new URL(redirect_uri);
239
+ u.searchParams.set('code', code);
240
+ if (state) u.searchParams.set('state', state);
241
+ res.redirect(302, u.href);
242
+ } catch (e) {
243
+ console.error('[emet] approve error', e);
244
+ res.locals.reason = 'approve_error';
245
+ res.status(500).end('approve failed');
246
+ }
247
+ });
248
+
249
+ // Protected MCP endpoint (bearer required -> body parse -> streamable HTTP)
250
+ app.post('/mcp', requireBearerAuth({ verifier: provider, resourceMetadataUrl: PRM_URL }), express.json({ limit: '256kb' }), async (req, res) => {
251
+ try {
252
+ const server = buildServer();
253
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
254
+ res.on('close', () => { try { transport.close(); } catch {} try { server.close(); } catch {} });
255
+ await server.connect(transport);
256
+ await transport.handleRequest(req, res, req.body);
257
+ } catch (e) {
258
+ console.error('[emet] mcp error', e);
259
+ res.locals.reason = 'mcp_error';
260
+ if (!res.headersSent) res.status(500).json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: null });
261
+ }
262
+ });
263
+
264
+ (async () => {
265
+ if (!PASSPHRASE) console.error('[emet] WARNING: EMET_OAUTH_PASSPHRASE unset — all authorization attempts will be rejected.');
266
+ try { await dbManager.ensureClient(); console.error('[emet] Mongo connected'); }
267
+ catch (e) { console.error('[emet] Mongo connect FAILED:', e.message); }
268
+ app.listen(PORT, HOST, () => console.error(`[emet] listening ${PUBLIC_URL} (bind ${HOST}:${PORT}); /mcp OAuth-protected; PRM ${PRM_URL}`));
269
+ })();
package/src/index.js ADDED
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * EMET - Enduring Memory & Epistemic Tiers
5
+ * Copyright (c) 2026 Dreamforge Studio LLC
6
+ *
7
+ * Derived from CASCADE Memory System
8
+ * Copyright (c) 2025-2026 CIPS Corp (C.I.P.S. LLC)
9
+ * Licensed under the MIT License - see LICENSE
10
+ *
11
+ * https://github.com/For-Sunny/cascade-memory-enterprise
12
+ *
13
+ * MCP Server - 6-Layer Structured Memory Architecture
14
+ *
15
+ * REFACTORED: January 22, 2026
16
+ * Split into modular architecture:
17
+ * - database.js: Connection pool, schema, dual-write pattern
18
+ * - tools.js: Tool definitions and handlers
19
+ * - index.js: Thin entry point (this file)
20
+ *
21
+ * DUAL-WRITE Architecture:
22
+ * - RAM disk for instant reads
23
+ * - READ: RAM first (instant) -> Disk fallback
24
+ */
25
+
26
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
27
+ import { setting } from './env.js';
28
+ import { redactText } from './redact.js';
29
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
30
+ import {
31
+ CallToolRequestSchema,
32
+ ListToolsRequestSchema,
33
+ } from "@modelcontextprotocol/sdk/types.js";
34
+ import { EventEmitter } from 'events';
35
+ import fs from 'fs';
36
+
37
+ // Import validation module
38
+ import {
39
+ ValidationError,
40
+ VALID_LAYERS,
41
+ CONTENT_LIMITS,
42
+ NUMERIC_LIMITS
43
+ } from './validation.js';
44
+
45
+ // Import database module
46
+ import {
47
+ EmetDatabase,
48
+ EmetError,
49
+ DatabaseError,
50
+ ConfigurationError,
51
+ ErrorCodes,
52
+ StatusCodes,
53
+ DEBUG,
54
+ MEMORY_LAYERS,
55
+ sanitizeErrorMessage,
56
+ sanitizeDetails
57
+ } from './database.js';
58
+
59
+ import { dispatchTool } from './dispatch.js';
60
+ // Import tools module (the per-tool handlers are reached through dispatch.js)
61
+ import {
62
+ TOOLS,
63
+ RateLimiter,
64
+ rateBucket,
65
+ RateLimitError,
66
+ RATE_LIMIT_CONFIG,
67
+ handleError,
68
+ createSuccessResponse,
69
+ getStatus
70
+ } from './tools.js';
71
+
72
+ // ============================================
73
+ // STRUCTURED LOGGING SYSTEM
74
+ // ============================================
75
+
76
+ // Extracted to ./logger.js on 2026-09-13 so the HTTP transport uses the same
77
+ // recorder instead of a stub. See that file for why.
78
+ import { LogLevel, AuditOperation, StructuredLogger } from './logger.js';
79
+ import { EMET_VERSION } from './version.js';
80
+
81
+ // ============================================
82
+ // INITIALIZATION
83
+ // ============================================
84
+
85
+ // Initialize logger
86
+ const LOG_LEVEL = process.env.LOG_LEVEL || (process.env.DEBUG === 'true' ? 'debug' : 'info');
87
+ const AUDIT_LOG_PATH = setting('AUDIT_LOG') || null;
88
+
89
+ const logger = new StructuredLogger({
90
+ serviceName: 'emet',
91
+ version: EMET_VERSION,
92
+ minLevel: LOG_LEVEL,
93
+ jsonOutput: process.env.LOG_FORMAT !== 'text',
94
+ auditEnabled: true,
95
+ auditLogPath: AUDIT_LOG_PATH
96
+ });
97
+
98
+ // Initialize database manager with dual-write paths
99
+ const dbManager = new EmetDatabase(logger);
100
+
101
+ // Initialize rate limiter
102
+ const rateLimiter = new RateLimiter(logger);
103
+
104
+ // ============================================
105
+ // MCP SERVER
106
+ // ============================================
107
+
108
+ /**
109
+ * Initialize MCP Server
110
+ */
111
+ const server = new Server(
112
+ {
113
+ name: "emet",
114
+ version: EMET_VERSION,
115
+ },
116
+ {
117
+ capabilities: {
118
+ tools: {},
119
+ },
120
+ }
121
+ );
122
+
123
+ /**
124
+ * List available tools
125
+ */
126
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
127
+ return { tools: TOOLS };
128
+ });
129
+
130
+ /**
131
+ * Handle tool calls with rate limiting and centralized error handling
132
+ */
133
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
134
+ const { name, arguments: args } = request.params;
135
+ const { actorStore, personFromAuth } = await import('./persons.js');
136
+ const auth = extra && extra.authInfo;
137
+ const actor = {
138
+ personId: personFromAuth(extra),
139
+ clientId: auth ? (auth.clientId || 'http:visitor') : 'stdio',
140
+ scopes: (auth && auth.scopes) || [],
141
+ transport: auth ? 'http' : 'stdio',
142
+ authenticated: Boolean(auth)
143
+ };
144
+ const bucket = rateBucket(actor);
145
+
146
+ // Rate limit check, per subject and client. Stdio is one bucket per process.
147
+ const rateLimitCheck = rateLimiter.checkLimit(name, bucket);
148
+
149
+ if (!rateLimitCheck.allowed) {
150
+ logger.audit(AuditOperation.RATE_LIMIT_HIT, {
151
+ tool: name,
152
+ reason: rateLimitCheck.reason,
153
+ retryAfterMs: rateLimitCheck.retryAfterMs,
154
+ currentStatus: rateLimiter.getStatus(bucket)
155
+ });
156
+
157
+ logger.warn('Rate limit exceeded', {
158
+ tool: name,
159
+ reason: rateLimitCheck.reason,
160
+ retryAfterMs: rateLimitCheck.retryAfterMs
161
+ });
162
+
163
+ const rateLimitError = new RateLimitError(rateLimitCheck.reason, rateLimitCheck.retryAfterMs);
164
+ return actorStore.run(actor, () => handleError(rateLimitError, name, logger));
165
+ }
166
+
167
+ rateLimiter.recordRequest(name, bucket);
168
+
169
+ // Every tool call leaves one audit entry (H6, 2026-09-19). callTool never
170
+ // throws: it returns either a success response or handleError's response.
171
+ const t0 = Date.now();
172
+ // Parameter NAMES, never values (the owner, 2026-09-27, code audit 1.6): the next
173
+ // audit can measure which parameters hosts use; nothing a host sent is stored.
174
+ const paramNames = Object.keys(args || {}).sort();
175
+ const result = await actorStore.run(actor, () => callTool(name, args));
176
+ logger.audit(AuditOperation.TOOL_CALL, { tool: name, success: result.isError !== true, durationMs: Date.now() - t0, params: paramNames });
177
+ return result;
178
+ });
179
+
180
+ async function callTool(name, args) {
181
+ // One dispatch table for both transports - src/dispatch.js (audit 2026-09-27).
182
+ return dispatchTool(name, args, { dbManager, logger, AuditOperation });
183
+ }
184
+
185
+ // ============================================
186
+ // SERVER STARTUP
187
+ // ============================================
188
+
189
+ /**
190
+ * Start server
191
+ */
192
+ async function main() {
193
+ const startTime = Date.now();
194
+
195
+ logger.info('============================================');
196
+ logger.info(`EMET Memory MCP Server v${EMET_VERSION}`);
197
+ logger.info('============================================');
198
+
199
+ logger.info('Server configuration loaded', {
200
+ debugMode: DEBUG,
201
+ logLevel: LOG_LEVEL,
202
+ auditLogPath: AUDIT_LOG_PATH
203
+ });
204
+
205
+ logger.info('Validation configuration', {
206
+ maxContentLength: CONTENT_LIMITS.MAX_CONTENT_LENGTH,
207
+ maxQueryLength: CONTENT_LIMITS.MAX_QUERY_LENGTH,
208
+ validLayers: VALID_LAYERS,
209
+ limitRange: `${NUMERIC_LIMITS.MIN_LIMIT}-${NUMERIC_LIMITS.MAX_LIMIT}`
210
+ });
211
+
212
+ logger.info('Rate limiting configuration', {
213
+ globalLimit: RATE_LIMIT_CONFIG.GLOBAL_MAX_REQUESTS,
214
+ globalWindowMs: RATE_LIMIT_CONFIG.GLOBAL_WINDOW_MS,
215
+ toolLimits: RATE_LIMIT_CONFIG.TOOL_MAX_REQUESTS,
216
+ cleanupIntervalMs: RATE_LIMIT_CONFIG.CLEANUP_INTERVAL_MS
217
+ });
218
+
219
+ logger.info('Logging configuration', {
220
+ sessionId: logger.sessionId,
221
+ logLevel: LOG_LEVEL,
222
+ jsonOutput: logger.jsonOutput,
223
+ auditEnabled: logger.auditEnabled,
224
+ auditLogPath: AUDIT_LOG_PATH
225
+ });
226
+
227
+ // Connect stdio transport immediately so MCP handshake completes before DB init
228
+ const transport = new StdioServerTransport();
229
+ await server.connect(transport);
230
+
231
+ // Initialize all databases
232
+ const initializedLayers = [];
233
+ const failedLayers = [];
234
+
235
+ for (const layer of Object.keys(MEMORY_LAYERS)) {
236
+ try {
237
+ await dbManager.getConnection(layer);
238
+ initializedLayers.push(layer);
239
+
240
+ logger.audit(AuditOperation.CONNECTION_OPEN, {
241
+ layer,
242
+ success: true
243
+ });
244
+ } catch (e) {
245
+ failedLayers.push({ layer, error: e.message });
246
+ logger.error('Failed to initialize layer', {
247
+ layer,
248
+ error: e
249
+ });
250
+
251
+ logger.audit(AuditOperation.CONNECTION_OPEN, {
252
+ layer,
253
+ success: false,
254
+ errorMessage: e.message
255
+ });
256
+ }
257
+ }
258
+
259
+ const startupDurationMs = Date.now() - startTime;
260
+
261
+ logger.audit(AuditOperation.SERVER_START, {
262
+ initializedLayers,
263
+ failedLayers: failedLayers.length > 0 ? failedLayers : undefined,
264
+ startupDurationMs,
265
+ configuration: {
266
+ rateLimitingEnabled: true,
267
+ auditLoggingEnabled: logger.auditEnabled
268
+ },
269
+ success: failedLayers.length === 0
270
+ });
271
+
272
+ logger.info('============================================');
273
+ logger.info(`EMET v${EMET_VERSION} ready!`, {
274
+ startupDurationMs,
275
+ layersInitialized: initializedLayers.length,
276
+ layersFailed: failedLayers.length,
277
+ sessionId: logger.sessionId
278
+ });
279
+ logger.info('DoS protection ENGAGED!');
280
+ logger.info('Centralized error handling ACTIVE!');
281
+ logger.info('Structured logging ACTIVE!');
282
+ logger.info('Audit trail ENABLED!');
283
+ logger.info('============================================');
284
+ }
285
+
286
+ // ============================================
287
+ // CRASH LOGGING
288
+ // ============================================
289
+
290
+ const CRASH_LOG_PATH = process.env.EMET_CRASH_LOG || null;
291
+
292
+ function writeCrashLog(type, err) {
293
+ try {
294
+ if (!CRASH_LOG_PATH) return; // opt-in: set EMET_CRASH_LOG to a writable path
295
+ const entry = `[${new Date().toISOString()}] ${type}: ${err?.stack || err?.message || String(err)}\n`;
296
+ fs.appendFileSync(CRASH_LOG_PATH, entry, 'utf8');
297
+ } catch (_) {}
298
+ }
299
+
300
+ process.on('uncaughtException', (err) => {
301
+ writeCrashLog('UNCAUGHT_EXCEPTION', err);
302
+ process.exit(1);
303
+ });
304
+
305
+ process.on('unhandledRejection', (reason) => {
306
+ writeCrashLog('UNHANDLED_REJECTION', reason instanceof Error ? reason : new Error(String(reason)));
307
+ // Don't exit — log and continue; unhandled rejections are often recoverable
308
+ });
309
+
310
+ process.on('exit', (code) => {
311
+ if (code !== 0) {
312
+ writeCrashLog('EXIT', new Error(`Process exiting with code ${code}`));
313
+ }
314
+ });
315
+
316
+ // ============================================
317
+ // SHUTDOWN HANDLERS
318
+ // ============================================
319
+
320
+ process.on('SIGINT', async () => {
321
+ logger.info('Received SIGINT signal - initiating shutdown');
322
+
323
+ logger.audit(AuditOperation.SERVER_STOP, {
324
+ signal: 'SIGINT',
325
+ auditStats: logger.getAuditStats()
326
+ });
327
+
328
+ rateLimiter.stop();
329
+ await logger.shutdown();
330
+ await dbManager.closeAll();
331
+
332
+ logger.info('Shutdown complete');
333
+ process.exit(0);
334
+ });
335
+
336
+ process.on('SIGTERM', async () => {
337
+ logger.info('Received SIGTERM signal - initiating shutdown');
338
+
339
+ logger.audit(AuditOperation.SERVER_STOP, {
340
+ signal: 'SIGTERM',
341
+ auditStats: logger.getAuditStats()
342
+ });
343
+
344
+ rateLimiter.stop();
345
+ await logger.shutdown();
346
+ await dbManager.closeAll();
347
+
348
+ logger.info('Shutdown complete');
349
+ process.exit(0);
350
+ });
351
+
352
+ // Start the server
353
+ main().catch((error) => {
354
+ logger.audit(AuditOperation.SERVER_STOP, {
355
+ signal: 'FATAL_ERROR',
356
+ success: false,
357
+ errorMessage: error?.message || 'Unknown fatal error'
358
+ });
359
+
360
+ logger.error('Fatal server error', {
361
+ error,
362
+ message: error?.message || 'Unknown fatal error'
363
+ });
364
+
365
+ process.exit(1);
366
+ });
367
+
368
+ // ============================================
369
+ // EXPORTS (for external use)
370
+ // ============================================
371
+
372
+ export {
373
+ // Logger
374
+ logger,
375
+ StructuredLogger,
376
+ LogLevel,
377
+ AuditOperation,
378
+
379
+ // Error classes
380
+ EmetError,
381
+ DatabaseError,
382
+ ConfigurationError,
383
+ ErrorCodes,
384
+ StatusCodes,
385
+
386
+ // Utilities
387
+ sanitizeErrorMessage,
388
+ handleError,
389
+ createSuccessResponse
390
+ };