Registers openproject_search_work_packages (readOnly: true), taking the tool registry from 18 to 19. searchWorkPackages now unions a precise server-side subject filter with a broad recency-ordered candidate window, ranks via the shared search pipeline, and deep-enriches top candidates with description and comment text. Exact-mode requests skip the broad query entirely. searchWorkPackages's return type changes from a flat PaginatedResult<WorkPackageSummary> to WorkPackageSearchPage, nesting the summary under `workPackage` alongside score, matchedFields, and an optional snippet. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
312 lines
8.3 KiB
TypeScript
312 lines
8.3 KiB
TypeScript
/**
|
|
* Work Package Tools for OpenProject MCP.
|
|
* Implements openproject_list_work_packages and openproject_get_work_package tools.
|
|
*/
|
|
|
|
import { z } from "zod";
|
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import {
|
|
formatToolSuccess,
|
|
formatToolError,
|
|
registerTool,
|
|
type McpToolResponse,
|
|
type ToolDefinition,
|
|
} from "./common";
|
|
import {
|
|
listWorkPackages,
|
|
getWorkPackage,
|
|
listWorkPackageActivities,
|
|
searchWorkPackages,
|
|
} from "../services/work-packages";
|
|
import type { FilterElement } from "../client/types";
|
|
|
|
export const listWorkPackagesShape = {
|
|
projectId: z
|
|
.union([z.number().int().positive(), z.string().min(1)])
|
|
.optional()
|
|
.describe(
|
|
"Scope work packages to a specific project by numeric ID or slug identifier"
|
|
),
|
|
status: z
|
|
.union([z.enum(["open", "closed"]), z.number().int().positive()])
|
|
.optional()
|
|
.describe("Filter by status: 'open', 'closed', or numeric status ID"),
|
|
type: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.describe(
|
|
"Filter by work package type ID (e.g. 1 for Task, 2 for Milestone)"
|
|
),
|
|
assigneeId: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.describe("Filter by assignee user ID"),
|
|
subject: z
|
|
.string()
|
|
.optional()
|
|
.describe("Filter by substring search in work package subject"),
|
|
pageSize: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(100)
|
|
.optional()
|
|
.describe("Number of items per page (max 100, default 20)"),
|
|
offset: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.describe("Page number to retrieve (1-based, default 1)"),
|
|
sortBy: z
|
|
.string()
|
|
.optional()
|
|
.describe("Sorting criteria (e.g. 'id:asc', 'updatedAt:desc')"),
|
|
filters: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Optional raw OpenProject JSON filter string array overriding individual filters"
|
|
),
|
|
};
|
|
|
|
export type ListWorkPackagesArgs = z.infer<
|
|
z.ZodObject<typeof listWorkPackagesShape>
|
|
>;
|
|
|
|
/**
|
|
* Tool execution handler for openproject_list_work_packages.
|
|
*/
|
|
export async function handleListWorkPackages(
|
|
args: ListWorkPackagesArgs
|
|
): Promise<McpToolResponse> {
|
|
try {
|
|
let customFilters: FilterElement[] | undefined;
|
|
if (args.filters) {
|
|
const parsed = JSON.parse(args.filters);
|
|
if (!Array.isArray(parsed)) {
|
|
throw new Error("Filters must be a JSON array");
|
|
}
|
|
customFilters = parsed as FilterElement[];
|
|
}
|
|
|
|
const result = await listWorkPackages({
|
|
projectId: args.projectId,
|
|
status: args.status,
|
|
typeId: args.type,
|
|
assigneeId: args.assigneeId,
|
|
subject: args.subject,
|
|
pageSize: args.pageSize,
|
|
offset: args.offset,
|
|
sortBy: args.sortBy,
|
|
customFilters,
|
|
});
|
|
|
|
return formatToolSuccess({
|
|
workPackages: result.items,
|
|
total: result.total,
|
|
count: result.count,
|
|
pageSize: result.pageSize,
|
|
offset: result.offset,
|
|
});
|
|
} catch (error) {
|
|
return formatToolError(error);
|
|
}
|
|
}
|
|
|
|
export const getWorkPackageShape = {
|
|
workPackageId: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.describe("The numeric ID of the work package"),
|
|
};
|
|
|
|
export type GetWorkPackageArgs = z.infer<
|
|
z.ZodObject<typeof getWorkPackageShape>
|
|
>;
|
|
|
|
/**
|
|
* Tool execution handler for openproject_get_work_package.
|
|
*/
|
|
export async function handleGetWorkPackage(
|
|
args: GetWorkPackageArgs
|
|
): Promise<McpToolResponse> {
|
|
try {
|
|
const result = await getWorkPackage(args.workPackageId);
|
|
return formatToolSuccess(result);
|
|
} catch (error) {
|
|
return formatToolError(error);
|
|
}
|
|
}
|
|
|
|
export const listWorkPackagesTool: ToolDefinition<
|
|
typeof listWorkPackagesShape,
|
|
ListWorkPackagesArgs
|
|
> = {
|
|
name: "openproject_list_work_packages",
|
|
description:
|
|
"Browse and filter work packages (tasks, bugs, milestones, features) across projects or within a specific project",
|
|
parameters: listWorkPackagesShape,
|
|
readOnly: true,
|
|
execute: handleListWorkPackages,
|
|
};
|
|
|
|
export const getWorkPackageTool: ToolDefinition<
|
|
typeof getWorkPackageShape,
|
|
GetWorkPackageArgs
|
|
> = {
|
|
name: "openproject_get_work_package",
|
|
description:
|
|
"Get full details of a specific work package by its numeric ID, including status, priority, assignee, parent, and child relations",
|
|
parameters: getWorkPackageShape,
|
|
readOnly: true,
|
|
execute: handleGetWorkPackage,
|
|
};
|
|
|
|
export const listWorkPackageActivitiesShape = {
|
|
workPackageId: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.describe("Numeric ID of the work package"),
|
|
onlyComments: z
|
|
.boolean()
|
|
.optional()
|
|
.default(false)
|
|
.describe(
|
|
"When true, filters out property change audits and returns only comments"
|
|
),
|
|
};
|
|
|
|
export type ListWorkPackageActivitiesArgs = z.infer<
|
|
z.ZodObject<typeof listWorkPackageActivitiesShape>
|
|
>;
|
|
|
|
/**
|
|
* Tool execution handler for openproject_list_work_package_activities.
|
|
*/
|
|
export async function handleListWorkPackageActivities(
|
|
args: ListWorkPackageActivitiesArgs
|
|
): Promise<McpToolResponse> {
|
|
try {
|
|
const result = await listWorkPackageActivities({
|
|
workPackageId: args.workPackageId,
|
|
onlyComments: args.onlyComments,
|
|
});
|
|
return formatToolSuccess(result);
|
|
} catch (error) {
|
|
return formatToolError(error);
|
|
}
|
|
}
|
|
|
|
export const listWorkPackageActivitiesTool: ToolDefinition<
|
|
typeof listWorkPackageActivitiesShape,
|
|
ListWorkPackageActivitiesArgs
|
|
> = {
|
|
name: "openproject_list_work_package_activities",
|
|
description:
|
|
"Retrieve timeline activities and comments for a work package. Can optionally filter to only include user comments.",
|
|
parameters: listWorkPackageActivitiesShape,
|
|
readOnly: true,
|
|
execute: handleListWorkPackageActivities,
|
|
};
|
|
|
|
export const searchWorkPackagesShape = {
|
|
query: z
|
|
.string()
|
|
.min(1)
|
|
.describe(
|
|
"Search text matched against work package subjects, descriptions, and " +
|
|
"comments. Natural-language phrasing works and typos are tolerated."
|
|
),
|
|
projectId: z
|
|
.union([z.number().int().positive(), z.string().min(1)])
|
|
.optional()
|
|
.describe("Scope search to a specific project by numeric ID or slug identifier"),
|
|
status: z
|
|
.union([z.enum(["open", "closed"]), z.number().int().positive()])
|
|
.optional()
|
|
.describe("Filter by status: 'open', 'closed', or numeric status ID"),
|
|
matchMode: z
|
|
.enum(["fuzzy", "exact"])
|
|
.optional()
|
|
.default("fuzzy")
|
|
.describe(
|
|
"Matching strategy. 'fuzzy' (default) tolerates typos, word reordering, and " +
|
|
"partial words. Use 'exact' only for literal strings you know verbatim."
|
|
),
|
|
offset: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.default(1)
|
|
.describe("Page number to retrieve (1-based, default 1)"),
|
|
pageSize: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.max(100)
|
|
.optional()
|
|
.default(20)
|
|
.describe("Number of items per page (max 100, default 20)"),
|
|
};
|
|
|
|
export type SearchWorkPackagesArgs = z.input<z.ZodObject<typeof searchWorkPackagesShape>>;
|
|
|
|
/**
|
|
* Tool execution handler for openproject_search_work_packages.
|
|
*/
|
|
export async function handleSearchWorkPackages(
|
|
args: SearchWorkPackagesArgs
|
|
): Promise<McpToolResponse> {
|
|
try {
|
|
const result = await searchWorkPackages(args.query, {
|
|
projectId: args.projectId,
|
|
status: args.status,
|
|
matchMode: args.matchMode,
|
|
offset: args.offset,
|
|
pageSize: args.pageSize,
|
|
});
|
|
return formatToolSuccess(result);
|
|
} catch (error) {
|
|
return formatToolError(error);
|
|
}
|
|
}
|
|
|
|
export const searchWorkPackagesTool: ToolDefinition<
|
|
typeof searchWorkPackagesShape,
|
|
SearchWorkPackagesArgs
|
|
> = {
|
|
name: "openproject_search_work_packages",
|
|
description:
|
|
"Search work packages by subject, description, and comments. Accepts " +
|
|
"natural-language phrasing and tolerates typos. Returns results ranked by " +
|
|
"relevance with matching excerpts.",
|
|
parameters: searchWorkPackagesShape,
|
|
readOnly: true,
|
|
execute: handleSearchWorkPackages,
|
|
};
|
|
|
|
export const workPackageTools = [
|
|
listWorkPackagesTool,
|
|
getWorkPackageTool,
|
|
listWorkPackageActivitiesTool,
|
|
searchWorkPackagesTool,
|
|
];
|
|
|
|
/**
|
|
* Registers all work package tools with an McpServer instance.
|
|
*/
|
|
export function registerWorkPackageTools(server: McpServer): void {
|
|
registerTool(server, listWorkPackagesTool);
|
|
registerTool(server, getWorkPackageTool);
|
|
registerTool(server, listWorkPackageActivitiesTool);
|
|
registerTool(server, searchWorkPackagesTool);
|
|
}
|