@hanhnd/agent-kit 1.0.39 → 1.0.41

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/dist/cli.js CHANGED
@@ -15195,6 +15195,22 @@ async function withTimeout(promise, timeoutMs, createError) {
15195
15195
  if (timer) clearTimeout(timer);
15196
15196
  }
15197
15197
  }
15198
+ async function mapLimit(items, limit, worker) {
15199
+ if (items.length === 0) return [];
15200
+ const concurrency = Math.max(1, Math.floor(limit));
15201
+ const results = new Array(items.length);
15202
+ let nextIndex = 0;
15203
+ async function runWorker() {
15204
+ while (nextIndex < items.length) {
15205
+ const currentIndex = nextIndex;
15206
+ nextIndex++;
15207
+ results[currentIndex] = await worker(items[currentIndex], currentIndex);
15208
+ }
15209
+ }
15210
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, () => runWorker());
15211
+ await Promise.all(runners);
15212
+ return results;
15213
+ }
15198
15214
 
15199
15215
  // src/utils/paths.ts
15200
15216
  import * as os2 from "os";
@@ -15912,9 +15928,19 @@ var MemoryIndexer = class {
15912
15928
  }
15913
15929
  _ready = false;
15914
15930
  async indexFile(absolutePath) {
15915
- return this.indexFileRelativeTo(absolutePath, this.config.wikiDir);
15931
+ const mutation = await this.prepareIndexFileRelativeTo(absolutePath, this.config.wikiDir);
15932
+ return this.commitPreparedMutation(mutation);
15916
15933
  }
15917
- async indexFileRelativeTo(absolutePath, sourceRoot) {
15934
+ makeSkippedMutation(source, skipped) {
15935
+ return {
15936
+ source,
15937
+ chunksToUpsert: [],
15938
+ embeddings: [],
15939
+ idsToDelete: [],
15940
+ stats: { indexed: 0, deleted: 0, skipped }
15941
+ };
15942
+ }
15943
+ async prepareIndexFileRelativeTo(absolutePath, sourceRoot) {
15918
15944
  const source = path8.relative(sourceRoot, absolutePath);
15919
15945
  const existingChunkIds = this.store.hashesBySource(source);
15920
15946
  let text;
@@ -15922,7 +15948,7 @@ var MemoryIndexer = class {
15922
15948
  text = await fs6.promises.readFile(absolutePath, "utf8");
15923
15949
  } catch (err) {
15924
15950
  console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
15925
- return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
15951
+ return this.makeSkippedMutation(source, existingChunkIds.size);
15926
15952
  }
15927
15953
  let fileMtimeAt;
15928
15954
  try {
@@ -15942,21 +15968,30 @@ var MemoryIndexer = class {
15942
15968
  embeddings = await this.embedder.embed(toIndex.map((c) => c.content));
15943
15969
  } catch (err) {
15944
15970
  console.warn(`[memory-indexer] Embedding failed for ${absolutePath}:`, err);
15945
- return { indexed: 0, deleted: 0, skipped: existingChunkIds.size };
15971
+ return this.makeSkippedMutation(source, existingChunkIds.size);
15946
15972
  }
15947
15973
  }
15948
- if (toIndex.length > 0) {
15949
- this.store.upsert(toIndex, embeddings);
15950
- }
15951
- if (toDelete.length > 0) {
15952
- this.store.deleteByIds(toDelete);
15953
- }
15954
15974
  return {
15955
- indexed: toIndex.length,
15956
- deleted: toDelete.length,
15957
- skipped: allChunks.length - toIndex.length
15975
+ source,
15976
+ chunksToUpsert: toIndex,
15977
+ embeddings,
15978
+ idsToDelete: toDelete,
15979
+ stats: {
15980
+ indexed: toIndex.length,
15981
+ deleted: toDelete.length,
15982
+ skipped: allChunks.length - toIndex.length
15983
+ }
15958
15984
  };
15959
15985
  }
15986
+ commitPreparedMutation(mutation) {
15987
+ if (mutation.chunksToUpsert.length > 0) {
15988
+ this.store.upsert(mutation.chunksToUpsert, mutation.embeddings);
15989
+ }
15990
+ if (mutation.idsToDelete.length > 0) {
15991
+ this.store.deleteByIds(mutation.idsToDelete);
15992
+ }
15993
+ return mutation.stats;
15994
+ }
15960
15995
  async indexDirectory(rootDir, opts = {}) {
15961
15996
  const totals = { indexed: 0, deleted: 0, skipped: 0 };
15962
15997
  let rootExists;
@@ -15998,8 +16033,13 @@ var MemoryIndexer = class {
15998
16033
  totals.deleted += 1;
15999
16034
  }
16000
16035
  }
16001
- for (const file of files) {
16002
- const stats = await this.indexFileRelativeTo(file, relativeBase);
16036
+ const preparedMutations = await mapLimit(
16037
+ files,
16038
+ opts.fileConcurrency ?? 2,
16039
+ (file) => this.prepareIndexFileRelativeTo(file, relativeBase)
16040
+ );
16041
+ for (const mutation of preparedMutations) {
16042
+ const stats = this.commitPreparedMutation(mutation);
16003
16043
  totals.indexed += stats.indexed;
16004
16044
  totals.deleted += stats.deleted;
16005
16045
  totals.skipped += stats.skipped;
@@ -1,6 +1,6 @@
1
- export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "scenario", "investigation", "code"];
1
+ export declare const HANDOFF_TYPES: readonly ["brainstorm", "clarification", "plan", "ticket", "research", "investigation", "code"];
2
2
  type HandoffType = (typeof HANDOFF_TYPES)[number];
3
- type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'scenario' | 'investigation' | 'code';
3
+ type CanonicalHandoffType = 'brainstorm' | 'clarification' | 'plan' | 'ticket' | 'research' | 'investigation' | 'code';
4
4
  export interface SavedHandoffFolderLocation {
5
5
  featureSlug: string;
6
6
  canonicalType: CanonicalHandoffType;
@@ -5,7 +5,6 @@ export const HANDOFF_TYPES = [
5
5
  'plan',
6
6
  'ticket',
7
7
  'research',
8
- 'scenario',
9
8
  'investigation',
10
9
  'code',
11
10
  ];
@@ -23,13 +23,13 @@ registerIntegrationTools(server);
23
23
  registerCoreTools(server);
24
24
  registerAgentTools(server);
25
25
  const workspaceRoot = getWorkspaceRoot();
26
- const memoryIndexer = registerMemoryTools(server, workspaceRoot);
26
+ const memorySubsystem = registerMemoryTools(server, workspaceRoot);
27
27
  // ═══════════════════════════════════════════════════════════════
28
28
  // START SERVER
29
29
  // ═══════════════════════════════════════════════════════════════
30
30
  const transport = new StdioServerTransport();
31
31
  await server.connect(transport);
32
32
  // Fire-and-forget startup indexing — must run after server.connect
33
- if (memoryIndexer) {
34
- void memoryIndexer.startupIndex();
33
+ if (memorySubsystem) {
34
+ void memorySubsystem.startWarmup();
35
35
  }
package/dist/mcp/agent.js CHANGED
@@ -49,11 +49,18 @@ function initAgentLog(workspaceRoot, slug, agent) {
49
49
  return { logPath: path.relative(workspaceRoot, logFile), logStream };
50
50
  }
51
51
  export function registerAgentTools(server) {
52
- server.tool('kit_trigger_agent', '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff folder (.agent-kit/handoffs/feature-slug/plan).', {
53
- agent: z.enum(['gemini', 'claude', 'codex']).describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
54
- task: z
55
- .string()
56
- .describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")'),
52
+ server.registerTool('kit_trigger_agent', {
53
+ title: 'Delegate to External Agent',
54
+ description: '!Important: Trigger only when user explicitly asks to delegate a task to an external agent CLI (gemini, claude, or codex). The task can be a direct message or a path to a handoff folder (.agent-kit/handoffs/feature-slug/plan).',
55
+ inputSchema: {
56
+ agent: z
57
+ .enum(['gemini', 'claude', 'codex'])
58
+ .describe('The agent CLI to invoke: "gemini", "claude", or "codex"'),
59
+ task: z
60
+ .string()
61
+ .describe('Task message or path to a handoff folder (e.g., ".agent-kit/handoffs/feature-slug/plan")'),
62
+ },
63
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
57
64
  }, async ({ agent, task }, extra) => {
58
65
  const workspaceRoot = getWorkspaceRoot();
59
66
  let job;
package/dist/mcp/core.js CHANGED
@@ -46,14 +46,19 @@ export function registerCoreTools(server) {
46
46
  // Writes handoff artifacts to the workspace as a folder of files
47
47
  // Returns the saved folder path to use in next-step instructions
48
48
  // ═══════════════════════════════════════════════════════════════
49
- server.tool('kit_save_handoff', `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved folder path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`, {
50
- type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
51
- files: z
52
- .record(z.string(), z.string())
53
- .describe('Markdown files to write into the handoff folder. May include one or many files per call; repeated calls with the same type and slug add or overwrite the provided files. Filenames must match /^[A-Za-z0-9_-]+\\.md$/.'),
54
- slug: z
55
- .string()
56
- .describe('Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
49
+ server.registerTool('kit_save_handoff', {
50
+ title: 'Save Handoff Artifact',
51
+ description: `Save a handoff artifact to .agent-kit/handoffs/. Returns the saved folder path to use in next-step instructions. Do NOT append version numbers (v2, v3, etc.) to the slug.`,
52
+ inputSchema: {
53
+ type: z.enum(HANDOFF_TYPES).describe('Handoff type'),
54
+ files: z
55
+ .record(z.string(), z.string())
56
+ .describe('Markdown files to write into the handoff folder. May include one or many files per call; repeated calls with the same type and slug add or overwrite the provided files. Filenames must match /^[A-Za-z0-9_-]+\\.md$/.'),
57
+ slug: z
58
+ .string()
59
+ .describe('Short identifier for the folder, e.g. "user-auth" or "PROJ-123". Do NOT append version numbers (v2, v3, etc.).'),
60
+ },
61
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
57
62
  }, async (args) => handleSaveHandoff(args, getWorkspaceRoot()));
58
63
  }
59
64
  // Export DEFAULT_EXTENSIONS for backward compatibility
@@ -74,14 +74,19 @@ async function callRestApi(url, auth, accept = 'application/json') {
74
74
  }
75
75
  export function registerIntegrationTools(server) {
76
76
  // TOOL: GET BITBUCKET PR
77
- server.tool('kit_get_bitbucket_pr', 'Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.', {
78
- input: z.string().describe('Bitbucket PR URL or numeric PR ID'),
79
- workspace: z
80
- .string()
81
- .optional()
82
- .describe('Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)'),
83
- repoSlug: z.string().optional().describe('Bitbucket repo slug (required for numeric ID)'),
84
- includeDiff: z.boolean().optional().default(true).describe('Include unified diff in response'),
77
+ server.registerTool('kit_get_bitbucket_pr', {
78
+ title: 'Get Bitbucket PR',
79
+ description: 'Get Bitbucket PR details and optionally the diff. Accepts a full PR URL or a numeric PR ID with workspace + repoSlug.',
80
+ inputSchema: {
81
+ input: z.string().describe('Bitbucket PR URL or numeric PR ID'),
82
+ workspace: z
83
+ .string()
84
+ .optional()
85
+ .describe('Bitbucket workspace slug (required for numeric ID if BITBUCKET_DEFAULT_WORKSPACE not set)'),
86
+ repoSlug: z.string().optional().describe('Bitbucket repo slug (required for numeric ID)'),
87
+ includeDiff: z.boolean().optional().default(true).describe('Include unified diff in response'),
88
+ },
89
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
85
90
  }, async ({ input, workspace, repoSlug, includeDiff }) => {
86
91
  try {
87
92
  let ws;
@@ -145,8 +150,13 @@ ${pr.description || 'No description'}`;
145
150
  }
146
151
  });
147
152
  // TOOL: JIRA GET TICKET
148
- server.tool('kit_jira_get_ticket', 'Get ticket details from Jira using the Atlassian REST API', {
149
- ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
153
+ server.registerTool('kit_jira_get_ticket', {
154
+ title: 'Get Jira Ticket',
155
+ description: 'Get ticket details from Jira using the Atlassian REST API',
156
+ inputSchema: {
157
+ ticketId: z.string().describe('Jira ticket ID (e.g., PROJ-123)'),
158
+ },
159
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
150
160
  }, async ({ ticketId }) => {
151
161
  try {
152
162
  const safeTicketId = ticketId.match(/^[A-Z]+-\d+$/)?.[0];
@@ -1,16 +1,11 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { MemoryIndexer } from '../services/memory/indexer.js';
3
- import { MemoryStore } from '../services/memory/store.js';
4
- import type { MemoryConfig } from '../services/memory/types.js';
2
+ import { type MemorySubsystem, type MemorySubsystemOverrides } from '../services/memory/subsystem.js';
5
3
  import { type ProjectSettings } from '../core/config/index.js';
6
4
  /**
7
5
  * Initializes the memory subsystem and registers all memory tools.
8
- * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
6
+ * Returns the MemorySubsystem so the caller can fire startWarmup() after server.connect().
9
7
  * Returns null when memory is disabled (settings.memory.enabled !== true).
10
8
  */
11
- export declare function registerMemoryTools(server: McpServer, workspaceRoot: string, overrides?: {
12
- indexer?: MemoryIndexer;
13
- store?: MemoryStore;
14
- config?: MemoryConfig;
9
+ export declare function registerMemoryTools(server: McpServer, workspaceRoot: string, overrides?: MemorySubsystemOverrides & {
15
10
  settings?: ProjectSettings;
16
- }): MemoryIndexer | null;
11
+ }): MemorySubsystem | null;
@@ -1,29 +1,38 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { z } from 'zod';
4
- import { Embedder } from '../services/memory/embedder.js';
5
- import { MemoryIndexer } from '../services/memory/indexer.js';
6
- import { MemoryStore } from '../services/memory/store.js';
4
+ import { createMemorySubsystem, } from '../services/memory/subsystem.js';
7
5
  import { SOURCE_TYPES } from '../services/memory/types.js';
8
6
  import { initializeConversationDigestModel } from '../services/digest/processor.js';
9
7
  import { DEFAULT_DIGEST_MODEL_ID } from '../services/digest/constants.js';
10
8
  import { formatError, mcpJson, mcpText } from '../utils/utils.js';
11
- import { loadGlobalSettings, resolveConversationDigestConfig, resolveMemoryConfig, } from '../core/config/index.js';
9
+ import { loadGlobalSettings, resolveConversationDigestConfig } from '../core/config/index.js';
12
10
  /**
13
- * Registers tool handlers onto an already-constructed indexer/store pair.
11
+ * Registers tool handlers against the current memory subsystem state.
14
12
  */
15
- function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot = process.cwd()) {
16
- server.tool('kit_memory_search', 'Search persistent memory for factual or semantic matches (decisions, concepts, entities). Extract concise keywords only, removing stopwords and filler words from the user request before querying. Use kit_memory_recent for temporal or recency questions.', {
17
- query: z.string().min(1).describe('Keyword-only search query with stopwords removed'),
18
- top_k: z.number().int().positive().optional().describe('Number of results to return'),
13
+ function registerMemoryToolHandlers(server, subsystem, workspaceRoot = process.cwd()) {
14
+ const { config } = subsystem;
15
+ server.registerTool('kit_memory_search', {
16
+ title: 'Search Memory',
17
+ description: 'Search persistent memory for factual or semantic matches (decisions, concepts, entities). Extract concise keywords only, removing stopwords and filler words from the user request before querying. Use kit_memory_recent for temporal or recency questions.',
18
+ inputSchema: {
19
+ query: z.string().min(1).describe('Keyword-only search query with stopwords removed'),
20
+ top_k: z.number().int().positive().optional().describe('Number of results to return'),
21
+ },
22
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
19
23
  }, async ({ query, top_k }) => {
20
24
  try {
25
+ if (!subsystem.indexer || !subsystem.store) {
26
+ return mcpText(formatInitializationFailure(subsystem));
27
+ }
28
+ const { indexer, store } = subsystem;
21
29
  const results = await indexer.search(query, top_k ?? config.topK);
30
+ const lifecycleNote = formatLifecycleNote(subsystem);
22
31
  const degradedNote = !store.vecAvailable
23
32
  ? '⚠️ Vector search unavailable — showing keyword-only results.\n\n'
24
33
  : '';
25
34
  if (results.length === 0) {
26
- return mcpText(`${degradedNote}No memories found for query: "${query}"`);
35
+ return mcpText(`${lifecycleNote}${degradedNote}No memories found for query: "${query}"`);
27
36
  }
28
37
  const formatted = results
29
38
  .map((r) => {
@@ -34,17 +43,26 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
34
43
  return `### ${displaySource} (score: ${r.score.toFixed(3)})\n${content}`;
35
44
  })
36
45
  .join('\n\n---\n\n');
37
- return mcpText(`${degradedNote}${formatted}`);
46
+ return mcpText(`${lifecycleNote}${degradedNote}${formatted}`);
38
47
  }
39
48
  catch (err) {
40
49
  return mcpText(`kit_memory_search failed: ${formatError(err)}`);
41
50
  }
42
51
  });
43
- server.tool('kit_memory_recent', 'Return the N most recently updated memory sources. Optionally filter by source type. Use for temporal questions like "what did we do last session" or "recent".', {
44
- n: z.number().int().positive().max(50).optional().describe('How many recent sources to return (default 5)'),
45
- source_type: z.enum(SOURCE_TYPES).optional().describe('Optional source type filter'),
52
+ server.registerTool('kit_memory_recent', {
53
+ title: 'Recent Memory Sources',
54
+ description: 'Return the N most recently updated memory sources. Optionally filter by source type. Use for temporal questions like "what did we do last session" or "recent".',
55
+ inputSchema: {
56
+ n: z.number().int().positive().max(50).optional().describe('How many recent sources to return (default 5)'),
57
+ source_type: z.enum(SOURCE_TYPES).optional().describe('Optional source type filter'),
58
+ },
59
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
46
60
  }, async ({ n, source_type }) => {
47
61
  try {
62
+ if (!subsystem.store) {
63
+ return mcpText(formatInitializationFailure(subsystem));
64
+ }
65
+ const store = subsystem.store;
48
66
  const rows = store.getRecentSources({ limit: n ?? 5, sourceType: source_type });
49
67
  if (rows.length === 0)
50
68
  return mcpText('No recent sources found.');
@@ -67,10 +85,22 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
67
85
  return mcpText(`kit_memory_recent failed: ${formatError(err)}`);
68
86
  }
69
87
  });
70
- server.tool('kit_memory_save', 'Save content to wiki/raw for inclusion after the next /wiki compile.', {
71
- content: z.string().min(1).describe('Content to save to memory'),
88
+ server.registerTool('kit_memory_save', {
89
+ title: 'Save to Memory',
90
+ description: 'Save content to wiki/raw for inclusion after the next /wiki compile.',
91
+ inputSchema: {
92
+ content: z.string().min(1).describe('Content to save to memory'),
93
+ },
94
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
72
95
  }, async ({ content }) => {
73
96
  try {
97
+ if (!subsystem.indexer) {
98
+ return mcpJson({
99
+ saved: false,
100
+ error: formatInitializationFailure(subsystem),
101
+ });
102
+ }
103
+ const indexer = subsystem.indexer;
74
104
  await indexer.save(content);
75
105
  return mcpJson({
76
106
  saved: true,
@@ -85,9 +115,14 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
85
115
  });
86
116
  }
87
117
  });
88
- server.tool('kit_memory_digest_init', 'Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.', {
89
- model_id: z.string().optional().describe('Digest model id'),
90
- enabled: z.boolean().optional().describe('Enable or disable conversation digesting'),
118
+ server.registerTool('kit_memory_digest_init', {
119
+ title: 'Initialize Conversation Digest',
120
+ description: 'Initialize or toggle local conversation digesting. Downloads/cache-loads the pinned local model only when explicitly invoked.',
121
+ inputSchema: {
122
+ model_id: z.string().optional().describe('Digest model id'),
123
+ enabled: z.boolean().optional().describe('Enable or disable conversation digesting'),
124
+ },
125
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
91
126
  }, async ({ model_id, enabled }) => {
92
127
  try {
93
128
  const existingConfig = resolveConversationDigestConfig(loadGlobalSettings());
@@ -109,22 +144,31 @@ function registerMemoryToolHandlers(server, indexer, store, config, workspaceRoo
109
144
  }
110
145
  });
111
146
  }
147
+ function formatLifecycleNote(subsystem) {
148
+ if (subsystem.status.state === 'warming' || subsystem.status.state === 'initializing') {
149
+ return '⚠️ Memory index is warming — results may be stale until startup indexing completes.\n\n';
150
+ }
151
+ if (subsystem.status.state === 'degraded') {
152
+ return `⚠️ Memory index is degraded${subsystem.status.error ? `: ${subsystem.status.error}` : ''}.\n\n`;
153
+ }
154
+ return '';
155
+ }
156
+ function formatInitializationFailure(subsystem) {
157
+ const detail = subsystem.status.error ? `: ${subsystem.status.error}` : '';
158
+ return `Memory initialization failed${detail}`;
159
+ }
112
160
  /**
113
161
  * Initializes the memory subsystem and registers all memory tools.
114
- * Returns the MemoryIndexer so the caller can fire startupIndex() after server.connect().
162
+ * Returns the MemorySubsystem so the caller can fire startWarmup() after server.connect().
115
163
  * Returns null when memory is disabled (settings.memory.enabled !== true).
116
164
  */
117
165
  export function registerMemoryTools(server, workspaceRoot, overrides) {
118
166
  const settings = overrides?.settings ?? loadGlobalSettings();
119
167
  if (settings.memory?.enabled !== true)
120
168
  return null;
121
- const config = overrides?.config ?? resolveMemoryConfig(settings, workspaceRoot);
122
- const store = overrides?.store ?? new MemoryStore(path.join(config.wikiDir, 'index.db'), config);
123
- const indexer = overrides?.indexer ??
124
- (() => {
125
- const embedder = new Embedder(config.embeddingModel);
126
- return new MemoryIndexer(store, embedder, config);
127
- })();
128
- registerMemoryToolHandlers(server, indexer, store, config, workspaceRoot);
129
- return indexer;
169
+ const subsystem = createMemorySubsystem(workspaceRoot, { ...overrides, settings });
170
+ if (!subsystem)
171
+ return null;
172
+ registerMemoryToolHandlers(server, subsystem, workspaceRoot);
173
+ return subsystem;
130
174
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,77 @@
1
+ import * as assert from 'node:assert/strict';
2
+ import { describe, test } from 'node:test';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
6
+ import { registerCoreTools } from './core.js';
7
+ import { registerAgentTools } from './agent.js';
8
+ import { registerIntegrationTools } from './integration.js';
9
+ const EXPECTED_NON_MEMORY_TOOLS = [
10
+ 'kit_save_handoff',
11
+ 'kit_trigger_agent',
12
+ 'kit_get_bitbucket_pr',
13
+ 'kit_jira_get_ticket',
14
+ ];
15
+ async function buildClientWithTools() {
16
+ const server = new McpServer({ name: 'test-kit', version: '0.0.0' });
17
+ registerIntegrationTools(server);
18
+ registerCoreTools(server);
19
+ registerAgentTools(server);
20
+ const [serverTransport, clientTransport] = InMemoryTransport.createLinkedPair();
21
+ const client = new Client({ name: 'test-client', version: '0.0.0' });
22
+ await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
23
+ return client;
24
+ }
25
+ describe('MCP tool registration smoke tests', () => {
26
+ test('R1: all 4 non-memory tool names are registered', async () => {
27
+ const client = await buildClientWithTools();
28
+ const { tools } = await client.listTools();
29
+ const names = tools.map((t) => t.name);
30
+ for (const expected of EXPECTED_NON_MEMORY_TOOLS) {
31
+ assert.ok(names.includes(expected), `expected tool "${expected}" to be registered, got: ${names.join(', ')}`);
32
+ }
33
+ });
34
+ test('R2: each non-memory tool has a non-empty description and inputSchema (BC3)', async () => {
35
+ const client = await buildClientWithTools();
36
+ const { tools } = await client.listTools();
37
+ const byName = Object.fromEntries(tools.map((t) => [t.name, t]));
38
+ for (const name of EXPECTED_NON_MEMORY_TOOLS) {
39
+ const tool = byName[name];
40
+ assert.ok(tool, `tool "${name}" not found`);
41
+ assert.ok(tool.description && tool.description.length > 0, `tool "${name}" must have a non-empty description`);
42
+ assert.ok(tool.inputSchema, `tool "${name}" must have an inputSchema`);
43
+ }
44
+ });
45
+ test('R3: kit_trigger_agent (readOnlyHint:false) has correct annotations (BC5)', async () => {
46
+ const client = await buildClientWithTools();
47
+ const { tools } = await client.listTools();
48
+ const tool = tools.find((t) => t.name === 'kit_trigger_agent');
49
+ assert.ok(tool, 'kit_trigger_agent must be registered');
50
+ assert.ok(tool.annotations, 'kit_trigger_agent must have annotations');
51
+ assert.equal(tool.annotations.readOnlyHint, false, 'readOnlyHint must be false');
52
+ assert.equal(tool.annotations.destructiveHint, true, 'destructiveHint must be true');
53
+ assert.equal(tool.annotations.idempotentHint, false, 'idempotentHint must be false');
54
+ assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
55
+ });
56
+ test('R4: kit_save_handoff (readOnlyHint:false) has correct annotations (BC5)', async () => {
57
+ const client = await buildClientWithTools();
58
+ const { tools } = await client.listTools();
59
+ const tool = tools.find((t) => t.name === 'kit_save_handoff');
60
+ assert.ok(tool, 'kit_save_handoff must be registered');
61
+ assert.ok(tool.annotations, 'kit_save_handoff must have annotations');
62
+ assert.equal(tool.annotations.readOnlyHint, false, 'readOnlyHint must be false');
63
+ assert.equal(tool.annotations.destructiveHint, false, 'destructiveHint must be false');
64
+ assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
65
+ assert.equal(tool.annotations.openWorldHint, false, 'openWorldHint must be false');
66
+ });
67
+ test('R5: kit_get_bitbucket_pr (readOnlyHint:true) has correct annotations (BC5)', async () => {
68
+ const client = await buildClientWithTools();
69
+ const { tools } = await client.listTools();
70
+ const tool = tools.find((t) => t.name === 'kit_get_bitbucket_pr');
71
+ assert.ok(tool, 'kit_get_bitbucket_pr must be registered');
72
+ assert.ok(tool.annotations, 'kit_get_bitbucket_pr must have annotations');
73
+ assert.equal(tool.annotations.readOnlyHint, true, 'readOnlyHint must be true');
74
+ assert.equal(tool.annotations.idempotentHint, true, 'idempotentHint must be true');
75
+ assert.equal(tool.annotations.openWorldHint, true, 'openWorldHint must be true');
76
+ });
77
+ });