mirror of
https://github.com/penpot/penpot.git
synced 2026-08-27 07:09:14 +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 { ImportPenpotFileTool } from "./tools/ImportPenpotFileTool";
|
||||||
import { CljsCompilerOutputTool } from "./tools/CljsCompilerOutputTool";
|
import { CljsCompilerOutputTool } from "./tools/CljsCompilerOutputTool";
|
||||||
import { CljCheckParentheses } from "./tools/CljCheckParentheses";
|
import { CljCheckParentheses } from "./tools/CljCheckParentheses";
|
||||||
|
import { ReadTaigaIssueTool } from "./tools/ReadTaigaIssueTool";
|
||||||
import { NreplClient } from "./NreplClient";
|
import { NreplClient } from "./NreplClient";
|
||||||
import { ReplServer } from "./ReplServer";
|
import { ReplServer } from "./ReplServer";
|
||||||
import { ApiDocs } from "./ApiDocs";
|
import { ApiDocs } from "./ApiDocs";
|
||||||
@ -198,6 +199,7 @@ export class PenpotMcpServer {
|
|||||||
toolInstances.push(new ImportPenpotFileTool(this, nreplClient));
|
toolInstances.push(new ImportPenpotFileTool(this, nreplClient));
|
||||||
toolInstances.push(new CljsCompilerOutputTool(this, nreplClient));
|
toolInstances.push(new CljsCompilerOutputTool(this, nreplClient));
|
||||||
toolInstances.push(new CljCheckParentheses(this));
|
toolInstances.push(new CljCheckParentheses(this));
|
||||||
|
toolInstances.push(new ReadTaigaIssueTool(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
return toolInstances.map((instance) => {
|
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