memorix 1.8.0 → 1.8.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.
- package/CHANGELOG.md +29 -0
- package/dist/cli/index.js +407 -130
- package/dist/dashboard/static/app.js +37 -23
- package/dist/dashboard/static/index.html +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +633 -391
- package/dist/maintenance-runner.js +31 -25
- package/dist/media-audio-runner.js +31 -25
- package/dist/media-video-runner.js +31 -25
- package/dist/memcode-runtime/CHANGELOG.md +29 -0
- package/dist/sdk.js +625 -390
- package/dist/vector-backfill-runner.js +31 -25
- package/docs/1.2.0-PROVIDER-QUALITY.md +15 -6
- package/docs/1.3.3-MULTIMODAL-MEDIA-AND-MCP-COMPAT.md +7 -6
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +10 -8
- package/docs/API_REFERENCE.md +12 -8
- package/docs/ARCHITECTURE.md +3 -1
- package/docs/CONFIGURATION.md +2 -2
- package/docs/DASHBOARD.md +6 -0
- package/docs/DEVELOPMENT.md +2 -2
- package/docs/README.md +14 -8
- package/package.json +1 -1
- package/plugins/antigravity/memorix/plugin.json +2 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +3 -3
- package/src/cli/commands/dashboard.ts +1 -1
- package/src/cli/commands/serve-http.ts +38 -3
- package/src/codegraph/external-provider.ts +299 -19
- package/src/config/dotenv-loader.ts +9 -4
- package/src/config.ts +16 -16
- package/src/dashboard/maintenance.ts +4 -1
- package/src/dashboard/server.ts +85 -86
- package/src/dashboard/static/index.html +1 -1
- package/src/llm/provider.ts +17 -4
- package/src/server/mcp-compat.ts +82 -0
- package/src/server/mcp-diagnostics.ts +13 -4
- package/src/server.ts +9 -6
package/src/dashboard/server.ts
CHANGED
|
@@ -958,18 +958,10 @@ interface DashboardState {
|
|
|
958
958
|
dataDir: string;
|
|
959
959
|
projectRoot: string | null;
|
|
960
960
|
projectResolved: boolean;
|
|
961
|
-
mode: 'standalone'
|
|
961
|
+
mode: 'standalone';
|
|
962
962
|
port: number;
|
|
963
963
|
}
|
|
964
964
|
|
|
965
|
-
/** Optional coordination instances passed from MCP server */
|
|
966
|
-
export interface TeamInstances {
|
|
967
|
-
registry: { listAgents: (filter?: any) => any[]; getActiveCount: () => number; getAgent: (id: string) => any };
|
|
968
|
-
fileLocks: { listLocks: (agentId?: string) => any[]; cleanExpired: () => void };
|
|
969
|
-
taskManager: { list: (filter?: any) => any[]; getAvailable: () => any[] };
|
|
970
|
-
messageBus: { getUnreadCount: (agentId: string) => number };
|
|
971
|
-
}
|
|
972
|
-
|
|
973
965
|
function parseJsonField(value: unknown, fallback: unknown): unknown {
|
|
974
966
|
if (typeof value !== 'string') return value ?? fallback;
|
|
975
967
|
try {
|
|
@@ -1027,7 +1019,72 @@ function normalizeDashboardTask(task: any) {
|
|
|
1027
1019
|
};
|
|
1028
1020
|
}
|
|
1029
1021
|
|
|
1030
|
-
|
|
1022
|
+
type TeamSnapshotStatus = 'ok' | 'degraded' | 'error';
|
|
1023
|
+
|
|
1024
|
+
interface TeamSnapshot {
|
|
1025
|
+
status: TeamSnapshotStatus;
|
|
1026
|
+
mode: DashboardState['mode'];
|
|
1027
|
+
readOnly: true;
|
|
1028
|
+
scope: string;
|
|
1029
|
+
agents: any[];
|
|
1030
|
+
activeCount: number;
|
|
1031
|
+
recentCount: number;
|
|
1032
|
+
historicalCount: number;
|
|
1033
|
+
totalAgents: number;
|
|
1034
|
+
recentWindowDays: number;
|
|
1035
|
+
locks: any[];
|
|
1036
|
+
tasks: any[];
|
|
1037
|
+
availableTasks: number;
|
|
1038
|
+
sessions: number;
|
|
1039
|
+
roles: any[];
|
|
1040
|
+
roleOccupancy: any[];
|
|
1041
|
+
handoffs: any[];
|
|
1042
|
+
openTasks: number;
|
|
1043
|
+
openHandoffs: number;
|
|
1044
|
+
totalUnread: number;
|
|
1045
|
+
activeSessions: number;
|
|
1046
|
+
error?: string;
|
|
1047
|
+
errorCode?: string;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function emptyTeamSnapshot(
|
|
1051
|
+
mode: DashboardState['mode'],
|
|
1052
|
+
scope: string,
|
|
1053
|
+
status: Exclude<TeamSnapshotStatus, 'ok'>,
|
|
1054
|
+
): TeamSnapshot {
|
|
1055
|
+
return {
|
|
1056
|
+
status,
|
|
1057
|
+
mode,
|
|
1058
|
+
readOnly: true,
|
|
1059
|
+
scope,
|
|
1060
|
+
agents: [],
|
|
1061
|
+
activeCount: 0,
|
|
1062
|
+
recentCount: 0,
|
|
1063
|
+
historicalCount: 0,
|
|
1064
|
+
totalAgents: 0,
|
|
1065
|
+
recentWindowDays: 7,
|
|
1066
|
+
locks: [],
|
|
1067
|
+
tasks: [],
|
|
1068
|
+
availableTasks: 0,
|
|
1069
|
+
sessions: 0,
|
|
1070
|
+
roles: [],
|
|
1071
|
+
roleOccupancy: [],
|
|
1072
|
+
handoffs: [],
|
|
1073
|
+
openTasks: 0,
|
|
1074
|
+
openHandoffs: 0,
|
|
1075
|
+
totalUnread: 0,
|
|
1076
|
+
activeSessions: 0,
|
|
1077
|
+
error: status === 'error' ? 'Coordination state unavailable' : 'Coordination state is incomplete',
|
|
1078
|
+
errorCode: status === 'error' ? 'COORDINATION_STATE_UNAVAILABLE' : 'COORDINATION_STATE_DEGRADED',
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function logTeamSnapshotFailure(scope: string, status: Exclude<TeamSnapshotStatus, 'ok'>): void {
|
|
1083
|
+
const safeScope = scope === 'global' ? 'global' : 'project';
|
|
1084
|
+
console.error('[dashboard] coordination snapshot unavailable', { status, scope: safeScope });
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function buildTeamSnapshot(dataDir: string, projectId: string, scope: string, mode: DashboardState['mode']): Promise<TeamSnapshot> {
|
|
1031
1088
|
try {
|
|
1032
1089
|
const { initTeamStore } = await import('../team/team-store.js');
|
|
1033
1090
|
const teamStore = await initTeamStore(dataDir);
|
|
@@ -1053,8 +1110,9 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
|
|
|
1053
1110
|
const roleOccupancy = effectiveProjectId ? teamStore.getRoleOccupancy(effectiveProjectId) : [];
|
|
1054
1111
|
const handoffs = effectiveProjectId ? teamStore.listHandoffs(effectiveProjectId) : [];
|
|
1055
1112
|
return {
|
|
1113
|
+
status: 'ok',
|
|
1056
1114
|
mode,
|
|
1057
|
-
readOnly:
|
|
1115
|
+
readOnly: true,
|
|
1058
1116
|
scope,
|
|
1059
1117
|
agents: withTier,
|
|
1060
1118
|
activeCount,
|
|
@@ -1075,28 +1133,8 @@ async function buildTeamSnapshot(dataDir: string, projectId: string, scope: stri
|
|
|
1075
1133
|
activeSessions: activeCount,
|
|
1076
1134
|
};
|
|
1077
1135
|
} catch {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
readOnly: mode === 'standalone',
|
|
1081
|
-
scope,
|
|
1082
|
-
agents: [],
|
|
1083
|
-
activeCount: 0,
|
|
1084
|
-
recentCount: 0,
|
|
1085
|
-
historicalCount: 0,
|
|
1086
|
-
totalAgents: 0,
|
|
1087
|
-
recentWindowDays: 7,
|
|
1088
|
-
locks: [],
|
|
1089
|
-
tasks: [],
|
|
1090
|
-
availableTasks: 0,
|
|
1091
|
-
sessions: 0,
|
|
1092
|
-
roles: [],
|
|
1093
|
-
roleOccupancy: [],
|
|
1094
|
-
handoffs: [],
|
|
1095
|
-
openTasks: 0,
|
|
1096
|
-
openHandoffs: 0,
|
|
1097
|
-
totalUnread: 0,
|
|
1098
|
-
activeSessions: 0,
|
|
1099
|
-
};
|
|
1136
|
+
logTeamSnapshotFailure(scope, 'error');
|
|
1137
|
+
return emptyTeamSnapshot(mode, scope, 'error');
|
|
1100
1138
|
}
|
|
1101
1139
|
}
|
|
1102
1140
|
|
|
@@ -1125,7 +1163,6 @@ export async function startDashboard(
|
|
|
1125
1163
|
projectId: string,
|
|
1126
1164
|
projectName: string,
|
|
1127
1165
|
autoOpen = true,
|
|
1128
|
-
teamInstances?: TeamInstances,
|
|
1129
1166
|
projectRoot: string | null = null,
|
|
1130
1167
|
projectResolved = true,
|
|
1131
1168
|
): Promise<void> {
|
|
@@ -1136,8 +1173,7 @@ export async function startDashboard(
|
|
|
1136
1173
|
const baseDir = getBaseDataDir();
|
|
1137
1174
|
|
|
1138
1175
|
// Mutable state — can be updated via /api/set-current-project
|
|
1139
|
-
const
|
|
1140
|
-
const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: isControlPlane ? 'control-plane' : 'standalone', port };
|
|
1176
|
+
const state: DashboardState = { projectId, projectName, dataDir, projectRoot, projectResolved, mode: 'standalone', port };
|
|
1141
1177
|
|
|
1142
1178
|
const server = createServer(async (req, res) => {
|
|
1143
1179
|
const url = req.url || '/';
|
|
@@ -1164,56 +1200,20 @@ export async function startDashboard(
|
|
|
1164
1200
|
return;
|
|
1165
1201
|
}
|
|
1166
1202
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const scope = parsedUrl.searchParams.get('scope') || 'project';
|
|
1171
|
-
sendJson(res, await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode));
|
|
1172
|
-
return;
|
|
1173
|
-
}
|
|
1174
|
-
try {
|
|
1175
|
-
teamInstances.fileLocks.cleanExpired();
|
|
1176
|
-
const agents = teamInstances.registry.listAgents();
|
|
1177
|
-
const locks = teamInstances.fileLocks.listLocks();
|
|
1178
|
-
const tasks = teamInstances.taskManager.list();
|
|
1179
|
-
const available = teamInstances.taskManager.getAvailable();
|
|
1180
|
-
|
|
1181
|
-
// Role occupancy and handoffs from TeamStore (if available)
|
|
1182
|
-
let roles: any[] = [];
|
|
1183
|
-
let roleOccupancy: any[] = [];
|
|
1184
|
-
let handoffs: any[] = [];
|
|
1185
|
-
try {
|
|
1186
|
-
const { getTeamStore, isTeamStoreInitialized } = await import('../team/team-store.js');
|
|
1187
|
-
if (isTeamStoreInitialized()) {
|
|
1188
|
-
const teamStore = getTeamStore();
|
|
1189
|
-
const projectId = state.projectId;
|
|
1190
|
-
roles = teamStore.listRoles(projectId);
|
|
1191
|
-
roleOccupancy = teamStore.getRoleOccupancy(projectId);
|
|
1192
|
-
handoffs = teamStore.listHandoffs(projectId);
|
|
1193
|
-
}
|
|
1194
|
-
} catch { /* team store not available in standalone mode */ }
|
|
1195
|
-
|
|
1203
|
+
const parsedUrl = new URL(url, `http://127.0.0.1:${port}`);
|
|
1204
|
+
if (parsedUrl.pathname === '/api/team') {
|
|
1205
|
+
if (req.method !== 'GET') {
|
|
1196
1206
|
sendJson(res, {
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
tasks,
|
|
1204
|
-
availableTasks: available.length,
|
|
1205
|
-
roles,
|
|
1206
|
-
roleOccupancy,
|
|
1207
|
-
handoffs,
|
|
1208
|
-
// Resume data for "Continue this project" area
|
|
1209
|
-
openTasks: tasks.filter((t: any) => t.status === 'pending' || t.status === 'in_progress').length,
|
|
1210
|
-
openHandoffs: handoffs.filter((h: any) => h.handoff_status === 'open' || h.handoffStatus === 'open').length,
|
|
1211
|
-
totalUnread: agents.reduce((sum: number, a: any) => sum + teamInstances!.messageBus.getUnreadCount(a.id), 0),
|
|
1212
|
-
activeSessions: agents.filter((a: any) => a.status === 'active').length,
|
|
1213
|
-
});
|
|
1214
|
-
} catch {
|
|
1215
|
-
sendJson(res, { agents: [], activeCount: 0, locks: [], tasks: [], availableTasks: 0, roles: [], roleOccupancy: [], handoffs: [] });
|
|
1207
|
+
status: 'error',
|
|
1208
|
+
readOnly: true,
|
|
1209
|
+
error: 'Coordination status is read-only',
|
|
1210
|
+
errorCode: 'COORDINATION_STATUS_READ_ONLY',
|
|
1211
|
+
}, 405);
|
|
1212
|
+
return;
|
|
1216
1213
|
}
|
|
1214
|
+
const scope = parsedUrl.searchParams.get('scope') || 'project';
|
|
1215
|
+
const snapshot = await buildTeamSnapshot(state.dataDir, state.projectId, scope, state.mode);
|
|
1216
|
+
sendJson(res, snapshot, snapshot.status === 'error' ? 503 : 200);
|
|
1217
1217
|
return;
|
|
1218
1218
|
}
|
|
1219
1219
|
|
|
@@ -1237,13 +1237,12 @@ export async function startDashboard(
|
|
|
1237
1237
|
server.listen(port, '127.0.0.1', () => {
|
|
1238
1238
|
const url = `http://127.0.0.1:${port}`;
|
|
1239
1239
|
const resolvedLabel = projectResolved ? 'resolved' : 'unresolved';
|
|
1240
|
-
const modeLabel =
|
|
1240
|
+
const modeLabel = 'Standalone';
|
|
1241
1241
|
console.error(` Memorix Dashboard [${modeLabel}]`);
|
|
1242
1242
|
console.error(` ───────────────────────`);
|
|
1243
1243
|
console.error(` Mode: ${modeLabel}`);
|
|
1244
1244
|
console.error(` Project: ${projectName} (${projectId}) [${resolvedLabel}]`);
|
|
1245
1245
|
console.error(` Local: ${url}`);
|
|
1246
|
-
if (isControlPlane) console.error(` MCP: ${url}/mcp`);
|
|
1247
1246
|
console.error(` Data dir: ${dataDir}`);
|
|
1248
1247
|
console.error(`\n Press Ctrl+C to stop\n`);
|
|
1249
1248
|
if (autoOpen) openBrowser(url);
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
</svg>
|
|
136
136
|
<span class="nav-label">Sessions</span>
|
|
137
137
|
</button>
|
|
138
|
-
<button class="nav-btn" data-page="team" title="Coordination" aria-label="Coordination">
|
|
138
|
+
<button class="nav-btn" data-page="team" title="Coordination Status" aria-label="Coordination Status">
|
|
139
139
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
140
140
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
141
141
|
<circle cx="9" cy="7" r="4"/>
|
package/src/llm/provider.ts
CHANGED
|
@@ -210,6 +210,10 @@ export type LLMConfigScope = 'memory' | 'agent';
|
|
|
210
210
|
|
|
211
211
|
export interface InitLLMOptions {
|
|
212
212
|
scope?: LLMConfigScope;
|
|
213
|
+
/** Resolve project TOML/.env instead of relying on process.cwd(). */
|
|
214
|
+
projectRoot?: string | null;
|
|
215
|
+
/** Override the home directory used for global TOML/.env resolution. */
|
|
216
|
+
homeDir?: string;
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
/**
|
|
@@ -218,20 +222,29 @@ export interface InitLLMOptions {
|
|
|
218
222
|
*/
|
|
219
223
|
export function initLLM(options: InitLLMOptions = {}): LLMConfig | null {
|
|
220
224
|
const scope = options.scope ?? 'memory';
|
|
221
|
-
const
|
|
225
|
+
const configOptions = { projectRoot: options.projectRoot, homeDir: options.homeDir };
|
|
226
|
+
const apiKey = scope === 'agent'
|
|
227
|
+
? getAgentLLMApiKey(configOptions)
|
|
228
|
+
: getLLMApiKey(configOptions);
|
|
222
229
|
if (!apiKey) {
|
|
223
230
|
currentConfig = null;
|
|
224
231
|
return null;
|
|
225
232
|
}
|
|
226
233
|
|
|
227
|
-
const provider = (scope === 'agent'
|
|
234
|
+
const provider = (scope === 'agent'
|
|
235
|
+
? getAgentLLMProvider(configOptions)
|
|
236
|
+
: getLLMProvider(configOptions)) as LLMConfig['provider'];
|
|
228
237
|
const defaults = PROVIDER_DEFAULTS[provider] ?? PROVIDER_DEFAULTS.openai;
|
|
229
238
|
|
|
230
239
|
currentConfig = {
|
|
231
240
|
provider,
|
|
232
241
|
apiKey,
|
|
233
|
-
model: scope === 'agent'
|
|
234
|
-
|
|
242
|
+
model: scope === 'agent'
|
|
243
|
+
? getAgentLLMModel(defaults.model, configOptions)
|
|
244
|
+
: getLLMModel(defaults.model, configOptions),
|
|
245
|
+
baseUrl: scope === 'agent'
|
|
246
|
+
? getAgentLLMBaseUrl(defaults.baseUrl, configOptions)
|
|
247
|
+
: getLLMBaseUrl(defaults.baseUrl, configOptions),
|
|
235
248
|
};
|
|
236
249
|
|
|
237
250
|
return currentConfig;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small MCP 2026-07-28 discovery adapter for the pinned v1 SDK.
|
|
3
|
+
*
|
|
4
|
+
* The v1 SDK can route an extension request through its low-level Server, but
|
|
5
|
+
* it does not implement the modern discovery/cache negotiation itself. Keep
|
|
6
|
+
* this adapter deliberately narrow: advertise discovery and the existing
|
|
7
|
+
* legacy tool handlers, while leaving unsupported extensions unadvertised.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { RequestSchema, type ServerCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { z } from 'zod/v4';
|
|
13
|
+
|
|
14
|
+
export const MCP_MODERN_PROTOCOL_VERSION = '2026-07-28';
|
|
15
|
+
export const MCP_LEGACY_PROTOCOL_VERSION = '2025-11-25';
|
|
16
|
+
export const MCP_OLDEST_LEGACY_PROTOCOL_VERSION = '2024-11-05';
|
|
17
|
+
export const MCP_SERVER_DISCOVER_METHOD = 'server/discover';
|
|
18
|
+
export const MCP_SERVER_INFO_META_KEY = 'io.modelcontextprotocol/serverInfo';
|
|
19
|
+
|
|
20
|
+
const ServerDiscoverRequestSchema = RequestSchema.extend({
|
|
21
|
+
method: z.literal(MCP_SERVER_DISCOVER_METHOD),
|
|
22
|
+
params: z.looseObject({}).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export interface McpServerInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
version: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface McpDiscoverResult {
|
|
31
|
+
resultType: 'complete';
|
|
32
|
+
supportedVersions: string[];
|
|
33
|
+
capabilities: ServerCapabilities;
|
|
34
|
+
_meta: {
|
|
35
|
+
[MCP_SERVER_INFO_META_KEY]: McpServerInfo;
|
|
36
|
+
};
|
|
37
|
+
ttlMs: number;
|
|
38
|
+
cacheScope: 'public' | 'private';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const registeredServers = new WeakSet<McpServer>();
|
|
42
|
+
|
|
43
|
+
/** Build the wire result without exposing unsupported cache/task features. */
|
|
44
|
+
export function buildMcpDiscoverResult(server: McpServer, serverInfo: McpServerInfo): McpDiscoverResult {
|
|
45
|
+
// SDK 1.30 exposes this method at runtime but marks it private in its
|
|
46
|
+
// declaration file. The discovery response must reflect the capabilities
|
|
47
|
+
// already registered on this exact low-level Server instance.
|
|
48
|
+
const getCapabilities = (server.server as unknown as {
|
|
49
|
+
getCapabilities: () => ServerCapabilities;
|
|
50
|
+
}).getCapabilities.bind(server.server);
|
|
51
|
+
return {
|
|
52
|
+
resultType: 'complete',
|
|
53
|
+
supportedVersions: [
|
|
54
|
+
MCP_MODERN_PROTOCOL_VERSION,
|
|
55
|
+
MCP_LEGACY_PROTOCOL_VERSION,
|
|
56
|
+
MCP_OLDEST_LEGACY_PROTOCOL_VERSION,
|
|
57
|
+
],
|
|
58
|
+
capabilities: getCapabilities(),
|
|
59
|
+
_meta: {
|
|
60
|
+
[MCP_SERVER_INFO_META_KEY]: { ...serverInfo },
|
|
61
|
+
},
|
|
62
|
+
// v1 has no response-cache negotiation. A zero private TTL is the
|
|
63
|
+
// conservative result and prevents clients from inventing a shared cache.
|
|
64
|
+
ttlMs: 0,
|
|
65
|
+
cacheScope: 'private',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Register `server/discover` on the v1 low-level Server.
|
|
71
|
+
*
|
|
72
|
+
* This is idempotent for a single McpServer instance because HTTP/session
|
|
73
|
+
* setup can revisit the registration boundary while reusing an object.
|
|
74
|
+
*/
|
|
75
|
+
export function registerMcpDiscovery(server: McpServer, serverInfo: McpServerInfo): void {
|
|
76
|
+
if (registeredServers.has(server)) return;
|
|
77
|
+
|
|
78
|
+
server.server.setRequestHandler(ServerDiscoverRequestSchema, async () => (
|
|
79
|
+
buildMcpDiscoverResult(server, serverInfo)
|
|
80
|
+
));
|
|
81
|
+
registeredServers.add(server);
|
|
82
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** MCP compatibility diagnostics shared by HTTP and CLI-facing checks. */
|
|
2
2
|
|
|
3
3
|
export const CURRENT_MCP_PROTOCOL_VERSION = '2026-07-28';
|
|
4
|
-
export const LEGACY_MCP_PROTOCOL_VERSION = '
|
|
4
|
+
export const LEGACY_MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
5
|
+
export const OLDEST_LEGACY_MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
5
6
|
export const MCP_SERVER_NAME = 'io.github.AVIDS2/memorix';
|
|
6
7
|
|
|
7
8
|
export interface McpDiagnostics {
|
|
@@ -9,13 +10,17 @@ export interface McpDiagnostics {
|
|
|
9
10
|
protocol: {
|
|
10
11
|
current: string;
|
|
11
12
|
legacy: string;
|
|
13
|
+
discovery: 'supported';
|
|
14
|
+
versionlessRequests: 'legacy-compatible';
|
|
15
|
+
modernResultMetadata: 'discovery-only';
|
|
12
16
|
statelessRequests: 'supported' | 'compatibility-only' | 'unsupported';
|
|
13
17
|
statefulStreamableHttp: 'supported';
|
|
14
18
|
};
|
|
15
19
|
capabilities: {
|
|
16
20
|
tasks: 'supported' | 'unsupported';
|
|
17
21
|
polling: 'supported' | 'unsupported';
|
|
18
|
-
|
|
22
|
+
subscriptions: 'supported' | 'unsupported';
|
|
23
|
+
listCache: 'supported' | 'unsupported';
|
|
19
24
|
explicitProjectHandle: 'supported';
|
|
20
25
|
};
|
|
21
26
|
}
|
|
@@ -27,13 +32,17 @@ export function getMcpDiagnostics(): McpDiagnostics {
|
|
|
27
32
|
protocol: {
|
|
28
33
|
current: CURRENT_MCP_PROTOCOL_VERSION,
|
|
29
34
|
legacy: LEGACY_MCP_PROTOCOL_VERSION,
|
|
30
|
-
|
|
35
|
+
discovery: 'supported',
|
|
36
|
+
versionlessRequests: 'legacy-compatible',
|
|
37
|
+
modernResultMetadata: 'discovery-only',
|
|
38
|
+
statelessRequests: 'compatibility-only',
|
|
31
39
|
statefulStreamableHttp: 'supported',
|
|
32
40
|
},
|
|
33
41
|
capabilities: {
|
|
34
42
|
tasks: 'unsupported',
|
|
35
43
|
polling: 'unsupported',
|
|
36
|
-
|
|
44
|
+
subscriptions: 'unsupported',
|
|
45
|
+
listCache: 'unsupported',
|
|
37
46
|
explicitProjectHandle: 'supported',
|
|
38
47
|
},
|
|
39
48
|
};
|
package/src/server.ts
CHANGED
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
type ProjectBindingController,
|
|
63
63
|
type ProjectBindingSource,
|
|
64
64
|
} from './server/request-context.js';
|
|
65
|
+
import { registerMcpDiscovery } from './server/mcp-compat.js';
|
|
65
66
|
|
|
66
67
|
// ── Timeout budgets for LLM-heavy paths ──────────────────────────
|
|
67
68
|
const FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS); // Formation pipeline (extract+resolve+evaluate)
|
|
@@ -198,7 +199,7 @@ function createDeterministicInstanceId(projectId: string, agentType: string, age
|
|
|
198
199
|
* Create and configure the Memorix MCP Server.
|
|
199
200
|
*/
|
|
200
201
|
/** Optional shared TeamStore — passed by serve-http so all sessions share state */
|
|
201
|
-
export interface
|
|
202
|
+
export interface SharedTeamStore {
|
|
202
203
|
teamStore: import('./team/team-store.js').TeamStore;
|
|
203
204
|
}
|
|
204
205
|
|
|
@@ -245,7 +246,7 @@ export function shouldAwaitProjectRuntime(toolName: string): boolean {
|
|
|
245
246
|
export async function createMemorixServer(
|
|
246
247
|
cwd?: string,
|
|
247
248
|
existingServer?: McpServer,
|
|
248
|
-
sharedTeam?:
|
|
249
|
+
sharedTeam?: SharedTeamStore,
|
|
249
250
|
options: CreateMemorixServerOptions = {},
|
|
250
251
|
): Promise<{
|
|
251
252
|
server: McpServer;
|
|
@@ -593,10 +594,12 @@ export async function createMemorixServer(
|
|
|
593
594
|
};
|
|
594
595
|
|
|
595
596
|
// Create MCP server (or use existing one from roots-aware flow)
|
|
596
|
-
const
|
|
597
|
+
const serverInfo = {
|
|
597
598
|
name: 'memorix',
|
|
598
599
|
version: typeof __MEMORIX_VERSION__ !== 'undefined' ? __MEMORIX_VERSION__ : '1.0.1',
|
|
599
|
-
}
|
|
600
|
+
};
|
|
601
|
+
const server = existingServer ?? new McpServer(serverInfo);
|
|
602
|
+
registerMcpDiscovery(server, serverInfo);
|
|
600
603
|
|
|
601
604
|
const originalRegisterTool = server.registerTool.bind(server);
|
|
602
605
|
server.registerTool = ((name: string, ...args: unknown[]) => {
|
|
@@ -4420,7 +4423,7 @@ export async function createMemorixServer(
|
|
|
4420
4423
|
console.error(`[memorix] Dashboard staticDir: ${staticDir}`);
|
|
4421
4424
|
|
|
4422
4425
|
// Start in background (non-blocking), disable auto-open (we'll open it ourselves)
|
|
4423
|
-
startDashboard(projectDir, portNum, staticDir, project.id, project.name, false,
|
|
4426
|
+
startDashboard(projectDir, portNum, staticDir, project.id, project.name, false, project.rootPath, projectResolved)
|
|
4424
4427
|
.then(() => { dashboardRunning = true; })
|
|
4425
4428
|
.catch((err) => { console.error('[memorix] Dashboard error:', err); dashboardRunning = false; });
|
|
4426
4429
|
|
|
@@ -4786,7 +4789,7 @@ export async function createMemorixServer(
|
|
|
4786
4789
|
server.registerTool(
|
|
4787
4790
|
'team_task',
|
|
4788
4791
|
{
|
|
4789
|
-
title: 'Task
|
|
4792
|
+
title: 'Task Status',
|
|
4790
4793
|
description:
|
|
4791
4794
|
'Create, claim, complete, or list tasks in the team task board. Supports dependencies. ' +
|
|
4792
4795
|
'Action "create": create a task. Action "claim": assign to yourself (atomic, race-safe). ' +
|