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:
saravanakumardb1 2026-03-05 13:36:55 -08:00
parent ecfbaa62b6
commit 33dd530d5f
3 changed files with 526 additions and 1 deletions

View File

@ -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);
}

View 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,
});
},
});

View File

@ -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,
});