From ca4c90920e748e242eeacf862ca3ba46078b0ba6 Mon Sep 17 00:00:00 2001 From: srstomp Date: Sun, 26 Apr 2026 20:35:40 +0200 Subject: [PATCH] feat: reduce MCP list endpoint context --- packages/ohno-core/src/db.test.ts | 121 ++++++++++++++++++- packages/ohno-core/src/db.ts | 49 ++++++-- packages/ohno-core/src/types.test.ts | 15 +++ packages/ohno-core/src/types.ts | 12 +- packages/ohno-mcp/src/server.test.ts | 173 ++++++++++++++++++++++++++- packages/ohno-mcp/src/server.ts | 28 +++-- 6 files changed, 373 insertions(+), 25 deletions(-) diff --git a/packages/ohno-core/src/db.test.ts b/packages/ohno-core/src/db.test.ts index a524e64..c073ad4 100644 --- a/packages/ohno-core/src/db.test.ts +++ b/packages/ohno-core/src/db.test.ts @@ -206,6 +206,34 @@ describe("TaskDatabase", () => { expect(tasks.length).toBe(2); }); + it("should respect offset", () => { + const page1 = db.getTasks({ limit: 2, offset: 0 }); + const page2 = db.getTasks({ limit: 2, offset: 2 }); + + expect(page1.length).toBe(2); + expect(page2.length).toBe(1); + expect(page1.map((task) => task.id)).not.toContain(page2[0].id); + }); + + it("should search by title case-insensitively", () => { + db.createTask({ title: "SQLite migration follow-up" }); + db.createTask({ title: "Unrelated cleanup" }); + + const tasks = db.getTasks({ search: "migration" }); + + expect(tasks.map((task) => task.title)).toEqual(["SQLite migration follow-up"]); + }); + + it("should default to a compact limit", () => { + for (let i = 0; i < 20; i++) { + db.createTask({ title: `Bulk task ${i}` }); + } + + const tasks = db.getTasks(); + + expect(tasks.length).toBe(10); + }); + describe("field selection", () => { it("should return minimal fields by default", () => { db.createTask({ title: "Test task", description: "A long description" }); @@ -942,6 +970,51 @@ describe("TaskDatabase", () => { }); }); + describe("Epic list queries", () => { + it("should search epics by title case-insensitively", () => { + db.createEpic({ title: "Migration epic", description: "Large epic description" }); + db.createEpic({ title: "Cleanup epic", description: "Other large description" }); + + const epics = db.getEpics({ search: "migration" }); + + expect(epics.map((epic) => epic.title)).toEqual(["Migration epic"]); + }); + + it("should default to a compact limit", () => { + for (let i = 0; i < 15; i++) { + db.createEpic({ title: `Bulk epic ${i}` }); + } + + const epics = db.getEpics(); + + expect(epics.length).toBe(10); + }); + + describe("field selection", () => { + it("should return minimal fields by default", () => { + db.createEpic({ title: "Short epic", description: "Large epic description", priority: "P0" }); + + const epics = db.getEpics(); + + expect(epics[0].id).toBeDefined(); + expect(epics[0].title).toBe("Short epic"); + expect(epics[0].priority).toBe("P0"); + expect(epics[0].status).toBe("todo"); + expect(epics[0].description).toBeUndefined(); + expect(epics[0].created_at).toBeUndefined(); + }); + + it("should return description when standard fields are requested", () => { + db.createEpic({ title: "Detailed epic", description: "Useful detail" }); + + const epics = db.getEpics({ fields: "standard" }); + + expect(epics[0].description).toBe("Useful detail"); + expect(epics[0].updated_at).toBeDefined(); + }); + }); + }); + describe("Story CRUD Operations", () => { describe("createStory", () => { it("should create a story with minimal options", () => { @@ -1177,6 +1250,48 @@ describe("TaskDatabase", () => { expect(stories.length).toBe(2); }); + it("should search stories by title case-insensitively", () => { + db.createStory({ title: "Migration story", description: "Large story description" }); + db.createStory({ title: "Cleanup story", description: "Other large description" }); + + const stories = db.getStories({ search: "migration" }); + + expect(stories.map((story) => story.title)).toEqual(["Migration story"]); + }); + + it("should default to a compact limit", () => { + for (let i = 0; i < 15; i++) { + db.createStory({ title: `Bulk story ${i}` }); + } + + const stories = db.getStories(); + + expect(stories.length).toBe(10); + }); + + describe("field selection", () => { + it("should return minimal fields by default", () => { + db.createStory({ title: "Short story", description: "Large story description" }); + + const stories = db.getStories(); + + expect(stories[0].id).toBeDefined(); + expect(stories[0].title).toBe("Short story"); + expect(stories[0].status).toBe("todo"); + expect(stories[0].description).toBeUndefined(); + expect(stories[0].created_at).toBeUndefined(); + }); + + it("should return description when standard fields are requested", () => { + db.createStory({ title: "Detailed story", description: "Useful detail" }); + + const stories = db.getStories({ fields: "standard" }); + + expect(stories[0].description).toBe("Useful detail"); + expect(stories[0].updated_at).toBeDefined(); + }); + }); + it("should respect offset option", () => { const story1 = db.createStory({ title: "Story 1" }); const story2 = db.createStory({ title: "Story 2" }); @@ -1216,7 +1331,7 @@ describe("TaskDatabase", () => { description: "A description", }); - const stories = db.getStories(); + const stories = db.getStories({ fields: "full" }); expect(stories.length).toBe(1); const story = stories[0]; @@ -1232,7 +1347,7 @@ describe("TaskDatabase", () => { it("should handle null epic_id in returned stories", () => { const storyId = db.createStory({ title: "Orphan Story" }); - const stories = db.getStories(); + const stories = db.getStories({ fields: "full" }); expect(stories.length).toBe(1); expect(stories[0].epic_id).toBeNull(); }); @@ -1240,7 +1355,7 @@ describe("TaskDatabase", () => { it("should handle null description in returned stories", () => { const storyId = db.createStory({ title: "No Description" }); - const stories = db.getStories(); + const stories = db.getStories({ fields: "full" }); expect(stories.length).toBe(1); expect(stories[0].description).toBeNull(); }); diff --git a/packages/ohno-core/src/db.ts b/packages/ohno-core/src/db.ts index db8cbe3..1fee7ba 100644 --- a/packages/ohno-core/src/db.ts +++ b/packages/ohno-core/src/db.ts @@ -70,6 +70,18 @@ const SQLITE_BUSY = 5; const SQLITE_BUSY_SNAPSHOT = 517; // SQLITE_BUSY | (2 << 8) const SQLITE_LOCKED = 6; +const STORY_FIELD_SETS: Record = { + minimal: ["id", "title", "status", "epic_id"], + standard: ["id", "title", "status", "epic_id", "description", "created_at", "updated_at"], + full: ["id", "epic_id", "title", "description", "status", "created_at", "updated_at"], +}; + +const EPIC_FIELD_SETS: Record = { + minimal: ["id", "title", "status", "priority"], + standard: ["id", "project_id", "title", "description", "priority", "status", "created_at", "updated_at"], + full: ["id", "project_id", "title", "description", "priority", "status", "created_at", "updated_at"], +}; + /** * Typed error for database lock contention that is recoverable by the user. * Thrown when node:sqlite reports SQLITE_BUSY or SQLITE_BUSY_SNAPSHOT. @@ -301,7 +313,7 @@ export class TaskDatabase { * Get tasks with optional filtering */ getTasks(opts: GetTasksOptions = {}): Task[] { - const { status, epic_id, priority, story_status, epic_status, limit = 50, fields = "minimal" } = opts; + const { status, epic_id, priority, story_status, epic_status, limit = 10, offset = 0, fields = "minimal", search } = opts; // Build SELECT clause based on fields parameter const fieldSet = FIELD_SETS[fields] || FIELD_SETS.minimal; @@ -339,10 +351,15 @@ export class TaskDatabase { params.push(epic_status); } + if (search) { + conditions.push("LOWER(t.title) LIKE LOWER(?)"); + params.push(`%${search}%`); + } + sql += ` WHERE ${conditions.join(" AND ")}`; sql += " ORDER BY CASE t.status WHEN 'in_progress' THEN 0 WHEN 'review' THEN 1 WHEN 'blocked' THEN 2 WHEN 'todo' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, t.updated_at DESC, t.created_at DESC"; - sql += " LIMIT ?"; - params.push(limit); + sql += " LIMIT ? OFFSET ?"; + params.push(limit, offset); return this.db.prepare(sql).all(...(params as never[])) as unknown as Task[]; } @@ -351,7 +368,7 @@ export class TaskDatabase { * Count tasks matching the given filters (ignores limit) */ countTasks(opts: GetTasksOptions = {}): number { - const { status, epic_id, priority, story_status, epic_status } = opts; + const { status, epic_id, priority, story_status, epic_status, search } = opts; let sql = `SELECT COUNT(*) as count FROM tasks t LEFT JOIN stories s ON t.story_id = s.id @@ -380,6 +397,10 @@ export class TaskDatabase { conditions.push("e.status = ?"); params.push(epic_status); } + if (search) { + conditions.push("LOWER(t.title) LIKE LOWER(?)"); + params.push(`%${search}%`); + } sql += ` WHERE ${conditions.join(" AND ")}`; @@ -754,9 +775,10 @@ export class TaskDatabase { * Get stories with optional filtering */ getStories(opts: GetStoriesOptions = {}): Story[] { - const { epic_id, status, limit = 50, offset = 0 } = opts; + const { epic_id, status, limit = 10, offset = 0, fields = "minimal", search } = opts; - let sql = "SELECT * FROM stories"; + const fieldSet = STORY_FIELD_SETS[fields] || STORY_FIELD_SETS.minimal; + let sql = `SELECT ${fieldSet.join(", ")} FROM stories`; const conditions: string[] = []; const params: unknown[] = []; @@ -775,6 +797,11 @@ export class TaskDatabase { params.push(status); } + if (search) { + conditions.push("LOWER(title) LIKE LOWER(?)"); + params.push(`%${search}%`); + } + if (conditions.length > 0) { sql += ` WHERE ${conditions.join(" AND ")}`; } @@ -798,9 +825,10 @@ export class TaskDatabase { * Get epics with optional filtering */ getEpics(opts: GetEpicsOptions = {}): Epic[] { - const { status, priority, limit = 50 } = opts; + const { status, priority, limit = 10, fields = "minimal", search } = opts; - let sql = "SELECT * FROM epics"; + const fieldSet = EPIC_FIELD_SETS[fields] || EPIC_FIELD_SETS.minimal; + let sql = `SELECT ${fieldSet.join(", ")} FROM epics`; const conditions: string[] = []; const params: unknown[] = []; @@ -814,6 +842,11 @@ export class TaskDatabase { params.push(priority); } + if (search) { + conditions.push("LOWER(title) LIKE LOWER(?)"); + params.push(`%${search}%`); + } + if (conditions.length > 0) { sql += ` WHERE ${conditions.join(" AND ")}`; } diff --git a/packages/ohno-core/src/types.test.ts b/packages/ohno-core/src/types.test.ts index c4be0d6..5a6601a 100644 --- a/packages/ohno-core/src/types.test.ts +++ b/packages/ohno-core/src/types.test.ts @@ -32,6 +32,17 @@ describe("Story Types", () => { expect(story.status).toBe("todo"); }); + it("should allow minimal list response fields", () => { + const story: Story = { + id: "story-123", + epic_id: "epic-456", + title: "User Authentication", + status: "todo", + }; + + expect(story.description).toBeUndefined(); + }); + it("should allow null epic_id", () => { const story: Story = { id: "story-123", @@ -146,12 +157,16 @@ describe("Story Types", () => { status: "done", limit: 5, offset: 0, + fields: "minimal", + search: "migration", }; expect(options.epic_id).toBe("epic-456"); expect(options.status).toBe("done"); expect(options.limit).toBe(5); expect(options.offset).toBe(0); + expect(options.fields).toBe("minimal"); + expect(options.search).toBe("migration"); }); }); }); diff --git a/packages/ohno-core/src/types.ts b/packages/ohno-core/src/types.ts index 0e34a4d..5b6e2fd 100644 --- a/packages/ohno-core/src/types.ts +++ b/packages/ohno-core/src/types.ts @@ -211,10 +211,10 @@ export interface Story { id: string; epic_id: string | null; title: string; - description: string | null; + description?: string | null; status: StoryStatus; - created_at: string; - updated_at: string; + created_at?: string; + updated_at?: string; } /** @@ -249,6 +249,8 @@ export interface GetEpicsOptions { status?: TaskStatus; priority?: Priority; limit?: number; + fields?: FieldSet; + search?: string; } /** @@ -259,6 +261,8 @@ export interface GetStoriesOptions { status?: StoryStatus; limit?: number; offset?: number; + fields?: FieldSet; + search?: string; } /** @@ -271,7 +275,9 @@ export interface GetTasksOptions { story_status?: TaskStatus; epic_status?: TaskStatus; limit?: number; + offset?: number; fields?: FieldSet; + search?: string; } /** diff --git a/packages/ohno-mcp/src/server.test.ts b/packages/ohno-mcp/src/server.test.ts index 2cf8a1e..db325e7 100644 --- a/packages/ohno-mcp/src/server.test.ts +++ b/packages/ohno-mcp/src/server.test.ts @@ -117,7 +117,7 @@ describe("MCP Server", () => { describe("GetTasksSchema", () => { it("should accept empty object", () => { const result = GetTasksSchema.parse({}); - expect(result.limit).toBe(50); // default + expect(result.limit).toBe(10); // default }); it("should accept valid status", () => { @@ -143,6 +143,21 @@ describe("MCP Server", () => { expect(result.limit).toBe(10); }); + it("should default to a compact limit", () => { + const result = GetTasksSchema.parse({}); + expect(result.limit).toBe(10); + }); + + it("should accept offset and search", () => { + const result = GetTasksSchema.parse({ offset: 20, search: "migration" }); + expect(result.offset).toBe(20); + expect(result.search).toBe("migration"); + }); + + it("should reject negative offset", () => { + expect(() => GetTasksSchema.parse({ offset: -1 })).toThrow(ZodError); + }); + it("should reject limit below minimum", () => { expect(() => GetTasksSchema.parse({ limit: 0 })).toThrow(ZodError); }); @@ -352,8 +367,9 @@ describe("MCP Server", () => { describe("GetStoriesSchema", () => { it("should accept empty object with defaults", () => { const result = GetStoriesSchema.parse({}); - expect(result.limit).toBe(50); + expect(result.limit).toBe(10); expect(result.offset).toBe(0); + expect(result.fields).toBe("minimal"); }); it("should accept epic_id filter", () => { @@ -398,6 +414,21 @@ describe("MCP Server", () => { expect(result.offset).toBe(10); }); + it("should accept fields parameter with valid values", () => { + expect(() => GetStoriesSchema.parse({ fields: "minimal" })).not.toThrow(); + expect(() => GetStoriesSchema.parse({ fields: "standard" })).not.toThrow(); + expect(() => GetStoriesSchema.parse({ fields: "full" })).not.toThrow(); + }); + + it("should reject invalid fields values", () => { + expect(() => GetStoriesSchema.parse({ fields: "invalid" })).toThrow(ZodError); + }); + + it("should accept search", () => { + const result = GetStoriesSchema.parse({ search: "migration" }); + expect(result.search).toBe("migration"); + }); + it("should reject negative offset", () => { expect(() => GetStoriesSchema.parse({ offset: -1 })).toThrow(ZodError); }); @@ -408,11 +439,15 @@ describe("MCP Server", () => { status: "todo", limit: 20, offset: 5, + fields: "standard", + search: "migration", }); expect(result.epic_id).toBe("epic-1"); expect(result.status).toBe("todo"); expect(result.limit).toBe(20); expect(result.offset).toBe(5); + expect(result.fields).toBe("standard"); + expect(result.search).toBe("migration"); }); }); @@ -658,7 +693,8 @@ describe("MCP Server", () => { describe("GetEpicsSchema", () => { it("should accept empty object with defaults", () => { const result = GetEpicsSchema.parse({}); - expect(result.limit).toBe(50); + expect(result.limit).toBe(10); + expect(result.fields).toBe("minimal"); }); it("should accept status filter", () => { @@ -676,6 +712,21 @@ describe("MCP Server", () => { expect(result.limit).toBe(25); }); + it("should accept fields parameter with valid values", () => { + expect(() => GetEpicsSchema.parse({ fields: "minimal" })).not.toThrow(); + expect(() => GetEpicsSchema.parse({ fields: "standard" })).not.toThrow(); + expect(() => GetEpicsSchema.parse({ fields: "full" })).not.toThrow(); + }); + + it("should reject invalid fields values", () => { + expect(() => GetEpicsSchema.parse({ fields: "invalid" })).toThrow(ZodError); + }); + + it("should accept search", () => { + const result = GetEpicsSchema.parse({ search: "migration" }); + expect(result.search).toBe("migration"); + }); + it("should validate limit bounds", () => { expect(() => GetEpicsSchema.parse({ limit: 0 })).toThrow(ZodError); expect(() => GetEpicsSchema.parse({ limit: 101 })).toThrow(ZodError); @@ -1032,6 +1083,37 @@ describe("MCP Server", () => { expect(result.tasks[0].title).toBe("In progress"); }); + it("should search by title", async () => { + db.createTask({ title: "SQLite migration" }); + db.createTask({ title: "Unrelated cleanup" }); + + const result = await handleTool("get_tasks", { search: "migration" }) as { + tasks: Array<{ title: string }>; + }; + + expect(result.tasks.map((task) => task.title)).toEqual(["SQLite migration"]); + }); + + it("should respect offset parameter", async () => { + for (let i = 0; i < 12; i++) { + db.createTask({ title: `Task ${i}` }); + } + + const result = await handleTool("get_tasks", { limit: 10, offset: 10 }) as { tasks: unknown[] }; + + expect(result.tasks.length).toBe(2); + }); + + it("should default to a compact limit", async () => { + for (let i = 0; i < 12; i++) { + db.createTask({ title: `Task ${i}` }); + } + + const result = await handleTool("get_tasks", {}) as { tasks: unknown[] }; + + expect(result.tasks.length).toBe(10); + }); + describe("field selection", () => { it("should return minimal fields by default", async () => { db.createTask({ title: "Test", description: "Long description here" }); @@ -1379,6 +1461,16 @@ describe("MCP Server", () => { expect(result.stories.length).toBe(5); }); + it("should default to a compact limit", async () => { + for (let i = 0; i < 12; i++) { + db.createStory({ title: `Story ${i}` }); + } + + const result = await handleTool("list_stories", {}) as { stories: unknown[] }; + + expect(result.stories.length).toBe(10); + }); + it("should respect offset parameter", async () => { for (let i = 0; i < 10; i++) { db.createStory({ title: `Story ${i}` }); @@ -1388,6 +1480,38 @@ describe("MCP Server", () => { expect(result.stories.length).toBe(2); }); + it("should search by title", async () => { + db.createStory({ title: "Migration story", description: "Large description" }); + db.createStory({ title: "Cleanup story", description: "Other description" }); + + const result = await handleTool("list_stories", { search: "migration" }) as { + stories: Array<{ title: string }>; + }; + + expect(result.stories.map((story) => story.title)).toEqual(["Migration story"]); + }); + + it("should return minimal fields by default", async () => { + db.createStory({ title: "Lean story", description: "Large description" }); + + const result = await handleTool("list_stories", {}) as { + stories: Array>; + }; + + expect(result.stories[0].title).toBe("Lean story"); + expect(result.stories[0].description).toBeUndefined(); + }); + + it("should return description when standard fields are requested", async () => { + db.createStory({ title: "Detailed story", description: "Useful detail" }); + + const result = await handleTool("list_stories", { fields: "standard" }) as { + stories: Array>; + }; + + expect(result.stories[0].description).toBe("Useful detail"); + }); + it("should combine filters", async () => { const dbInstance = db as unknown as { db: DatabaseSync }; dbInstance.db.prepare( @@ -1406,6 +1530,7 @@ describe("MCP Server", () => { epic_id: "epic-1", status: "in_progress", limit: 10, + search: "Story", }) as { stories: Array<{ status: string }> }; expect(result.stories.length).toBe(2); @@ -1617,6 +1742,48 @@ describe("MCP Server", () => { const result = await handleTool("get_epics", { limit: 5 }) as { epics: unknown[] }; expect(result.epics.length).toBe(5); }); + + it("should default to a compact limit", async () => { + for (let i = 0; i < 12; i++) { + db.createEpic({ title: `Epic ${i}` }); + } + + const result = await handleTool("get_epics", {}) as { epics: unknown[] }; + + expect(result.epics.length).toBe(10); + }); + + it("should search by title", async () => { + db.createEpic({ title: "Migration epic", description: "Large description" }); + db.createEpic({ title: "Cleanup epic", description: "Other description" }); + + const result = await handleTool("get_epics", { search: "migration" }) as { + epics: Array<{ title: string }>; + }; + + expect(result.epics.map((epic) => epic.title)).toEqual(["Migration epic"]); + }); + + it("should return minimal fields by default", async () => { + db.createEpic({ title: "Lean epic", description: "Large description" }); + + const result = await handleTool("get_epics", {}) as { + epics: Array>; + }; + + expect(result.epics[0].title).toBe("Lean epic"); + expect(result.epics[0].description).toBeUndefined(); + }); + + it("should return description when standard fields are requested", async () => { + db.createEpic({ title: "Detailed epic", description: "Useful detail" }); + + const result = await handleTool("get_epics", { fields: "standard" }) as { + epics: Array>; + }; + + expect(result.epics[0].description).toBe("Useful detail"); + }); }); describe("update_epic", () => { diff --git a/packages/ohno-mcp/src/server.ts b/packages/ohno-mcp/src/server.ts index 14d5fa5..2cf49e7 100644 --- a/packages/ohno-mcp/src/server.ts +++ b/packages/ohno-mcp/src/server.ts @@ -22,8 +22,10 @@ const GetTasksSchema = z.object({ priority: z.enum(["P0", "P1", "P2", "P3"]).optional(), story_status: z.enum(["todo", "in_progress", "review", "done", "blocked"]).optional(), epic_status: z.enum(["todo", "in_progress", "review", "done", "blocked"]).optional(), - limit: z.number().min(1).max(100).default(50), + limit: z.number().min(1).max(100).default(10), + offset: z.number().min(0).default(0), fields: z.enum(["minimal", "standard", "full"]).default("minimal"), + search: z.string().min(1).optional(), }); const TaskIdSchema = z.object({ @@ -147,8 +149,10 @@ const StoryIdSchema = z.object({ const GetStoriesSchema = z.object({ epic_id: z.string().optional(), status: z.enum(["todo", "in_progress", "done"]).optional(), - limit: z.number().min(1).max(100).default(50), + limit: z.number().min(1).max(100).default(10), offset: z.number().min(0).default(0), + fields: z.enum(["minimal", "standard", "full"]).default("minimal"), + search: z.string().min(1).optional(), }); const UpdateStorySchema = z.object({ @@ -170,7 +174,9 @@ const UpdateEpicSchema = z.object({ const GetEpicsSchema = z.object({ status: z.enum(["todo", "in_progress", "review", "done", "blocked"]).optional(), priority: z.enum(["P0", "P1", "P2", "P3"]).optional(), - limit: z.number().min(1).max(100).default(50), + limit: z.number().min(1).max(100).default(10), + fields: z.enum(["minimal", "standard", "full"]).default("minimal"), + search: z.string().min(1).optional(), }); const KanbanBoardSchema = z.object({ @@ -225,8 +231,10 @@ const TOOLS = [ priority: { type: "string", enum: ["P0", "P1", "P2", "P3"], description: "Filter by epic priority" }, story_status: { type: "string", enum: ["todo", "in_progress", "review", "done", "blocked"], description: "Filter by parent story status" }, epic_status: { type: "string", enum: ["todo", "in_progress", "review", "done", "blocked"], description: "Filter by parent epic status" }, - limit: { type: "number", description: "Maximum tasks to return (1-100)", default: 50 }, + limit: { type: "number", description: "Maximum tasks to return (1-100)", default: 10 }, + offset: { type: "number", description: "Number of tasks to skip", default: 0 }, fields: { type: "string", enum: ["minimal", "standard", "full"], description: "Field set to return: minimal (default, for selection), standard (with descriptions), full (all fields)", default: "minimal" }, + search: { type: "string", description: "Case-insensitive title substring search" }, }, }, }, @@ -395,14 +403,16 @@ const TOOLS = [ }, { name: "list_stories", - description: "List stories with optional filtering by epic and status", + description: "List stories with optional filtering by epic, status, and title. Returns minimal fields by default for efficiency.", inputSchema: { type: "object" as const, properties: { epic_id: { type: "string", description: "Filter by epic ID" }, status: { type: "string", enum: ["todo", "in_progress", "done"], description: "Filter by story status" }, - limit: { type: "number", description: "Maximum stories to return (1-100)", default: 50 }, + limit: { type: "number", description: "Maximum stories to return (1-100)", default: 10 }, offset: { type: "number", description: "Number of stories to skip", default: 0 }, + fields: { type: "string", enum: ["minimal", "standard", "full"], description: "Field set to return: minimal (default, no descriptions), standard (with descriptions), full (all fields)", default: "minimal" }, + search: { type: "string", description: "Case-insensitive title substring search" }, }, }, }, @@ -448,13 +458,15 @@ const TOOLS = [ }, { name: "get_epics", - description: "List epics with optional filtering by status and priority", + description: "List epics with optional filtering by status, priority, and title. Returns minimal fields by default for efficiency.", inputSchema: { type: "object" as const, properties: { status: { type: "string", enum: ["todo", "in_progress", "review", "done", "blocked"], description: "Filter by status" }, priority: { type: "string", enum: ["P0", "P1", "P2", "P3"], description: "Filter by priority" }, - limit: { type: "number", description: "Maximum epics to return (1-100)", default: 50 }, + limit: { type: "number", description: "Maximum epics to return (1-100)", default: 10 }, + fields: { type: "string", enum: ["minimal", "standard", "full"], description: "Field set to return: minimal (default, no descriptions), standard (with descriptions), full (all fields)", default: "minimal" }, + search: { type: "string", description: "Case-insensitive title substring search" }, }, }, },