[main] feat: GitLab 위키 API 기능 추가

🚀 Breaking Changes:
- 새로운 위키 페이지 관련 API 추가
- USE_GITLAB_WIKI 환경 변수에 따라 위키 도구 활성화/비활성화 가능

📝 Details:
- 위키 페이지 목록 조회, 특정 페이지 조회, 페이지 생성, 업데이트 및 삭제 기능 구현
- 관련 스키마 추가 및 패키지 종속성 업데이트
This commit is contained in:
simple
2025-04-24 23:53:24 +09:00
parent 06e9f0225d
commit 42419bae24
3 changed files with 240 additions and 2 deletions

197
index.ts
View File

@ -6,6 +6,7 @@ import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import FormData from "form-data";
import fetch from "node-fetch";
import { SocksProxyAgent } from 'socks-proxy-agent';
import { HttpsProxyAgent } from 'https-proxy-agent';
@ -75,6 +76,12 @@ import {
DeleteLabelSchema,
CreateNoteSchema,
ListGroupProjectsSchema,
ListWikiPagesSchema,
GetWikiPageSchema,
CreateWikiPageSchema,
UpdateWikiPageSchema,
DeleteWikiPageSchema,
GitLabWikiPageSchema,
// Discussion Schemas
GitLabDiscussionNoteSchema, // Added
GitLabDiscussionSchema,
@ -101,6 +108,11 @@ import {
// Discussion Types
type GitLabDiscussionNote, // Added
type GitLabDiscussion,
type GetWikiPageOptions,
type CreateWikiPageOptions,
type UpdateWikiPageOptions,
type DeleteWikiPageOptions,
type GitLabWikiPage,
} from "./schemas.js";
/**
@ -133,6 +145,7 @@ const server = new Server(
const GITLAB_PERSONAL_ACCESS_TOKEN = process.env.GITLAB_PERSONAL_ACCESS_TOKEN;
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
const HTTP_PROXY = process.env.HTTP_PROXY;
@ -348,6 +361,31 @@ const allTools = [
description: "List projects in a GitLab group with filtering options",
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
@ -371,6 +409,16 @@ const readOnlyTools = [
"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
*
@ -1787,11 +1835,126 @@ async function listGroupProjects(
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 () => {
// If read-only mode is enabled, filter out write operations
const tools = GITLAB_READ_ONLY_MODE
// Apply read-only filter first
const tools0 = GITLAB_READ_ONLY_MODE
? allTools.filter((tool) => readOnlyTools.includes(tool.name))
: allTools;
// Toggle wiki tools by USE_GITLAB_WIKI flag
const tools = USE_GITLAB_WIKI
? tools0
: tools0.filter((tool) => !wikiToolNames.includes(tool.name));
return {
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:
throw new Error(`Unknown tool: ${request.params.name}`);
}

View File

@ -23,6 +23,7 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "1.8.0",
"form-data": "^4.0.0",
"@types/node-fetch": "^2.6.12",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",

View File

@ -756,6 +756,44 @@ export const ListGroupProjectsSchema = z.object({
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 type GitLabAuthor = z.infer<typeof GitLabAuthorSchema>;
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 GitLabProject = z.infer<typeof GitLabProjectSchema>;
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>;