mirror of
https://github.com/penpot/penpot.git
synced 2026-05-15 04:54:10 +00:00
🎉 Add ReadTaigaIssueTool to Penpot MCP server
The tool is enabled in the agentic devenv to enable agents to read Penpot issues on Taiga. GitHub #9303
This commit is contained in:
parent
e3df1d6f1f
commit
2a326ba23e
@ -15,6 +15,7 @@ import { CljsReplTool } from "./tools/CljsReplTool";
|
||||
import { ImportPenpotFileTool } from "./tools/ImportPenpotFileTool";
|
||||
import { CljsCompilerOutputTool } from "./tools/CljsCompilerOutputTool";
|
||||
import { CljCheckParentheses } from "./tools/CljCheckParentheses";
|
||||
import { ReadTaigaIssueTool } from "./tools/ReadTaigaIssueTool";
|
||||
import { NreplClient } from "./NreplClient";
|
||||
import { ReplServer } from "./ReplServer";
|
||||
import { ApiDocs } from "./ApiDocs";
|
||||
@ -198,6 +199,7 @@ export class PenpotMcpServer {
|
||||
toolInstances.push(new ImportPenpotFileTool(this, nreplClient));
|
||||
toolInstances.push(new CljsCompilerOutputTool(this, nreplClient));
|
||||
toolInstances.push(new CljCheckParentheses(this));
|
||||
toolInstances.push(new ReadTaigaIssueTool(this));
|
||||
}
|
||||
|
||||
return toolInstances.map((instance) => {
|
||||
|
||||
163
mcp/packages/server/src/tools/ReadTaigaIssueTool.ts
Normal file
163
mcp/packages/server/src/tools/ReadTaigaIssueTool.ts
Normal file
@ -0,0 +1,163 @@
|
||||
import { z } from "zod";
|
||||
import { Tool } from "../Tool";
|
||||
import "reflect-metadata";
|
||||
import type { ToolResponse } from "../ToolResponse";
|
||||
import { TextResponse } from "../ToolResponse";
|
||||
import { PenpotMcpServer } from "../PenpotMcpServer";
|
||||
|
||||
/**
|
||||
* Arguments for the {@link ReadTaigaIssueTool}.
|
||||
*/
|
||||
export class ReadTaigaIssueArgs {
|
||||
static schema = {
|
||||
issueNumber: z
|
||||
.number()
|
||||
.int()
|
||||
.positive()
|
||||
.describe(
|
||||
"The Penpot issue number as it appears in Taiga URLs, " +
|
||||
"e.g. 14177 for https://tree.taiga.io/project/penpot/issue/14177"
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* The Penpot issue number as it appears in Taiga issue URLs
|
||||
* (e.g. 14177 for https://tree.taiga.io/project/penpot/issue/14177).
|
||||
*/
|
||||
issueNumber!: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a file attachment on a Taiga issue.
|
||||
*/
|
||||
interface TaigaAttachment {
|
||||
filename: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a comment on a Taiga issue.
|
||||
*/
|
||||
interface TaigaComment {
|
||||
username: string;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The resolved issue data returned by the tool.
|
||||
*/
|
||||
interface TaigaIssueData {
|
||||
subject: string;
|
||||
description: string;
|
||||
status: string;
|
||||
attachments: TaigaAttachment[];
|
||||
comments: TaigaComment[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tool for reading Penpot issues from the Taiga project tracker.
|
||||
*
|
||||
* Resolves a Penpot issue number to its internal Taiga ID and retrieves the issue's
|
||||
* subject, description, status, attachments, and comments via the Taiga REST API.
|
||||
*/
|
||||
export class ReadTaigaIssueTool extends Tool<ReadTaigaIssueArgs> {
|
||||
private static readonly TAIGA_API_BASE = "https://api.taiga.io/api/v1";
|
||||
|
||||
constructor(mcpServer: PenpotMcpServer) {
|
||||
super(mcpServer, ReadTaigaIssueArgs.schema);
|
||||
}
|
||||
|
||||
public getToolName(): string {
|
||||
return "read_taiga_issue";
|
||||
}
|
||||
|
||||
public getToolDescription(): string {
|
||||
return "Reads a Penpot issue from the Taiga project tracker, returning its subject, description, status, attachments, and comments.";
|
||||
}
|
||||
|
||||
protected async executeCore(args: ReadTaigaIssueArgs): Promise<ToolResponse> {
|
||||
const { projectId, issueId } = await this.resolveIssue(args.issueNumber);
|
||||
const issueData = await this.fetchIssueData(projectId, issueId);
|
||||
return new TextResponse(JSON.stringify(issueData, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a Penpot issue number to the internal Taiga project and issue IDs.
|
||||
*/
|
||||
private async resolveIssue(issueNumber: number): Promise<{ projectId: number; issueId: number }> {
|
||||
const url = `${ReadTaigaIssueTool.TAIGA_API_BASE}/resolver?project=penpot&issue=${issueNumber}`;
|
||||
const data = await this.fetchJson(url);
|
||||
return { projectId: data.project, issueId: data.issue };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the full issue data including details, attachments, and comments.
|
||||
*/
|
||||
private async fetchIssueData(projectId: number, issueId: number): Promise<TaigaIssueData> {
|
||||
// fetch issue details, attachments, and history in parallel
|
||||
const [details, attachments, comments] = await Promise.all([
|
||||
this.fetchIssueDetails(issueId),
|
||||
this.fetchAttachments(projectId, issueId),
|
||||
this.fetchComments(issueId),
|
||||
]);
|
||||
|
||||
return {
|
||||
subject: details.subject,
|
||||
description: details.description ?? "",
|
||||
status: details.status_extra_info?.name ?? "Unknown",
|
||||
attachments,
|
||||
comments,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the core issue details from the Taiga API.
|
||||
*/
|
||||
private async fetchIssueDetails(issueId: number): Promise<any> {
|
||||
const url = `${ReadTaigaIssueTool.TAIGA_API_BASE}/issues/${issueId}`;
|
||||
return this.fetchJson(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the attachments for an issue.
|
||||
*/
|
||||
private async fetchAttachments(projectId: number, issueId: number): Promise<TaigaAttachment[]> {
|
||||
const url = `${ReadTaigaIssueTool.TAIGA_API_BASE}/issues/attachments?project=${projectId}&object_id=${issueId}`;
|
||||
const data: any[] = await this.fetchJson(url);
|
||||
return data.map((a) => ({
|
||||
filename: a.name,
|
||||
size: a.size,
|
||||
url: a.url,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches comments from the issue history.
|
||||
*
|
||||
* History entries that have a non-empty `comment` field are treated as comments.
|
||||
*/
|
||||
private async fetchComments(issueId: number): Promise<TaigaComment[]> {
|
||||
const url = `${ReadTaigaIssueTool.TAIGA_API_BASE}/history/issue/${issueId}`;
|
||||
const history: any[] = await this.fetchJson(url);
|
||||
return history
|
||||
.filter((entry) => entry.comment && entry.comment.trim().length > 0)
|
||||
.map((entry) => ({
|
||||
username: entry.user?.username ?? "unknown",
|
||||
comment: entry.comment,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a GET request and returns the parsed JSON response.
|
||||
*
|
||||
* @throws Error if the HTTP response status is not OK
|
||||
*/
|
||||
private async fetchJson(url: string): Promise<any> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Taiga API request failed: ${response.status} ${response.statusText} (${url})`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user