import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
Tool,
Resource,
ResourceTemplate,
} from '@modelcontextprotocol/sdk/types.js';
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { VergeApiClient } from './client/api.js';
import { AuthManager } from './auth/index.js';
import { handleBrowserLogin, handleLogin, handleGetCurrentUser, handleUpdateUser, handleLogout } from './tools/auth.js';
import {
handleListBlogs,
handleGetBlog,
handleCreateBlog,
handleUpdateBlog,
handleDeleteBlog,
} from './tools/blogs.js';
import {
handleListArticles,
handleGetArticle,
handleCreateArticle,
handleUpdateArticle,
handlePublishArticle,
handleDeleteArticle,
} from './tools/articles.js';
import {
LoginToolInputSchema,
UpdateUserToolInputSchema,
ListBlogsToolInputSchema,
GetBlogToolInputSchema,
CreateBlogToolInputSchema,
UpdateBlogToolInputSchema,
DeleteBlogToolInputSchema,
ListArticlesToolInputSchema,
GetArticleToolInputSchema,
CreateArticleToolInputSchema,
UpdateArticleToolInputSchema,
PublishArticleToolInputSchema,
DeleteArticleToolInputSchema,
} from './tools/schemas.js';
/**
* Verge MCP Server ファクトリ関数
* 統合テストでも再利用できるようにサーバー生成ロジックを抽出
*
* @param apiClient VergeApiClientのインスタンス (テストではモックを渡せる)
* @param authManager AuthManagerのインスタンス (オプション、ログアウト機能で使用)
* @returns 設定済みのMCPサーバーインスタンス
*/
export function createPlumeServer(apiClient: VergeApiClient, authManager?: AuthManager): Server {
const server = new Server(
{
name: 'verge-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// ツール定義
const tools: Tool[] = [
{
name: 'verge_browser_login',
description: 'ブラウザを開いてVergeCMSにログインします(OAuth認証)。保存されたトークンがある場合はそれを使用します。',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'verge_login',
description: 'メールアドレスとパスワードでVergeCMSにログインします(API認証)',
inputSchema: {
type: 'object',
properties: {
email: {
type: 'string',
description: 'ログインメールアドレス',
},
password: {
type: 'string',
description: 'パスワード',
},
},
required: ['email', 'password'],
},
},
{
name: 'verge_get_current_user',
description: '現在ログイン中のユーザー情報を取得します',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'verge_update_user',
description: 'ユーザー情報を更新します',
inputSchema: {
type: 'object',
properties: {
user_id: {
type: 'number',
description: 'ユーザーID',
},
name: {
type: 'string',
description: 'ユーザー名',
},
email: {
type: 'string',
description: 'メールアドレス',
},
password: {
type: 'string',
description: '新しいパスワード',
},
},
required: ['user_id'],
},
},
{
name: 'verge_list_blogs',
description: 'ブログ一覧を取得します',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'verge_get_blog',
description: 'ブログ詳細を取得します',
inputSchema: {
type: 'object',
properties: {
blog_id: {
type: 'number',
description: 'ブログID',
},
},
required: ['blog_id'],
},
},
{
name: 'verge_create_blog',
description: '新規ブログを作成します',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'ブログ名',
},
slug: {
type: 'string',
description: 'URL用スラッグ',
},
description: {
type: 'string',
description: 'ブログ説明',
},
},
required: ['name', 'slug'],
},
},
{
name: 'verge_update_blog',
description: 'ブログ情報を更新します',
inputSchema: {
type: 'object',
properties: {
blog_id: {
type: 'number',
description: 'ブログID',
},
name: {
type: 'string',
description: 'ブログ名',
},
slug: {
type: 'string',
description: 'URL用スラッグ',
},
description: {
type: 'string',
description: 'ブログ説明',
},
},
required: ['blog_id'],
},
},
{
name: 'verge_delete_blog',
description: 'ブログを削除します',
inputSchema: {
type: 'object',
properties: {
blog_id: {
type: 'number',
description: 'ブログID',
},
},
required: ['blog_id'],
},
},
{
name: 'verge_list_articles',
description: '記事一覧を取得します (検索・フィルタリング可能)',
inputSchema: {
type: 'object',
properties: {
blog_id: {
type: 'number',
description: 'ブログID',
},
search: {
type: 'string',
description: '検索キーワード (タイトル・本文)',
},
status: {
type: 'string',
enum: ['draft', 'published'],
description: '記事ステータス',
},
category_ids: {
type: 'array',
items: { type: 'number' },
description: 'フィルタするカテゴリID配列',
},
tag_ids: {
type: 'array',
items: { type: 'number' },
description: 'フィルタするタグID配列',
},
},
required: ['blog_id'],
},
},
{
name: 'verge_get_article',
description: '記事詳細を取得します',
inputSchema: {
type: 'object',
properties: {
article_id: {
type: 'number',
description: '記事ID',
},
blog_id: {
type: 'number',
description: 'ブログID',
},
},
required: ['article_id', 'blog_id'],
},
},
{
name: 'verge_create_article',
description: '新規記事を作成します',
inputSchema: {
type: 'object',
properties: {
blog_id: {
type: 'number',
description: 'ブログID',
},
title: {
type: 'string',
description: '記事タイトル',
},
content: {
type: 'string',
description: '記事本文 (Markdown形式)',
},
slug: {
type: 'string',
description: 'URL用スラッグ',
},
status: {
type: 'string',
enum: ['draft', 'published'],
description: '記事ステータス (デフォルト: draft)',
},
featured_image: {
type: 'string',
description: 'アイキャッチ画像URL',
},
excerpt: {
type: 'string',
description: '記事の抜粋',
},
author_id: {
type: 'number',
description: '著者ID',
},
category_ids: {
type: 'array',
items: { type: 'number' },
description: 'カテゴリID配列',
},
tag_ids: {
type: 'array',
items: { type: 'number' },
description: 'タグID配列',
},
},
required: ['blog_id', 'title', 'content', 'slug'],
},
},
{
name: 'verge_update_article',
description: '既存記事を更新します',
inputSchema: {
type: 'object',
properties: {
article_id: {
type: 'number',
description: '記事ID',
},
blog_id: {
type: 'number',
description: 'ブログID',
},
title: {
type: 'string',
description: '記事タイトル',
},
content: {
type: 'string',
description: '記事本文 (Markdown形式)',
},
slug: {
type: 'string',
description: 'URL用スラッグ',
},
status: {
type: 'string',
enum: ['draft', 'published'],
description: '記事ステータス',
},
featured_image: {
type: 'string',
description: 'アイキャッチ画像URL',
},
excerpt: {
type: 'string',
description: '記事の抜粋',
},
author_id: {
type: 'number',
description: '著者ID',
},
category_ids: {
type: 'array',
items: { type: 'number' },
description: 'カテゴリID配列',
},
tag_ids: {
type: 'array',
items: { type: 'number' },
description: 'タグID配列',
},
},
required: ['article_id', 'blog_id'],
},
},
{
name: 'verge_publish_article',
description: '記事を公開状態に変更します',
inputSchema: {
type: 'object',
properties: {
article_id: {
type: 'number',
description: '記事ID',
},
blog_id: {
type: 'number',
description: 'ブログID',
},
},
required: ['article_id', 'blog_id'],
},
},
{
name: 'verge_delete_article',
description: '記事を削除します',
inputSchema: {
type: 'object',
properties: {
article_id: {
type: 'number',
description: '記事ID',
},
blog_id: {
type: 'number',
description: 'ブログID',
},
},
required: ['article_id', 'blog_id'],
},
},
{
name: 'verge_logout',
description: 'ログアウトして保存されたトークンを削除します',
inputSchema: {
type: 'object',
properties: {},
},
},
];
// ツール一覧リクエスト
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// ツール実行リクエスト
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'verge_browser_login': {
if (!authManager) {
throw new McpError(ErrorCode.InternalError, 'AuthManager is not configured');
}
return await handleBrowserLogin(authManager, apiClient);
}
case 'verge_login': {
const input = LoginToolInputSchema.parse(args);
return await handleLogin(input, apiClient);
}
case 'verge_get_current_user': {
return await handleGetCurrentUser(apiClient);
}
case 'verge_update_user': {
const input = UpdateUserToolInputSchema.parse(args);
return await handleUpdateUser(input, apiClient);
}
case 'verge_list_blogs': {
const input = ListBlogsToolInputSchema.parse(args);
return await handleListBlogs(input, apiClient);
}
case 'verge_get_blog': {
const input = GetBlogToolInputSchema.parse(args);
return await handleGetBlog(input, apiClient);
}
case 'verge_create_blog': {
const input = CreateBlogToolInputSchema.parse(args);
return await handleCreateBlog(input, apiClient);
}
case 'verge_update_blog': {
const input = UpdateBlogToolInputSchema.parse(args);
return await handleUpdateBlog(input, apiClient);
}
case 'verge_delete_blog': {
const input = DeleteBlogToolInputSchema.parse(args);
return await handleDeleteBlog(input, apiClient);
}
case 'verge_list_articles': {
const input = ListArticlesToolInputSchema.parse(args);
return await handleListArticles(input, apiClient);
}
case 'verge_get_article': {
const input = GetArticleToolInputSchema.parse(args);
return await handleGetArticle(input, apiClient);
}
case 'verge_create_article': {
const input = CreateArticleToolInputSchema.parse(args);
return await handleCreateArticle(input, apiClient);
}
case 'verge_update_article': {
const input = UpdateArticleToolInputSchema.parse(args);
return await handleUpdateArticle(input, apiClient);
}
case 'verge_publish_article': {
const input = PublishArticleToolInputSchema.parse(args);
return await handlePublishArticle(input, apiClient);
}
case 'verge_delete_article': {
const input = DeleteArticleToolInputSchema.parse(args);
return await handleDeleteArticle(input, apiClient);
}
case 'verge_logout': {
if (!authManager) {
return {
isError: true,
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: false,
error: 'AuthManager is not configured',
},
null,
2
),
},
],
};
}
return await handleLogout(authManager);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
if (error instanceof Error) {
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(
{
success: false,
error: error.message,
},
null,
2
),
},
],
isError: true,
};
}
throw error;
}
});
// リソース定義
const blogListResource: Resource = {
uri: 'blogs://list',
name: 'Blog List',
description: '認証済みユーザーがアクセスできるブログの一覧(最新20件)',
mimeType: 'application/json',
};
const articleResourceTemplates: ResourceTemplate[] = [
{
uriTemplate: 'articles://blog/{blogId}',
name: 'Articles in Blog',
description: '指定されたブログの記事一覧(最新20件)。{blogId}にブログIDを指定してください',
mimeType: 'application/json',
},
];
// リソース一覧リクエスト
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [blogListResource],
resourceTemplates: articleResourceTemplates,
};
});
// リソース読み取りリクエスト
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
try {
// ブログリストリソース
if (uri === 'blogs://list') {
const blogs = await apiClient.listBlogs();
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(
{
total: blogs.length,
blogs: blogs.slice(0, 20).map(blog => ({
id: blog.id,
name: blog.name,
description: blog.description,
created_at: blog.created_at,
})),
},
null,
2
),
},
],
};
}
// 記事リストリソーステンプレート
const articleMatch = uri.match(/^articles:\/\/blog\/(?<blogId>\d+)$/);
if (articleMatch?.groups?.blogId) {
const blogId = Number(articleMatch.groups.blogId);
const articles = await apiClient.listArticles(blogId);
return {
contents: [
{
uri,
mimeType: 'application/json',
text: JSON.stringify(
{
blogId,
total: articles.length,
articles: articles.slice(0, 20).map(article => ({
id: article.id,
title: article.title,
status: article.status,
published_at: article.published_at,
created_at: article.created_at,
})),
},
null,
2
),
},
],
};
}
// 未知のリソース
throw new McpError(
ErrorCode.InvalidRequest,
`Unknown resource URI: ${uri}`
);
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
`Failed to read resource: ${error instanceof Error ? error.message : 'Unknown error'}`
);
}
});
return server;
}