Github-Oauth MCP Server

  • src
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import { getPost, getPosts, searchPosts, createPost, updatePost, deletePost, getPostBySlug, getPage, getPages, createPage, updatePage, deletePage, getPageBySlug, getTags, getAuthors, getMember, getMembers, searchMembers, createMember, updateMember, deleteMember, uploadImage, toolSchemas } from './tools/index.js'; import { isPaginationParams, isSearchParams, isMemberPaginationParams, isMemberSearchParams, isCreateMemberParams, isUpdateMemberParams, isImageUploadParams, PostFormat, PostInclude, PostStatus, PostVisibility, MemberInclude, CreateMemberParams, UpdateMemberParams } from './types/index.js'; const isPostStatus = (value: string): value is PostStatus => ['published', 'draft', 'scheduled'].includes(value); const isPostVisibility = (value: string): value is PostVisibility => ['public', 'members', 'paid', 'tiers'].includes(value); class GhostServer { private server: Server; constructor() { this.server = new Server( { name: 'ghost-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolSchemas, })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { try { const args = request.params.arguments || {}; switch (request.params.name) { case 'get_posts': if (!isPaginationParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid pagination parameters'); } return getPosts(args); case 'get_post': { const id = args.id; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return getPost({ id }); } case 'search_posts': if (!isSearchParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid search parameters'); } return searchPosts(args); case 'get_tags': if (!isPaginationParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid pagination parameters'); } return getTags(args); case 'get_authors': if (!isPaginationParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid pagination parameters'); } return getAuthors(args); case 'create_post': { const { title, html, lexical, status, visibility, published_at, tags, authors, featured, email_subject, email_only, newsletter } = args; if (!title || typeof title !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Title is required and must be a string'); } return createPost({ title, html: typeof html === 'string' ? html : undefined, lexical: typeof lexical === 'string' ? lexical : undefined, status: typeof status === 'string' && isPostStatus(status) ? status : undefined, visibility: typeof visibility === 'string' && isPostVisibility(visibility) ? visibility : undefined, published_at: typeof published_at === 'string' ? published_at : undefined, tags: Array.isArray(tags) ? tags.filter((t): t is string => typeof t === 'string') : undefined, authors: Array.isArray(authors) ? authors.filter((a): a is string => typeof a === 'string') : undefined, featured: typeof featured === 'boolean' ? featured : undefined, email_subject: typeof email_subject === 'string' ? email_subject : undefined, email_only: typeof email_only === 'boolean' ? email_only : undefined, newsletter: typeof newsletter === 'boolean' ? newsletter : undefined, }); } case 'update_post': { const { id, title, html, lexical, status, visibility, published_at, tags, authors, featured, email_subject, email_only, newsletter } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return updatePost({ id, title: typeof title === 'string' ? title : undefined, html: typeof html === 'string' ? html : undefined, lexical: typeof lexical === 'string' ? lexical : undefined, status: typeof status === 'string' && isPostStatus(status) ? status : undefined, visibility: typeof visibility === 'string' && isPostVisibility(visibility) ? visibility : undefined, published_at: typeof published_at === 'string' ? published_at : undefined, tags: Array.isArray(tags) ? tags.filter((t): t is string => typeof t === 'string') : undefined, authors: Array.isArray(authors) ? authors.filter((a): a is string => typeof a === 'string') : undefined, featured: typeof featured === 'boolean' ? featured : undefined, email_subject: typeof email_subject === 'string' ? email_subject : undefined, email_only: typeof email_only === 'boolean' ? email_only : undefined, newsletter: typeof newsletter === 'boolean' ? newsletter : undefined, updated_at: new Date().toISOString(), }); } case 'delete_post': { const { id } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return deletePost({ id }); } case 'get_post_by_slug': { const { slug, formats, include } = args; if (typeof slug !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Slug must be a string'); } return getPostBySlug({ slug, formats: Array.isArray(formats) ? formats.filter((f): f is PostFormat => typeof f === 'string' && ['html', 'mobiledoc', 'lexical'].includes(f)) : undefined, include: Array.isArray(include) ? include.filter((i): i is PostInclude => typeof i === 'string' && ['authors', 'tags'].includes(i)) : undefined, }); } // Pages case 'get_pages': if (!isPaginationParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid pagination parameters'); } return getPages(args); case 'get_page': { const id = args.id; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return getPage({ id }); } case 'create_page': { const { title, html, lexical, status, visibility, published_at, tags, authors, featured } = args; if (!title || typeof title !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Title is required and must be a string'); } return createPage({ title, html: typeof html === 'string' ? html : undefined, lexical: typeof lexical === 'string' ? lexical : undefined, status: typeof status === 'string' && isPostStatus(status) ? status : undefined, visibility: typeof visibility === 'string' && isPostVisibility(visibility) ? visibility : undefined, published_at: typeof published_at === 'string' ? published_at : undefined, tags: Array.isArray(tags) ? tags.filter((t): t is string => typeof t === 'string') : undefined, authors: Array.isArray(authors) ? authors.filter((a): a is string => typeof a === 'string') : undefined, featured: typeof featured === 'boolean' ? featured : undefined, }); } case 'update_page': { const { id, title, html, lexical, status, visibility, published_at, tags, authors, featured } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return updatePage({ id, title: typeof title === 'string' ? title : undefined, html: typeof html === 'string' ? html : undefined, lexical: typeof lexical === 'string' ? lexical : undefined, status: typeof status === 'string' && isPostStatus(status) ? status : undefined, visibility: typeof visibility === 'string' && isPostVisibility(visibility) ? visibility : undefined, published_at: typeof published_at === 'string' ? published_at : undefined, tags: Array.isArray(tags) ? tags.filter((t): t is string => typeof t === 'string') : undefined, authors: Array.isArray(authors) ? authors.filter((a): a is string => typeof a === 'string') : undefined, featured: typeof featured === 'boolean' ? featured : undefined, updated_at: new Date().toISOString(), }); } case 'delete_page': { const { id } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return deletePage({ id }); } case 'get_page_by_slug': { const { slug, formats, include } = args; if (typeof slug !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'Slug must be a string'); } return getPageBySlug({ slug, formats: Array.isArray(formats) ? formats.filter((f): f is PostFormat => typeof f === 'string' && ['html', 'mobiledoc', 'lexical'].includes(f)) : undefined, include: Array.isArray(include) ? include.filter((i): i is PostInclude => typeof i === 'string' && ['authors', 'tags'].includes(i)) : undefined, }); } // Members case 'get_members': if (!isMemberPaginationParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid pagination parameters'); } return getMembers(args); case 'get_member': { const { id, include } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return getMember({ id, include: Array.isArray(include) ? include.filter((i): i is MemberInclude => typeof i === 'string' && ['labels', 'newsletters'].includes(i)) : undefined, }); } case 'search_members': if (!isMemberSearchParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid search parameters'); } return searchMembers(args); case 'create_member': if (!isCreateMemberParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid member parameters'); } return createMember(args as unknown as CreateMemberParams); case 'update_member': if (!isUpdateMemberParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid member parameters'); } return updateMember(args as unknown as UpdateMemberParams); case 'delete_member': { const { id } = args; if (typeof id !== 'string') { throw new McpError(ErrorCode.InvalidParams, 'ID must be a string'); } return deleteMember({ id }); } // Images case 'upload_image': if (!isImageUploadParams(args)) { throw new McpError(ErrorCode.InvalidParams, 'Invalid image upload parameters'); } return uploadImage(args); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } console.error('Ghost API Error:', error); throw new McpError( ErrorCode.InternalError, `Ghost API error: ${(error as Error).message}` ); } }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Ghost MCP server running on stdio'); } } const server = new GhostServer(); server.run().catch(console.error);