feat(mcp-server): add tracker.* namespace — 13 tools for items, votes, comments, public roadmap
- tracker-client functions appended to platform-client.ts (tracker is part of platform-service)
- tracker.items.{stats,list,get,create,updateStatus,delete}
- tracker.votes.{toggle,list}
- tracker.comments.{list,add,delete}
- tracker.public.{roadmap,stats}
- All 13 tools require admin role; productId forwarded as x-product-id
This commit is contained in:
parent
ecfbaa62b6
commit
33dd530d5f
@ -289,3 +289,242 @@ export async function diagnosticsGetTraces(
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tracker — items ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface TrackerItemDoc {
|
||||
id: string;
|
||||
productId: string;
|
||||
type: string;
|
||||
status: string;
|
||||
priority: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
labels?: string[];
|
||||
assignee?: string | null;
|
||||
reportedBy?: string;
|
||||
source?: string;
|
||||
visibility?: string;
|
||||
voteCount: number;
|
||||
commentCount: number;
|
||||
targetRelease?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function trackerItemsList(
|
||||
params: {
|
||||
productId?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
priority?: string;
|
||||
q?: string;
|
||||
labels?: string;
|
||||
visibility?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ items: TrackerItemDoc[]; total: number; limit: number; offset: number }> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.productId) qs.set('productId', params.productId);
|
||||
if (params.type) qs.set('type', params.type);
|
||||
if (params.status) qs.set('status', params.status);
|
||||
if (params.priority) qs.set('priority', params.priority);
|
||||
if (params.q) qs.set('q', params.q);
|
||||
if (params.labels) qs.set('labels', params.labels);
|
||||
if (params.visibility) qs.set('visibility', params.visibility);
|
||||
if (params.sortBy) qs.set('sortBy', params.sortBy);
|
||||
if (params.sortOrder) qs.set('sortOrder', params.sortOrder);
|
||||
qs.set(
|
||||
'limit',
|
||||
String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT))
|
||||
);
|
||||
if (params.offset !== undefined) qs.set('offset', String(params.offset));
|
||||
return platformFetch<{ items: TrackerItemDoc[]; total: number; limit: number; offset: number }>(
|
||||
`/api/items?${qs}`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerItemsStats(opts: PlatformClientOptions): Promise<{
|
||||
total: number;
|
||||
byType: Record<string, number>;
|
||||
byStatus: Record<string, number>;
|
||||
byPriority: Record<string, number>;
|
||||
}> {
|
||||
return platformFetch<{
|
||||
total: number;
|
||||
byType: Record<string, number>;
|
||||
byStatus: Record<string, number>;
|
||||
byPriority: Record<string, number>;
|
||||
}>('/api/items/stats', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
export function trackerItemsGet(
|
||||
itemId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<TrackerItemDoc> {
|
||||
return platformFetch<TrackerItemDoc>(
|
||||
`/api/items/${encodeURIComponent(itemId)}`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerItemsCreate(
|
||||
input: {
|
||||
productId?: string;
|
||||
type: string;
|
||||
priority: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
labels?: string[];
|
||||
assignee?: string;
|
||||
source?: string;
|
||||
visibility?: string;
|
||||
targetRelease?: string;
|
||||
},
|
||||
opts: PlatformClientOptions
|
||||
): Promise<TrackerItemDoc> {
|
||||
return platformFetch<TrackerItemDoc>(
|
||||
'/api/items',
|
||||
{ method: 'POST', body: JSON.stringify(input) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerItemsUpdateStatus(
|
||||
itemId: string,
|
||||
status: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<TrackerItemDoc> {
|
||||
return platformFetch<TrackerItemDoc>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/status`,
|
||||
{ method: 'PATCH', body: JSON.stringify({ status }) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerItemsDelete(
|
||||
itemId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ success: boolean }> {
|
||||
return platformFetch<{ success: boolean }>(
|
||||
`/api/items/${encodeURIComponent(itemId)}`,
|
||||
{ method: 'DELETE' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tracker — votes ───────────────────────────────────────────────────────────
|
||||
|
||||
export function trackerVotesToggle(
|
||||
itemId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ voted: boolean; voteCount: number }> {
|
||||
return platformFetch<{ voted: boolean; voteCount: number }>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/vote`,
|
||||
{ method: 'POST', body: '{}' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerVotesList(
|
||||
itemId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ votes: unknown[]; count: number }> {
|
||||
return platformFetch<{ votes: unknown[]; count: number }>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/votes`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tracker — comments ────────────────────────────────────────────────────────
|
||||
|
||||
export function trackerCommentsList(
|
||||
itemId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ comments: unknown[]; count: number }> {
|
||||
return platformFetch<{ comments: unknown[]; count: number }>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/comments`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerCommentsAdd(
|
||||
itemId: string,
|
||||
body: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<unknown> {
|
||||
return platformFetch<unknown>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/comments`,
|
||||
{ method: 'POST', body: JSON.stringify({ body }) },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerCommentsDelete(
|
||||
itemId: string,
|
||||
commentId: string,
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ success: boolean }> {
|
||||
return platformFetch<{ success: boolean }>(
|
||||
`/api/items/${encodeURIComponent(itemId)}/comments/${encodeURIComponent(commentId)}`,
|
||||
{ method: 'DELETE' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tracker — public roadmap ──────────────────────────────────────────────────
|
||||
|
||||
export function trackerPublicRoadmap(
|
||||
params: {
|
||||
productId?: string;
|
||||
type?: string;
|
||||
status?: string;
|
||||
q?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
opts: PlatformClientOptions
|
||||
): Promise<{ items: TrackerItemDoc[]; total: number }> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.productId) qs.set('productId', params.productId);
|
||||
if (params.type) qs.set('type', params.type);
|
||||
if (params.status) qs.set('status', params.status);
|
||||
if (params.q) qs.set('q', params.q);
|
||||
if (params.sortBy) qs.set('sortBy', params.sortBy);
|
||||
if (params.sortOrder) qs.set('sortOrder', params.sortOrder);
|
||||
qs.set(
|
||||
'limit',
|
||||
String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT))
|
||||
);
|
||||
if (params.offset !== undefined) qs.set('offset', String(params.offset));
|
||||
return platformFetch<{ items: TrackerItemDoc[]; total: number }>(
|
||||
`/api/public/roadmap?${qs}`,
|
||||
{ method: 'GET' },
|
||||
opts
|
||||
);
|
||||
}
|
||||
|
||||
export function trackerPublicStats(opts: PlatformClientOptions): Promise<{
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
byType: Record<string, number>;
|
||||
totalVotes: number;
|
||||
}> {
|
||||
return platformFetch<{
|
||||
total: number;
|
||||
byStatus: Record<string, number>;
|
||||
byType: Record<string, number>;
|
||||
totalVotes: number;
|
||||
}>('/api/public/roadmap/stats', { method: 'GET' }, opts);
|
||||
}
|
||||
|
||||
284
services/mcp-server/src/modules/tracker/tracker-tools.ts
Normal file
284
services/mcp-server/src/modules/tracker/tracker-tools.ts
Normal file
@ -0,0 +1,284 @@
|
||||
/**
|
||||
* Tracker MCP tools — tracker.items.*, tracker.votes.*, tracker.comments.*, tracker.public.*
|
||||
*
|
||||
* Backed by: platform-service (port 4003) — items, votes, comments, public modules.
|
||||
* All tools require admin role minimum.
|
||||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { registerTool } from '../tools/registry.js';
|
||||
import { config } from '../../lib/config.js';
|
||||
import {
|
||||
trackerItemsList,
|
||||
trackerItemsStats,
|
||||
trackerItemsGet,
|
||||
trackerItemsCreate,
|
||||
trackerItemsUpdateStatus,
|
||||
trackerItemsDelete,
|
||||
trackerVotesToggle,
|
||||
trackerVotesList,
|
||||
trackerCommentsList,
|
||||
trackerCommentsAdd,
|
||||
trackerCommentsDelete,
|
||||
trackerPublicRoadmap,
|
||||
trackerPublicStats,
|
||||
} from '../../lib/platform-client.js';
|
||||
import type { McpToolRequest } from '../tools/types.js';
|
||||
|
||||
const tokenOf = (req: McpToolRequest) => req.headers.authorization?.slice(7);
|
||||
|
||||
// ── tracker.items.stats ───────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.stats',
|
||||
description:
|
||||
'Aggregate tracker item counts by type, status, and priority for the product scoped to the request. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
productId: z.string().min(1).describe('Product ID to scope the stats'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsStats({ token: tokenOf(req), requestId: req.id, productId: args.productId });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.items.list ────────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.list',
|
||||
description:
|
||||
'List tracker items (bugs, feature requests, tasks, improvements). Filter by type, status, priority, visibility, or free text. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
productId: z.string().min(1).describe('Product ID'),
|
||||
type: z
|
||||
.enum(['bug', 'feature', 'task', 'improvement'])
|
||||
.optional()
|
||||
.describe('Filter by item type'),
|
||||
status: z
|
||||
.enum(['open', 'in_progress', 'in_review', 'closed', 'wont_fix'])
|
||||
.optional()
|
||||
.describe('Filter by status'),
|
||||
priority: z
|
||||
.enum(['critical', 'high', 'medium', 'low'])
|
||||
.optional()
|
||||
.describe('Filter by priority'),
|
||||
visibility: z
|
||||
.enum(['public', 'internal'])
|
||||
.optional()
|
||||
.describe('Filter by visibility (public = on roadmap)'),
|
||||
q: z.string().optional().describe('Full-text search across title and description'),
|
||||
sortBy: z
|
||||
.enum(['createdAt', 'updatedAt', 'voteCount', 'priority'])
|
||||
.optional()
|
||||
.default('createdAt'),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
||||
limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsList(args, {
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
productId: args.productId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.items.get ─────────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.get',
|
||||
description:
|
||||
'Get a single tracker item by ID including voteCount, commentCount, labels, assignee, and targetRelease. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID (trk_…)'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsGet(args.itemId, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.items.create ──────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.create',
|
||||
description:
|
||||
'Create a new tracker item. Sets status=open automatically. Visibility defaults to internal — set to public to surface on the roadmap. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
productId: z.string().min(1).describe('Product ID'),
|
||||
type: z.enum(['bug', 'feature', 'task', 'improvement']),
|
||||
priority: z.enum(['critical', 'high', 'medium', 'low']).default('medium'),
|
||||
title: z.string().min(1).max(250).describe('Item title'),
|
||||
description: z.string().optional().describe('Detailed description (markdown supported)'),
|
||||
labels: z.array(z.string()).optional().describe('Label tags'),
|
||||
assignee: z.string().optional().describe('Assignee user ID or email'),
|
||||
visibility: z.enum(['public', 'internal']).optional().default('internal'),
|
||||
targetRelease: z.string().optional().describe('Target release version string'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsCreate(args, {
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
productId: args.productId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.items.updateStatus ────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.updateStatus',
|
||||
description:
|
||||
'Quick status transition for a tracker item (open → in_progress → in_review → closed / wont_fix). Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID'),
|
||||
status: z.enum(['open', 'in_progress', 'in_review', 'closed', 'wont_fix']),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsUpdateStatus(args.itemId, args.status, {
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.items.delete ──────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.items.delete',
|
||||
description:
|
||||
'Permanently delete a tracker item. This also removes associated vote counts. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID to delete'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerItemsDelete(args.itemId, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.votes.toggle ──────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.votes.toggle',
|
||||
description:
|
||||
'Toggle an upvote on a tracker item on behalf of the authenticated admin user. Returns new voteCount and voted state. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID to vote on'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerVotesToggle(args.itemId, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.votes.list ────────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.votes.list',
|
||||
description: 'List voters for a tracker item (userId list + count). Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerVotesList(args.itemId, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.comments.list ─────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.comments.list',
|
||||
description: 'List all comments for a tracker item in chronological order. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerCommentsList(args.itemId, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.comments.add ──────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.comments.add',
|
||||
description:
|
||||
'Add a comment to a tracker item. The comment body supports Markdown. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID'),
|
||||
body: z.string().min(1).describe('Comment body (Markdown supported)'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerCommentsAdd(args.itemId, args.body, { token: tokenOf(req), requestId: req.id });
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.comments.delete ───────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.comments.delete',
|
||||
description: 'Delete a comment (admin can delete any comment). Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
itemId: z.string().min(1).describe('Tracker item ID'),
|
||||
commentId: z.string().min(1).describe('Comment ID (cmt_…)'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerCommentsDelete(args.itemId, args.commentId, {
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.public.roadmap ────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.public.roadmap',
|
||||
description:
|
||||
'List public roadmap items (visibility=public, excludes closed/wont_fix) as seen by end-users. No auth required on the backend — useful for admin inspection of what is publicly visible. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
productId: z.string().min(1).describe('Product ID'),
|
||||
type: z.enum(['bug', 'feature', 'task', 'improvement']).optional().describe('Filter by type'),
|
||||
status: z.enum(['open', 'in_progress', 'in_review']).optional().describe('Filter by status'),
|
||||
q: z.string().optional().describe('Free-text search'),
|
||||
sortBy: z.enum(['createdAt', 'updatedAt', 'voteCount']).optional().default('voteCount'),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),
|
||||
limit: z.coerce.number().min(1).max(config.QUERY_MAX_LIMIT).default(config.QUERY_DEFAULT_LIMIT),
|
||||
offset: z.coerce.number().min(0).default(0),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerPublicRoadmap(args, {
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
productId: args.productId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ── tracker.public.stats ──────────────────────────────────────────────────────
|
||||
|
||||
registerTool({
|
||||
name: 'tracker.public.stats',
|
||||
description:
|
||||
'Aggregate public roadmap stats: total public items, counts by status and type, total votes cast. Requires admin role.',
|
||||
requiredRole: 'admin',
|
||||
inputSchema: z.object({
|
||||
productId: z.string().min(1).describe('Product ID'),
|
||||
}),
|
||||
async execute(args, req) {
|
||||
return trackerPublicStats({
|
||||
token: tokenOf(req),
|
||||
requestId: req.id,
|
||||
productId: args.productId,
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -12,6 +12,7 @@
|
||||
* chronomind.* — timers, routines, syncStatus
|
||||
* nomgap.* — fasting sessions, push triggers
|
||||
* peakpulse.* — adventure sessions, GPS routes, stats
|
||||
* tracker.* — items, votes, comments, public roadmap
|
||||
*
|
||||
* Auth: JWT Bearer tokens issued by platform-service (same JWT_SECRET).
|
||||
* Role gating: viewer / admin / super_admin per tool.
|
||||
@ -35,12 +36,13 @@ import './modules/jarvis/jarvis-tools.js';
|
||||
import './modules/chronomind/chronomind-tools.js';
|
||||
import './modules/nomgap/nomgap-tools.js';
|
||||
import './modules/peakpulse/peakpulse-tools.js';
|
||||
import './modules/tracker/tracker-tools.js';
|
||||
|
||||
const app = await createServiceApp({
|
||||
name: 'mcp-server',
|
||||
version: '0.1.0',
|
||||
description:
|
||||
'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*',
|
||||
'ByteLyst MCP Server — platform.*, extraction.*, support.*, mindlyst.*, lysnrai.*, jarvis.*, chronomind.*, nomgap.*, peakpulse.*, tracker.*',
|
||||
corsOrigin: config.CORS_ORIGIN,
|
||||
logLevel: config.LOG_LEVEL,
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user