#!/usr/bin/env node
/**
* 这是一个模板 MCP 服务器,实现了一个简单的笔记系统。
* 它通过提供以下功能演示核心 MCP 概念:
* - 将笔记列为资源
* - 阅读单个笔记
* - 通过工具创建新笔记
* - 通过提示总结所有笔记
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import { CMSBlogClient } from './cms-blog.js'
/**
* 笔记对象的类型别名。
*/
type Note = { title: string; content: string }
/**
* 简单的内存笔记存储。
* 在真实实现中通常会使用数据库作为后端。
*/
const notes: { [id: string]: Note } = {
'1': { title: 'First Note', content: 'This is note 1' },
'2': { title: 'Second Note', content: 'This is note 2' },
}
/**
* CMS 博客客户端实例
*/
const cmsApiUrl = process.env.CMS_BLOG_API_URL
const cmsClient = cmsApiUrl ? new CMSBlogClient(cmsApiUrl) : new CMSBlogClient()
/**
* 创建一个 MCP 服务器,提供资源(列出/读取笔记)、工具(创建新笔记)和提示(总结笔记)等能力。
*/
const server = new Server(
{
name: 'seo-blog',
version: '0.1.0',
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
}
)
/**
* 用于将可用笔记列为资源的处理器。
* 每条笔记都会以以下形式暴露为资源:
* - note:// URI 协议
* - 纯文本 MIME 类型
* - 包含笔记标题的人类可读名称和描述
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: Object.entries(notes).map(([id, note]) => ({
uri: `note:///${id}`,
mimeType: 'text/plain',
name: note.title,
description: `A text note: ${note.title}`,
})),
}
})
/**
* 读取指定笔记内容的处理器。
* 接收一个 note:// URI,并以纯文本形式返回笔记内容。
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri)
const id = url.pathname.replace(/^\//, '')
const note = notes[id]
if (!note) {
throw new Error(`Note ${id} not found`)
}
return {
contents: [
{
uri: request.params.uri,
mimeType: 'text/plain',
text: note.content,
},
],
}
})
/**
* 列出可用工具的处理器。
* 提供一个 "create_note" 工具,允许客户端创建新笔记。
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_note',
description: 'Create a new note',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title of the note',
},
content: {
type: 'string',
description: 'Text content of the note',
},
},
required: ['title', 'content'],
},
},
{
name: 'write_note',
description: 'Write a new note',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title of the note',
},
content: {
type: 'string',
description: 'Text content of the note',
},
},
required: ['title', 'content'],
},
},
{
name: 'post_blog',
description: 'Post a blog article to the CMS API using Zod validation',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Title of the blog post',
},
description: {
type: 'string',
description: 'Description of the blog post',
},
content: {
type: 'string',
description: 'Content of the blog post',
},
slug: {
type: 'string',
description: 'URL slug for the blog post',
},
status: {
type: 'string',
description: 'Status of the blog post (draft), only draft is supported',
enum: ['draft'],
},
cover_url: {
type: 'string',
description: 'Cover image URL',
},
author_name: {
type: 'string',
description: 'Author name',
},
author_avatar_url: {
type: 'string',
description: 'Author avatar URL',
},
locale: {
type: 'string',
description: 'Language locale',
},
},
required: ['title', 'description', 'content', 'slug'],
},
},
{
name: 'validate_blog',
description: 'Validate blog data using Zod schema without posting',
inputSchema: {
type: 'object',
properties: {
title: { type: 'string' },
description: { type: 'string' },
content: { type: 'string' },
slug: { type: 'string' },
status: { type: 'string' },
cover_url: { type: 'string' },
author_name: { type: 'string' },
author_avatar_url: { type: 'string' },
locale: { type: 'string' },
},
required: ['title', 'description', 'content', 'slug'],
},
},
],
}
})
/**
* "create_note" 工具的处理器。
* 使用提供的标题和内容创建新笔记,并返回成功信息。
*/
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case 'create_note': {
const title = String(request.params.arguments?.title)
const content = String(request.params.arguments?.content)
if (!title || !content) {
throw new Error('Title and content are required')
}
const id = String(Object.keys(notes).length + 1)
notes[id] = { title, content }
return {
content: [
{
type: 'text',
text: `Created note ${id}: ${title}`,
},
],
}
}
case 'write_note': {
const title = String(request.params.arguments?.title)
const content = String(request.params.arguments?.content)
if (!title || !content) {
throw new Error('Title and content are required')
}
const id = String(Object.keys(notes).length + 1)
notes[id] = { title, content }
return {
content: [
{
type: 'text',
text: `Wrote note ${id}: ${title}`,
},
],
}
}
case 'post_blog': {
try {
// 使用 CMS 客户端发布博客,内置 Zod 验证
const result = await cmsClient.postBlog(request.params.arguments || {})
return {
content: [
{
type: 'text',
text: `Successfully posted blog!\nResponse: ${JSON.stringify(result, null, 2)}`,
},
],
}
} catch (error) {
throw new Error(`Failed to post blog: ${error instanceof Error ? error.message : String(error)}`)
}
}
case 'validate_blog': {
try {
// 验证博客数据但不发布
const validation = cmsClient.validateBlogData(request.params.arguments || {})
if (validation.success) {
return {
content: [
{
type: 'text',
text: `✅ Blog data validation successful!\nValidated data: ${JSON.stringify(validation.data, null, 2)}`,
},
],
}
} else {
return {
content: [
{
type: 'text',
text: `❌ Blog data validation failed!\nErrors: ${validation.errors?.join(', ')}`,
},
],
}
}
} catch (error) {
throw new Error(`Validation error: ${error instanceof Error ? error.message : String(error)}`)
}
}
default:
throw new Error('Unknown tool')
}
})
/**
* 列出可用提示的处理器。
* 提供一个 "summarize_notes" 提示,用于总结所有笔记。
*/
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: 'summarize_notes',
description: 'Summarize all notes',
},
{
name: 'post_blog',
description: 'use this prompt when publishing a cms blog',
},
],
}
})
/**
* "summarize_notes" 提示的处理器。
* 返回一个请求总结所有笔记的提示,并将笔记内容作为资源嵌入。
*/
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name !== 'summarize_notes') {
throw new Error('Unknown prompt')
}
const embeddedNotes = Object.entries(notes).map(([id, note]) => ({
type: 'resource' as const,
resource: {
uri: `note:///${id}`,
mimeType: 'text/plain',
text: note.content,
},
}))
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: 'Please summarize the following notes:',
},
},
...embeddedNotes.map((note) => ({
role: 'user' as const,
content: note,
})),
{
role: 'user',
content: {
type: 'text',
text: 'Provide a concise summary of all the notes above.',
},
},
{
role: 'user',
content: {
type: 'text',
text: 'Use the post_blog tool to publish the blog',
},
},
],
}
})
/**
* 使用 stdio 传输启动服务器。
* 这使服务器能够通过标准输入/输出流进行通信。
*/
async function main() {
const transport = new StdioServerTransport()
await server.connect(transport)
}
main().catch((error) => {
console.error('Server error:', error)
process.exit(1)
})