New app apps/mcp (NestJS + @nestjs-mcp/server 2): Streamable HTTP at POST /mcp, stateless. - 24 tools: projects (mine, public, details with settings, board, stats, analytics, activity), tickets (my work, search, details with comments and history, create, update, move, assign, release, comment, link), releases (list, upcoming, details with progress and burnup, create, publish) and users (whoami, search). Admin operations stay in the web. - No credentials of its own: each call's Authorization header (a Pireon token) is forwarded to the API; without it only public projects are read. - Typed REST client generated from apps/api/openapi.json (openapi-fetch + openapi-typescript via `nx run mcp:openapi-types`); API refusals become readable tool errors; parameters validated with zod. - Tests: the real MCP against a simulated API (token forwarding, errors, validation, writes). - Docker target `mcp` (Node 24) and an optional compose service under the `mcp` profile; zod declared as a root dependency. - Docs: chapter 9 «Servidor MCP», references in 01/06/07 and the index; Word/PDF regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
1.3 KiB
TypeScript
50 lines
1.3 KiB
TypeScript
import { McpContext, Resolver, Tool } from '@nestjs-mcp/server';
|
|
import { z } from 'zod';
|
|
import { TaskStreamApi } from '../api/taskstream-api';
|
|
import { limit } from './params';
|
|
import { toolResult } from './tool-result';
|
|
|
|
const READ = { readOnlyHint: true, openWorldHint: false } as const;
|
|
|
|
@Resolver()
|
|
export class UsersResolver {
|
|
constructor(private readonly api: TaskStreamApi) {}
|
|
|
|
@Tool({
|
|
name: 'whoami',
|
|
description:
|
|
'The user this MCP acts as (from the Pireon token the client sends).',
|
|
annotations: READ,
|
|
})
|
|
whoami(ctx: McpContext) {
|
|
return toolResult(() =>
|
|
this.api.data(this.api.as(ctx).GET('/api/users/me')),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'search_users',
|
|
description:
|
|
'Find Pireon users by name or email (to assign tickets: use their id).',
|
|
paramsSchema: z.object({
|
|
search: z.string().min(2),
|
|
limit: limit(50, 10),
|
|
}),
|
|
annotations: READ,
|
|
})
|
|
search(
|
|
{ search, limit }: { search: string; limit: number },
|
|
ctx: McpContext,
|
|
) {
|
|
return toolResult(async () =>
|
|
(
|
|
await this.api.data(
|
|
this.api
|
|
.as(ctx)
|
|
.GET('/api/users', { params: { query: { search, limit } } }),
|
|
)
|
|
).map((u) => ({ id: u.id, name: u.name, email: u.email })),
|
|
);
|
|
}
|
|
}
|