Files
task-stream/apps/mcp/src/app/tools/tool-result.ts
Josep Piguillem 9236cb73c6 feat(mcp): MCP server for AI clients, consuming the REST API
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>
2026-10-07 18:59:38 +02:00

35 lines
907 B
TypeScript

import type { CallToolResult } from '@modelcontextprotocol/server';
import { ApiError } from '../api/taskstream-api';
/**
* Runs a tool and answers with its result as JSON text, or, if the API
* refuses it, with an error result the model can read and act on (instead of
* a protocol error that aborts the call).
*/
export async function toolResult(
run: () => Promise<unknown>,
): Promise<CallToolResult> {
try {
const result = await run();
return {
content: [
{
type: 'text',
text:
typeof result === 'string'
? result
: JSON.stringify(result ?? { ok: true }, null, 2),
},
],
};
} catch (error) {
if (!(error instanceof ApiError)) throw error;
return {
isError: true,
content: [
{ type: 'text', text: `Error ${error.status}: ${error.message}` },
],
};
}
}