[main] feat: GitLab 위키 API 기능 추가 ✨
🚀 Breaking Changes: - 새로운 위키 페이지 관련 API 추가 - USE_GITLAB_WIKI 환경 변수에 따라 위키 도구 활성화/비활성화 가능 📝 Details: - 위키 페이지 목록 조회, 특정 페이지 조회, 페이지 생성, 업데이트 및 삭제 기능 구현 - 관련 스키마 추가 및 패키지 종속성 업데이트
This commit is contained in:
197
index.ts
197
index.ts
@ -6,6 +6,7 @@ import {
|
|||||||
CallToolRequestSchema,
|
CallToolRequestSchema,
|
||||||
ListToolsRequestSchema,
|
ListToolsRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import FormData from "form-data";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { SocksProxyAgent } from 'socks-proxy-agent';
|
import { SocksProxyAgent } from 'socks-proxy-agent';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
@ -75,6 +76,12 @@ import {
|
|||||||
DeleteLabelSchema,
|
DeleteLabelSchema,
|
||||||
CreateNoteSchema,
|
CreateNoteSchema,
|
||||||
ListGroupProjectsSchema,
|
ListGroupProjectsSchema,
|
||||||
|
ListWikiPagesSchema,
|
||||||
|
GetWikiPageSchema,
|
||||||
|
CreateWikiPageSchema,
|
||||||
|
UpdateWikiPageSchema,
|
||||||
|
DeleteWikiPageSchema,
|
||||||
|
GitLabWikiPageSchema,
|
||||||
// Discussion Schemas
|
// Discussion Schemas
|
||||||
GitLabDiscussionNoteSchema, // Added
|
GitLabDiscussionNoteSchema, // Added
|
||||||
GitLabDiscussionSchema,
|
GitLabDiscussionSchema,
|
||||||
@ -101,6 +108,11 @@ import {
|
|||||||
// Discussion Types
|
// Discussion Types
|
||||||
type GitLabDiscussionNote, // Added
|
type GitLabDiscussionNote, // Added
|
||||||
type GitLabDiscussion,
|
type GitLabDiscussion,
|
||||||
|
type GetWikiPageOptions,
|
||||||
|
type CreateWikiPageOptions,
|
||||||
|
type UpdateWikiPageOptions,
|
||||||
|
type DeleteWikiPageOptions,
|
||||||
|
type GitLabWikiPage,
|
||||||
} from "./schemas.js";
|
} from "./schemas.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -133,6 +145,7 @@ const server = new Server(
|
|||||||
|
|
||||||
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
|
||||||
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
const GITLAB_READ_ONLY_MODE = process.env.GITLAB_READ_ONLY_MODE === "true";
|
||||||
|
const USE_GITLAB_WIKI = process.env.USE_GITLAB_WIKI === "true";
|
||||||
|
|
||||||
// Add proxy configuration
|
// Add proxy configuration
|
||||||
const HTTP_PROXY = process.env.HTTP_PROXY;
|
const HTTP_PROXY = process.env.HTTP_PROXY;
|
||||||
@ -348,6 +361,31 @@ const allTools = [
|
|||||||
description: "List projects in a GitLab group with filtering options",
|
description: "List projects in a GitLab group with filtering options",
|
||||||
inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
|
inputSchema: zodToJsonSchema(ListGroupProjectsSchema),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "list_wiki_pages",
|
||||||
|
description: "List wiki pages in a GitLab project",
|
||||||
|
inputSchema: zodToJsonSchema(ListWikiPagesSchema),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "get_wiki_page",
|
||||||
|
description: "Get details of a specific wiki page",
|
||||||
|
inputSchema: zodToJsonSchema(GetWikiPageSchema),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create_wiki_page",
|
||||||
|
description: "Create a new wiki page in a GitLab project",
|
||||||
|
inputSchema: zodToJsonSchema(CreateWikiPageSchema),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update_wiki_page",
|
||||||
|
description: "Update an existing wiki page in a GitLab project",
|
||||||
|
inputSchema: zodToJsonSchema(UpdateWikiPageSchema),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "delete_wiki_page",
|
||||||
|
description: "Delete a wiki page from a GitLab project",
|
||||||
|
inputSchema: zodToJsonSchema(DeleteWikiPageSchema),
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define which tools are read-only
|
// Define which tools are read-only
|
||||||
@ -371,6 +409,16 @@ const readOnlyTools = [
|
|||||||
"list_group_projects",
|
"list_group_projects",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Define which tools are related to wiki and can be toggled by USE_GITLAB_WIKI
|
||||||
|
const wikiToolNames = [
|
||||||
|
"list_wiki_pages",
|
||||||
|
"get_wiki_page",
|
||||||
|
"create_wiki_page",
|
||||||
|
"update_wiki_page",
|
||||||
|
"delete_wiki_page",
|
||||||
|
"upload_wiki_attachment",
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smart URL handling for GitLab API
|
* Smart URL handling for GitLab API
|
||||||
*
|
*
|
||||||
@ -1787,11 +1835,126 @@ async function listGroupProjects(
|
|||||||
return GitLabProjectSchema.array().parse(projects);
|
return GitLabProjectSchema.array().parse(projects);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wiki API helper functions
|
||||||
|
/**
|
||||||
|
* List wiki pages in a project
|
||||||
|
*/
|
||||||
|
async function listWikiPages(
|
||||||
|
projectId: string,
|
||||||
|
options: Omit<z.infer<typeof ListWikiPagesSchema>, 'project_id'> = {}
|
||||||
|
): Promise<GitLabWikiPage[]> {
|
||||||
|
const url = new URL(
|
||||||
|
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`
|
||||||
|
);
|
||||||
|
if (options.page) url.searchParams.append('page', options.page.toString());
|
||||||
|
if (options.per_page) url.searchParams.append('per_page', options.per_page.toString());
|
||||||
|
const response = await fetch(url.toString(), {
|
||||||
|
...DEFAULT_FETCH_CONFIG,
|
||||||
|
});
|
||||||
|
await handleGitLabError(response);
|
||||||
|
const data = await response.json();
|
||||||
|
return GitLabWikiPageSchema.array().parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific wiki page
|
||||||
|
*/
|
||||||
|
async function getWikiPage(
|
||||||
|
projectId: string,
|
||||||
|
slug: string
|
||||||
|
): Promise<GitLabWikiPage> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(
|
||||||
|
slug
|
||||||
|
)}`,
|
||||||
|
{ ...DEFAULT_FETCH_CONFIG }
|
||||||
|
);
|
||||||
|
await handleGitLabError(response);
|
||||||
|
const data = await response.json();
|
||||||
|
return GitLabWikiPageSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new wiki page
|
||||||
|
*/
|
||||||
|
async function createWikiPage(
|
||||||
|
projectId: string,
|
||||||
|
title: string,
|
||||||
|
content: string,
|
||||||
|
format?: string
|
||||||
|
): Promise<GitLabWikiPage> {
|
||||||
|
const body: Record<string, any> = { title, content };
|
||||||
|
if (format) body.format = format;
|
||||||
|
const response = await fetch(
|
||||||
|
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis`,
|
||||||
|
{
|
||||||
|
...DEFAULT_FETCH_CONFIG,
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await handleGitLabError(response);
|
||||||
|
const data = await response.json();
|
||||||
|
return GitLabWikiPageSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing wiki page
|
||||||
|
*/
|
||||||
|
async function updateWikiPage(
|
||||||
|
projectId: string,
|
||||||
|
slug: string,
|
||||||
|
title?: string,
|
||||||
|
content?: string,
|
||||||
|
format?: string
|
||||||
|
): Promise<GitLabWikiPage> {
|
||||||
|
const body: Record<string, any> = {};
|
||||||
|
if (title) body.title = title;
|
||||||
|
if (content) body.content = content;
|
||||||
|
if (format) body.format = format;
|
||||||
|
const response = await fetch(
|
||||||
|
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(
|
||||||
|
slug
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
...DEFAULT_FETCH_CONFIG,
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await handleGitLabError(response);
|
||||||
|
const data = await response.json();
|
||||||
|
return GitLabWikiPageSchema.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a wiki page
|
||||||
|
*/
|
||||||
|
async function deleteWikiPage(
|
||||||
|
projectId: string,
|
||||||
|
slug: string
|
||||||
|
): Promise<void> {
|
||||||
|
const response = await fetch(
|
||||||
|
`${GITLAB_API_URL}/projects/${encodeURIComponent(projectId)}/wikis/${encodeURIComponent(
|
||||||
|
slug
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
...DEFAULT_FETCH_CONFIG,
|
||||||
|
method: 'DELETE',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await handleGitLabError(response);
|
||||||
|
}
|
||||||
|
|
||||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
// If read-only mode is enabled, filter out write operations
|
// Apply read-only filter first
|
||||||
const tools = GITLAB_READ_ONLY_MODE
|
const tools0 = GITLAB_READ_ONLY_MODE
|
||||||
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
|
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
|
||||||
: allTools;
|
: allTools;
|
||||||
|
// Toggle wiki tools by USE_GITLAB_WIKI flag
|
||||||
|
const tools = USE_GITLAB_WIKI
|
||||||
|
? tools0
|
||||||
|
: tools0.filter((tool) => !wikiToolNames.includes(tool.name));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
tools,
|
||||||
@ -2287,6 +2450,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case "list_wiki_pages": {
|
||||||
|
const { project_id, page, per_page } = ListWikiPagesSchema.parse(request.params.arguments);
|
||||||
|
const wikiPages = await listWikiPages(project_id, { page, per_page });
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(wikiPages, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "get_wiki_page": {
|
||||||
|
const { project_id, slug } = GetWikiPageSchema.parse(request.params.arguments);
|
||||||
|
const wikiPage = await getWikiPage(project_id, slug);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "create_wiki_page": {
|
||||||
|
const { project_id, title, content, format } = CreateWikiPageSchema.parse(request.params.arguments);
|
||||||
|
const wikiPage = await createWikiPage(project_id, title, content, format);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "update_wiki_page": {
|
||||||
|
const { project_id, slug, title, content, format } = UpdateWikiPageSchema.parse(request.params.arguments);
|
||||||
|
const wikiPage = await updateWikiPage(project_id, slug, title, content, format);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(wikiPage, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
case "delete_wiki_page": {
|
||||||
|
const { project_id, slug } = DeleteWikiPageSchema.parse(request.params.arguments);
|
||||||
|
await deleteWikiPage(project_id, slug);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", message: "Wiki page deleted successfully" }, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown tool: ${request.params.name}`);
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "1.8.0",
|
"@modelcontextprotocol/sdk": "1.8.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"http-proxy-agent": "^7.0.2",
|
"http-proxy-agent": "^7.0.2",
|
||||||
"https-proxy-agent": "^7.0.6",
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
44
schemas.ts
44
schemas.ts
@ -756,6 +756,44 @@ export const ListGroupProjectsSchema = z.object({
|
|||||||
with_security_reports: z.boolean().optional().describe("Include security reports")
|
with_security_reports: z.boolean().optional().describe("Include security reports")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add wiki operation schemas
|
||||||
|
export const ListWikiPagesSchema = z.object({
|
||||||
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||||
|
page: z.number().optional().describe("Page number for pagination"),
|
||||||
|
per_page: z.number().optional().describe("Number of items per page"),
|
||||||
|
});
|
||||||
|
export const GetWikiPageSchema = z.object({
|
||||||
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||||
|
slug: z.string().describe("URL-encoded slug of the wiki page"),
|
||||||
|
});
|
||||||
|
export const CreateWikiPageSchema = z.object({
|
||||||
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||||
|
title: z.string().describe("Title of the wiki page"),
|
||||||
|
content: z.string().describe("Content of the wiki page"),
|
||||||
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
||||||
|
});
|
||||||
|
export const UpdateWikiPageSchema = z.object({
|
||||||
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||||
|
slug: z.string().describe("URL-encoded slug of the wiki page"),
|
||||||
|
title: z.string().optional().describe("New title of the wiki page"),
|
||||||
|
content: z.string().optional().describe("New content of the wiki page"),
|
||||||
|
format: z.string().optional().describe("Content format, e.g., markdown, rdoc"),
|
||||||
|
});
|
||||||
|
export const DeleteWikiPageSchema = z.object({
|
||||||
|
project_id: z.string().describe("Project ID or URL-encoded path"),
|
||||||
|
slug: z.string().describe("URL-encoded slug of the wiki page"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define wiki response schemas
|
||||||
|
export const GitLabWikiPageSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
format: z.string(),
|
||||||
|
content: z.string(),
|
||||||
|
created_at: z.string().optional(),
|
||||||
|
updated_at: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
|
export type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
|
||||||
export type GitLabFork = z.infer<typeof GitLabForkSchema>;
|
export type GitLabFork = z.infer<typeof GitLabForkSchema>;
|
||||||
@ -783,3 +821,9 @@ export type GitLabNamespace = z.infer<typeof GitLabNamespaceSchema>;
|
|||||||
export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>;
|
export type GitLabNamespaceExistsResponse = z.infer<typeof GitLabNamespaceExistsResponseSchema>;
|
||||||
export type GitLabProject = z.infer<typeof GitLabProjectSchema>;
|
export type GitLabProject = z.infer<typeof GitLabProjectSchema>;
|
||||||
export type GitLabLabel = z.infer<typeof GitLabLabelSchema>;
|
export type GitLabLabel = z.infer<typeof GitLabLabelSchema>;
|
||||||
|
export type ListWikiPagesOptions = z.infer<typeof ListWikiPagesSchema>;
|
||||||
|
export type GetWikiPageOptions = z.infer<typeof GetWikiPageSchema>;
|
||||||
|
export type CreateWikiPageOptions = z.infer<typeof CreateWikiPageSchema>;
|
||||||
|
export type UpdateWikiPageOptions = z.infer<typeof UpdateWikiPageSchema>;
|
||||||
|
export type DeleteWikiPageOptions = z.infer<typeof DeleteWikiPageSchema>;
|
||||||
|
export type GitLabWikiPage = z.infer<typeof GitLabWikiPageSchema>;
|
||||||
|
Reference in New Issue
Block a user