Refactor mem-search documentation and optimize API tool definitions

- Updated SKILL.md to emphasize batch fetching for observations, clarifying usage and efficiency.
- Removed deprecated tools from mcp-server.ts and streamlined tool definitions for clarity.
- Enhanced formatting in FormattingService.ts for better output readability.
- Adjusted SearchManager.ts to improve result headers and removed unnecessary search tips from combined text.
This commit is contained in:
Alex Newman
2025-12-14 19:23:48 -05:00
parent 01e235c058
commit 29e6441d32
6 changed files with 138 additions and 322 deletions
+50 -243
View File
@@ -30,18 +30,8 @@ const WORKER_BASE_URL = `http://${WORKER_HOST}:${WORKER_PORT}`;
const TOOL_ENDPOINT_MAP: Record<string, string> = {
'search': '/api/search',
'timeline': '/api/timeline',
'decisions': '/api/decisions',
'changes': '/api/changes',
'how_it_works': '/api/how-it-works',
'search_observations': '/api/search/observations',
'search_sessions': '/api/search/sessions',
'search_user_prompts': '/api/search/prompts',
'find_by_concept': '/api/search/by-concept',
'find_by_file': '/api/search/by-file',
'find_by_type': '/api/search/by-type',
'get_recent_context': '/api/context/recent',
'get_context_timeline': '/api/context/timeline',
'get_timeline_by_query': '/api/timeline/by-query',
'progressive_ix': '/api/instructions'
};
@@ -192,24 +182,25 @@ async function verifyWorkerConnection(): Promise<boolean> {
/**
* Tool definitions with HTTP-based handlers
* Descriptions removed - use progressive_ix tool for parameter documentation
*/
const tools = [
{
name: 'search',
description: 'Search observations, sessions, and prompts',
inputSchema: z.object({
query: z.string().optional().describe('Natural language search query for semantic ranking via ChromaDB vector search. Optional - omit for date-filtered queries only (Chroma cannot filter by date, requires direct SQLite).'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED for initial search), "full" for complete details (use only after reviewing index results)'),
type: z.enum(['observations', 'sessions', 'prompts']).optional().describe('Filter by document type (observations, sessions, or prompts). Omit to search all types.'),
obs_type: z.string().optional().describe('Filter observations by type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change). Only applies when type="observations"'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list). Only applies when type="observations"'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match). Only applies when type="observations"'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc').describe('Sort order')
query: z.string().optional(),
format: z.enum(['index', 'full']).default('index'),
type: z.enum(['observations', 'sessions', 'prompts']).optional(),
obs_type: z.string().optional(),
concepts: z.string().optional(),
files: z.string().optional(),
project: z.string().optional(),
dateStart: z.union([z.string(), z.number()]).optional(),
dateEnd: z.union([z.string(), z.number()]).optional(),
limit: z.number().min(1).max(100).default(20),
offset: z.number().min(0).default(0),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('date_desc')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['search'];
@@ -220,195 +211,33 @@ const tools = [
name: 'timeline',
description: 'Get timeline around observation ID or query',
inputSchema: z.object({
query: z.string().optional().describe('Natural language query to find anchor observation (query-based mode). Mutually exclusive with anchor.'),
anchor: z.number().optional().describe('Observation ID to use as anchor (anchor-based mode). Mutually exclusive with query.'),
depth_before: z.number().min(0).max(100).default(10).describe('Number of observations to fetch before anchor'),
depth_after: z.number().min(0).max(100).default(10).describe('Number of observations to fetch after anchor'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter observations by type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name')
query: z.string().optional(),
anchor: z.number().optional(),
depth_before: z.number().min(0).max(100).default(10),
depth_after: z.number().min(0).max(100).default(10),
format: z.enum(['index', 'full']).default('index'),
type: z.string().optional(),
concepts: z.string().optional(),
files: z.string().optional(),
project: z.string().optional()
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['timeline'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'decisions',
description: 'Find architectural and design decisions',
inputSchema: z.object({
query: z.string().describe('Natural language query for finding decisions'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['decisions'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'changes',
description: 'Find code changes and refactorings',
inputSchema: z.object({
query: z.string().describe('Natural language query for finding changes'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['changes'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'how_it_works',
description: 'Understand system architecture',
inputSchema: z.object({
query: z.string().describe('Natural language query for understanding how something works'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['how_it_works'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'search_observations',
description: '[DEPRECATED] Search observations only',
inputSchema: z.object({
query: z.string().optional().describe('Full-text search query (FTS5)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order (relevance only when query provided)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['search_observations'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'search_sessions',
description: '[DEPRECATED] Search sessions only',
inputSchema: z.object({
query: z.string().optional().describe('Full-text search query (FTS5)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order (relevance only when query provided)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['search_sessions'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'search_user_prompts',
description: '[DEPRECATED] Search prompts only',
inputSchema: z.object({
query: z.string().optional().describe('Full-text search query (FTS5)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['relevance', 'date_desc', 'date_asc']).default('relevance').describe('Sort order (relevance only when query provided)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['search_user_prompts'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'find_by_concept',
description: 'Find observations by concept tags',
inputSchema: z.object({
concepts: z.string().describe('Concept tag(s) to filter by (single value or comma-separated list)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['find_by_concept'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'find_by_file',
description: 'Find observations by file paths',
inputSchema: z.object({
files: z.string().describe('File path(s) to filter by (single value or comma-separated list for partial match)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['find_by_file'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'find_by_type',
description: 'Find observations by type',
inputSchema: z.object({
type: z.string().describe('Observation type(s) to filter by (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)'),
limit: z.number().min(1).max(100).default(20).describe('Maximum number of results'),
offset: z.number().min(0).default(0).describe('Number of results to skip'),
orderBy: z.enum(['date_desc', 'date_asc']).default('date_desc').describe('Sort order')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['find_by_type'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'get_recent_context',
description: 'Get recent timeline items',
inputSchema: z.object({
limit: z.number().min(1).max(100).default(30).describe('Maximum number of timeline items to return'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)')
limit: z.number().min(1).max(100).default(30),
format: z.enum(['index', 'full']).default('index'),
type: z.string().optional(),
concepts: z.string().optional(),
files: z.string().optional(),
project: z.string().optional(),
dateStart: z.union([z.string(), z.number()]).optional(),
dateEnd: z.union([z.string(), z.number()]).optional()
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['get_recent_context'];
@@ -419,47 +248,25 @@ const tools = [
name: 'get_context_timeline',
description: 'Get timeline around specific observation',
inputSchema: z.object({
anchor: z.number().describe('Observation ID to use as anchor point'),
depth_before: z.number().min(0).max(100).default(10).describe('Number of observations to fetch before anchor'),
depth_after: z.number().min(0).max(100).default(10).describe('Number of observations to fetch after anchor'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name')
anchor: z.number(),
depth_before: z.number().min(0).max(100).default(10),
depth_after: z.number().min(0).max(100).default(10),
format: z.enum(['index', 'full']).default('index'),
type: z.string().optional(),
concepts: z.string().optional(),
files: z.string().optional(),
project: z.string().optional()
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['get_context_timeline'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'get_timeline_by_query',
description: 'Search and get timeline in one call',
inputSchema: z.object({
query: z.string().describe('Natural language query to find anchor observation'),
depth_before: z.number().min(0).max(100).default(10).describe('Number of observations to fetch before anchor'),
depth_after: z.number().min(0).max(100).default(10).describe('Number of observations to fetch after anchor'),
format: z.enum(['index', 'full']).default('index').describe('Output format: "index" for titles/dates only (default, RECOMMENDED), "full" for complete details'),
type: z.string().optional().describe('Filter by observation type (single value or comma-separated list: decision,bugfix,feature,refactor,discovery,change)'),
concepts: z.string().optional().describe('Filter by concept tags (single value or comma-separated list)'),
files: z.string().optional().describe('Filter by file paths (single value or comma-separated list for partial match)'),
project: z.string().optional().describe('Filter by project name'),
dateStart: z.union([z.string(), z.number()]).optional().describe('Start date for filtering (ISO string or epoch timestamp)'),
dateEnd: z.union([z.string(), z.number()]).optional().describe('End date for filtering (ISO string or epoch timestamp)')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['get_timeline_by_query'];
return await callWorkerAPI(endpoint, args);
}
},
{
name: 'progressive_ix',
description: 'Load detailed instructions for mem-search tools',
description: 'Load parameter docs and usage instructions',
inputSchema: z.object({
topic: z.enum(['workflow', 'search_params', 'examples', 'all'])
.default('all')
.describe('Which instruction section to load: workflow (4-step process), search_params (parameters reference), examples (usage examples), all (complete guide)')
topic: z.enum(['workflow', 'search_params', 'examples', 'all']).default('all')
}),
handler: async (args: any) => {
const endpoint = TOOL_ENDPOINT_MAP['progressive_ix'];
@@ -468,9 +275,9 @@ const tools = [
},
{
name: 'get_observation',
description: 'Get full details for a single observation by ID',
description: 'Get observation by ID',
inputSchema: z.object({
id: z.number().describe('Observation ID from search/timeline results')
id: z.number()
}),
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/observation', args.id);
@@ -478,12 +285,12 @@ const tools = [
},
{
name: 'get_batch_observations',
description: 'Get full details for multiple observations by IDs in one request',
description: 'Get multiple observations by IDs',
inputSchema: z.object({
ids: z.array(z.number()).describe('Array of observation IDs to fetch'),
orderBy: z.enum(['date_desc', 'date_asc']).optional().describe('Sort order for results'),
limit: z.number().optional().describe('Maximum number of results to return'),
project: z.string().optional().describe('Filter by project name')
ids: z.array(z.number()),
orderBy: z.enum(['date_desc', 'date_asc']).optional(),
limit: z.number().optional(),
project: z.string().optional()
}),
handler: async (args: any) => {
return await callWorkerAPIPost('/api/observations/batch', args);
@@ -491,9 +298,9 @@ const tools = [
},
{
name: 'get_session',
description: 'Get full session summary by ID',
description: 'Get session summary by ID',
inputSchema: z.object({
id: z.number().describe('Session ID (just the number from S1234)')
id: z.number()
}),
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/session', args.id);
@@ -503,7 +310,7 @@ const tools = [
name: 'get_prompt',
description: 'Get user prompt by ID',
inputSchema: z.object({
id: z.number().describe('Prompt ID from search results')
id: z.number()
}),
handler: async (args: any) => {
return await callWorkerAPIWithPath('/api/prompt', args.id);