WordPress MCP 서버 제작 가이드: AI로 워드프레스를 자유자재로 관리하기




목차

  1. 들어가며
  2. 1단계: 프로젝트 초기 설정
  3. 2단계: TypeScript 설정
  4. 3단계: WordPress 타입 정의
  5. 4단계: WordPress API 클라이언트 구현
  6. 5단계: 도구 클래스 구현
  7. 6단계: 메인 서버 구현
  8. 7단계: WordPress 설정 및 서버 실행
  9. 8단계: 빌드 및 실행
  10. 9단계: 실제 사용 예시
  11. 고급 기능 확장
  12. 마무리

들어가며

Claude와 같은 AI 어시스턴트가 여러분의 WordPress 사이트를 직접 관리할 수 있다면 어떨까요? 포스트를 작성하고, 카테고리를 만들고, 미디어를 업로드하는 모든 작업을 자연어로 명령할 수 있다면 말이죠.

이 가이드에서는 Model Context Protocol(MCP)을 사용하여 WordPress와 AI를 연결하는 완전한 서버를 처음부터 끝까지 만들어보겠습니다. 실제 테스트를 완료한 코드를 기반으로 하므로, 따라하시면 바로 사용할 수 있는 완성된 솔루션을 얻을 수 있습니다.


1단계: 프로젝트 초기 설정

프로젝트 구조 생성

먼저 프로젝트 폴더를 생성하고 기본 구조를 만들어보겠습니다.

mkdir wordpress-mcp-server
cd wordpress-mcp-server
npm init -y

필수 패키지 설치

# 런타임 의존성
npm install @modelcontextprotocol/sdk axios dotenv zod

# 개발 의존성
npm install -D @types/node tsx typescript

폴더 구조 생성

mkdir src src/tools src/types src/utils

최종 구조:

wordpress-mcp-server/
├── src/
 │   ├── tools/
 │   ├── types/
 │   ├── utils/
 │   └── index.ts
├── package.json
└── tsconfig.json

2단계: TypeScript 설정

tsconfig.json 생성

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

package.json 스크립트 추가

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/index.js",
    "dev": "tsx src/index.ts",
    "watch": "tsx watch src/index.ts"
  }
}

3단계: WordPress 타입 정의

src/types/wordpress.ts 생성

WordPress REST API의 모든 타입을 정의합니다:

export interface WordPressPost {
  id: number;
  date: string;
  date_gmt: string;
  guid: {
    rendered: string;
  };
  modified: string;
  modified_gmt: string;
  slug: string;
  status: 'publish' | 'future' | 'draft' | 'pending' | 'private';
  type: string;
  link: string;
  title: {
    rendered: string;
  };
  content: {
    rendered: string;
    protected: boolean;
  };
  excerpt: {
    rendered: string;
    protected: boolean;
  };
  author: number;
  featured_media: number;
  comment_status: 'open' | 'closed';
  ping_status: 'open' | 'closed';
  sticky: boolean;
  template: string;
  format: string;
  meta: any[];
  categories: number[];
  tags: number[];
  _links: {
    self: Array<{ href: string }>;
    collection: Array<{ href: string }>;
    about: Array<{ href: string }>;
    author: Array<{ embeddable: boolean; href: string }>;
    replies: Array<{ embeddable: boolean; href: string }>;
    'version-history': Array<{ count: number; href: string }>;
    'predecessor-version': Array<{ id: number; href: string }>;
    'wp:featuredmedia': Array<{ embeddable: boolean; href: string }>;
    'wp:attachment': Array<{ href: string }>;
    'wp:term': Array<{ taxonomy: string; embeddable: boolean; href: string }>;
    curies: Array<{ name: string; href: string; templated: boolean }>;
  };
}

export interface CreatePostRequest {
  title: string;
  content: string;
  excerpt?: string;
  status?: 'publish' | 'draft' | 'private';
  categories?: number[];
  tags?: number[];
  featured_media?: number;
  slug?: string;
}

export interface WordPressCategory {
  id: number;
  count: number;
  description: string;
  link: string;
  name: string;
  slug: string;
  taxonomy: string;
  parent: number;
  meta: any[];
  _links: {
    self: Array<{ href: string }>;
    collection: Array<{ href: string }>;
    about: Array<{ href: string }>;
    'wp:post_type': Array<{ href: string }>;
    curies: Array<{ name: string; href: string; templated: boolean }>;
  };
}

export interface CreateCategoryRequest {
  name: string;
  description?: string;
  slug?: string;
  parent?: number;
}

export interface WordPressMedia {
  id: number;
  date: string;
  date_gmt: string;
  guid: {
    rendered: string;
  };
  modified: string;
  modified_gmt: string;
  slug: string;
  status: 'inherit' | 'private' | 'trash';
  type: string;
  link: string;
  title: {
    rendered: string;
  };
  author: number;
  comment_status: 'open' | 'closed';
  ping_status: 'open' | 'closed';
  template: string;
  meta: any[];
  description: {
    rendered: string;
  };
  caption: {
    rendered: string;
  };
  alt_text: string;
  media_type: string;
  mime_type: string;
  media_details: {
    width: number;
    height: number;
    file: string;
    sizes: Record<string, {
      file: string;
      width: number;
      height: number;
      mime_type: string;
      source_url: string;
    }>;
    image_meta: any;
  };
  post: number;
  source_url: string;
  _links: {
    self: Array<{ href: string }>;
    collection: Array<{ href: string }>;
    about: Array<{ href: string }>;
    author: Array<{ embeddable: boolean; href: string }>;
    replies: Array<{ embeddable: boolean; href: string }>;
  };
}

export interface WordPressUser {
  id: number;
  name: string;
  url: string;
  description: string;
  link: string;
  slug: string;
  avatar_urls: Record<string, string>;
  meta: any[];
  _links: {
    self: Array<{ href: string }>;
    collection: Array<{ href: string }>;
  };
}

export interface GetPostsOptions {
  page?: number;
  per_page?: number;
  search?: string;
  categories?: number[];
  status?: string;
  orderby?: string;
  order?: 'asc' | 'desc';
}

4단계: WordPress API 클라이언트 구현

src/utils/api.ts 생성

WordPress REST API와 통신하는 클라이언트를 만듭니다:

import axios, { AxiosInstance } from 'axios';
import {
  WordPressPost,
  CreatePostRequest,
  WordPressCategory,
  CreateCategoryRequest,
  WordPressMedia,
  WordPressUser,
  GetPostsOptions
} from '../types/wordpress.js';

export class WordPressAPI {
  private client: AxiosInstance;
  private baseURL: string;
  private apiBase: string;

  constructor() {
    this.baseURL = process.env.WORDPRESS_URL || '';
    this.apiBase = process.env.WORDPRESS_API_BASE || '/wp-json/wp/v2';
    
    if (!this.baseURL) {
      throw new Error('WORDPRESS_URL environment variable is required');
    }

    const username = process.env.WORDPRESS_USERNAME;
    const password = process.env.WORDPRESS_APPLICATION_PASSWORD || process.env.WORDPRESS_PASSWORD;

    if (!username || !password) {
      throw new Error('WORDPRESS_USERNAME and WORDPRESS_APPLICATION_PASSWORD environment variables are required');
    }

    // Basic authentication for WordPress REST API
    const auth = Buffer.from(`${username}:${password}`).toString('base64');

    this.client = axios.create({
      baseURL: this.baseURL,
      headers: {
        'Authorization': `Basic ${auth}`,
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }
    });
  }

  // Posts
  async createPost(postData: CreatePostRequest): Promise<WordPressPost> {
    try {
      const response = await this.client.post(`${this.apiBase}/posts`, postData);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to create post: ${this.getErrorMessage(error)}`);
    }
  }

  async getPosts(options: GetPostsOptions = {}): Promise<WordPressPost[]> {
    try {
      const params = new URLSearchParams();
      
      if (options.page) params.append('page', options.page.toString());
      if (options.per_page) params.append('per_page', options.per_page.toString());
      if (options.search) params.append('search', options.search);
      if (options.status) params.append('status', options.status);
      if (options.orderby) params.append('orderby', options.orderby);
      if (options.order) params.append('order', options.order);
      if (options.categories) {
        options.categories.forEach(catId => params.append('categories', catId.toString()));
      }

      const response = await this.client.get(`${this.apiBase}/posts?${params.toString()}`);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get posts: ${this.getErrorMessage(error)}`);
    }
  }

  async getPost(id: number): Promise<WordPressPost> {
    try {
      const response = await this.client.get(`${this.apiBase}/posts/${id}`);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get post ${id}: ${this.getErrorMessage(error)}`);
    }
  }

  // Categories
  async getCategories(): Promise<WordPressCategory[]> {
    try {
      const response = await this.client.get(`${this.apiBase}/categories`);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get categories: ${this.getErrorMessage(error)}`);
    }
  }

  async createCategory(categoryData: CreateCategoryRequest): Promise<WordPressCategory> {
    try {
      const response = await this.client.post(`${this.apiBase}/categories`, categoryData);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to create category: ${this.getErrorMessage(error)}`);
    }
  }

  // Media
  async uploadMedia(filePath: string, fileName: string): Promise<WordPressMedia> {
    try {
      // For simplicity, we'll use a file URL approach
      // In a real implementation, you'd need to handle file uploads properly
      const mediaData = {
        file: filePath,
        title: fileName,
        caption: '',
        alt_text: ''
      };

      const response = await this.client.post(`${this.apiBase}/media`, mediaData);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to upload media: ${this.getErrorMessage(error)}`);
    }
  }

  async getMedia(id: number): Promise<WordPressMedia> {
    try {
      const response = await this.client.get(`${this.apiBase}/media/${id}`);
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get media ${id}: ${this.getErrorMessage(error)}`);
    }
  }

  // Users
  async getCurrentUser(): Promise<WordPressUser> {
    try {
      const response = await this.client.get('/wp-json/wp/v2/users/me');
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get current user: ${this.getErrorMessage(error)}`);
    }
  }

  async getUsers(): Promise<WordPressUser[]> {
    try {
      const response = await this.client.get('/wp-json/wp/v2/users');
      return response.data;
    } catch (error) {
      throw new Error(`Failed to get users: ${this.getErrorMessage(error)}`);
    }
  }

  private getErrorMessage(error: any): string {
    if (error.response?.data?.message) {
      return error.response.data.message;
    }
    if (error.response?.statusText) {
      return `${error.response.status}: ${error.response.statusText}`;
    }
    return error.message || 'Unknown error';
  }
}

5단계: 도구 클래스 구현

포스트 관리 도구 (src/tools/posts.ts)

import { WordPressAPI } from '../utils/api.js';
import { CreatePostRequest, GetPostsOptions } from '../types/wordpress.js';

export class PostTools {
  private api: WordPressAPI;

  constructor(api: WordPressAPI) {
    this.api = api;
  }

  async createPost(
    title: string,
    content: string,
    excerpt?: string,
    status: 'publish' | 'draft' | 'private' = 'draft',
    categories?: number[],
    tags?: number[],
    featuredMedia?: number,
    slug?: string
  ): Promise<string> {
    try {
      const postData: CreatePostRequest = {
        title,
        content,
        excerpt,
        status,
        categories,
        tags,
        featured_media: featuredMedia,
        slug
      };

      const post = await this.api.createPost(postData);
      
      return `Post created successfully!
ID: ${post.id}
Title: ${post.title.rendered}
Status: ${post.status}
Link: ${post.link}
Slug: ${post.slug}`;
    } catch (error) {
      throw new Error(`Failed to create post: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  async getPosts(
    page: number = 1,
    perPage: number = 10,
    search?: string,
    categories?: number[],
    status?: string,
    orderBy: string = 'date',
    order: 'asc' | 'desc' = 'desc'
  ): Promise<string> {
    try {
      const options: GetPostsOptions = {
        page,
        per_page: perPage,
        search,
        categories,
        status,
        orderby: orderBy,
        order
      };

      const posts = await this.api.getPosts(options);
      
      if (posts.length === 0) {
        return 'No posts found matching the criteria.';
      }

      const postList = posts.map(post => `
ID: ${post.id}
Title: ${post.title.rendered}
Status: ${post.status}
Date: ${new Date(post.date).toLocaleDateString()}
Link: ${post.link}
Excerpt: ${post.excerpt.rendered.substring(0, 100)}${post.excerpt.rendered.length > 100 ? '...' : ''}
---`).join('\n');

      return `Found ${posts.length} posts:\n${postList}`;
    } catch (error) {
      throw new Error(`Failed to get posts: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }
}

카테고리 관리 도구 (src/tools/categories.ts)

import { WordPressAPI } from '../utils/api.js';
import { CreateCategoryRequest } from '../types/wordpress.js';

export class CategoryTools {
  private api: WordPressAPI;

  constructor(api: WordPressAPI) {
    this.api = api;
  }

  async getCategories(): Promise<string> {
    try {
      const categories = await this.api.getCategories();
      
      if (categories.length === 0) {
        return 'No categories found.';
      }

      const categoryList = categories.map(category => `
ID: ${category.id}
Name: ${category.name}
Slug: ${category.slug}
Description: ${category.description}
Count: ${category.count} posts
---`).join('\n');

      return `Found ${categories.length} categories:\n${categoryList}`;
    } catch (error) {
      throw new Error(`Failed to get categories: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  async createCategory(
    name: string,
    description?: string,
    slug?: string,
    parent?: number
  ): Promise<string> {
    try {
      const categoryData: CreateCategoryRequest = {
        name,
        description,
        slug,
        parent
      };

      const category = await this.api.createCategory(categoryData);
      
      return `Category created successfully!
ID: ${category.id}
Name: ${category.name}
Slug: ${category.slug}
Description: ${category.description}`;
    } catch (error) {
      throw new Error(`Failed to create category: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }
}

미디어 관리 도구 (src/tools/media.ts)

import { WordPressAPI } from '../utils/api.js';

export class MediaTools {
  private api: WordPressAPI;

  constructor(api: WordPressAPI) {
    this.api = api;
  }

  async uploadMedia(filePath: string, fileName: string): Promise<string> {
    try {
      const media = await this.api.uploadMedia(filePath, fileName);
      
      return `Media uploaded successfully!
ID: ${media.id}
Title: ${media.title.rendered}
URL: ${media.source_url}
Type: ${media.media_type}
MIME Type: ${media.mime_type}
File: ${media.media_details.file}`;
    } catch (error) {
      throw new Error(`Failed to upload media: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  async getMedia(id: number): Promise<string> {
    try {
      const media = await this.api.getMedia(id);
      
      return `Media Details:
ID: ${media.id}
Title: ${media.title.rendered}
URL: ${media.source_url}
Type: ${media.media_type}
MIME Type: ${media.mime_type}
File: ${media.media_details.file}
Dimensions: ${media.media_details.width}x${media.media_details.height}
Caption: ${media.caption.rendered}
Alt Text: ${media.alt_text}`;
    } catch (error) {
      throw new Error(`Failed to get media ${id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }
}

사용자 관리 도구 (src/tools/users.ts)

import { WordPressAPI } from '../utils/api.js';

export class UserTools {
  private api: WordPressAPI;

  constructor(api: WordPressAPI) {
    this.api = api;
  }

  async getCurrentUser(): Promise<string> {
    try {
      const user = await this.api.getCurrentUser();
      
      return `Current User:
ID: ${user.id}
Name: ${user.name}
Slug: ${user.slug}
Description: ${user.description}
URL: ${user.url}
Avatar URLs: ${Object.keys(user.avatar_urls).join(', ')}`;
    } catch (error) {
      throw new Error(`Failed to get current user: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }

  async getUsers(): Promise<string> {
    try {
      const users = await this.api.getUsers();
      
      if (users.length === 0) {
        return 'No users found.';
      }

      const userList = users.map(user => `
ID: ${user.id}
Name: ${user.name}
Slug: ${user.slug}
Description: ${user.description}
URL: ${user.url}
---`).join('\n');

      return `Found ${users.length} users:\n${userList}`;
    } catch (error) {
      throw new Error(`Failed to get users: ${error instanceof Error ? error.message : 'Unknown error'}`);
    }
  }
}

6단계: 메인 서버 구현

src/index.ts 생성

이제 모든 것을 하나로 합치는 메인 서버를 만듭니다:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { WordPressAPI } from './utils/api.js';
import { PostTools } from './tools/posts.js';
import { CategoryTools } from './tools/categories.js';
import { MediaTools } from './tools/media.js';
import { UserTools } from './tools/users.js';
import { z } from 'zod';

// Parse command line arguments
const args = process.argv.slice(2);
const apiConfig = {
  url: args.find(arg => arg.startsWith('--url='))?.split('=')[1] || process.env.WORDPRESS_URL,
  username: args.find(arg => arg.startsWith('--username='))?.split('=')[1] || process.env.WORDPRESS_USERNAME,
  password: args.find(arg => arg.startsWith('--password='))?.split('=')[1] || process.env.WORDPRESS_APPLICATION_PASSWORD,
  apiBase: args.find(arg => arg.startsWith('--api-base='))?.split('=')[1] || process.env.WORDPRESS_API_BASE || '/wp-json/wp/v2'
};

// Check if we should run in demo mode
const isDemoMode = !apiConfig.url || !apiConfig.username || !apiConfig.password;

async function main() {
  try {
    if (isDemoMode) {
      console.error('Running in DEMO MODE - no actual WordPress site connected');
    } else {
      // Set environment variables for WordPressAPI
      process.env.WORDPRESS_URL = apiConfig.url;
      process.env.WORDPRESS_USERNAME = apiConfig.username;
      process.env.WORDPRESS_APPLICATION_PASSWORD = apiConfig.password;
      process.env.WORDPRESS_API_BASE = apiConfig.apiBase;
    }

    // Initialize WordPress API (or dummy)
    let api: WordPressAPI | null = null;
    let postTools: PostTools | null = null;
    let categoryTools: CategoryTools | null = null;
    let mediaTools: MediaTools | null = null;
    let userTools: UserTools | null = null;

    if (!isDemoMode) {
      api = new WordPressAPI();
      postTools = new PostTools(api);
      categoryTools = new CategoryTools(api);
      mediaTools = new MediaTools(api);
      userTools = new UserTools(api);
    }

    // Create MCP server
    const server = new McpServer({
      name: 'wordpress-mcp-server',
      version: '1.0.0',
    });

    // Register create_post tool
    server.registerTool(
      'create_post',
      {
        title: 'Create Post',
        description: isDemoMode ? 'Create a new WordPress post (Demo)' : 'Create a new WordPress post',
        inputSchema: {
          title: z.string(),
          content: z.string(),
          excerpt: z.string().optional(),
          status: z.enum(['publish', 'draft', 'private']).default('draft'),
          categories: z.array(z.number()).optional(),
          tags: z.array(z.number()).optional(),
          featured_media: z.number().optional(),
          slug: z.string().optional(),
        },
      },
      async (args: any) => {
        if (isDemoMode) {
          return {
            content: [
              {
                type: 'text',
                text: `✅ Post created successfully! (Demo Mode)
Title: ${args.title}
Content: ${args.content.substring(0, 100)}${args.content.length > 100 ? '...' : ''}
Status: ${args.status}
Post ID: ${Math.floor(Math.random() * 1000) + 1}
(Note: This is a demo response - no actual WordPress site connected)`,
              },
            ],
          };
        }
        
        return {
          content: [
            {
              type: 'text',
              text: await postTools!.createPost(
                args.title,
                args.content,
                args.excerpt,
                args.status,
                args.categories,
                args.tags,
                args.featured_media,
                args.slug
              ),
            },
          ],
        };
      }
    );

    // Register get_posts tool
    server.registerTool(
      'get_posts',
      {
        title: 'Get Posts',
        description: isDemoMode ? 'Get a list of WordPress posts (Demo)' : 'Get a list of WordPress posts',
        inputSchema: {
          page: z.number().default(1),
          per_page: z.number().default(10),
          search: z.string().optional(),
          categories: z.array(z.number()).optional(),
          status: z.string().optional(),
          orderby: z.string().default('date'),
          order: z.enum(['asc', 'desc']).default('desc'),
        },
      },
      async (args: any) => {
        if (isDemoMode) {
          const dummyPosts = [
            { id: 1, title: 'Sample Post 1', excerpt: 'This is a sample post for testing.' },
            { id: 2, title: 'Sample Post 2', excerpt: 'Another sample post for demonstration.' },
            { id: 3, title: 'Sample Post 3', excerpt: 'Third sample post to show functionality.' },
          ];

          return {
            content: [
              {
                type: 'text',
                text: `📝 Found ${dummyPosts.length} posts (Page ${args.page}, ${args.per_page} per page):

${dummyPosts.map(post => 
  `ID: ${post.id}
Title: ${post.title}
Excerpt: ${post.excerpt}
---`
).join('\n')}

(Note: This is demo data - no actual WordPress site connected)`,
              },
            ],
          };
        }

        return {
          content: [
            {
              type: 'text',
              text: await postTools!.getPosts(
                args.page,
                args.per_page,
                args.search,
                args.categories,
                args.status,
                args.orderby,
                args.order
              ),
            },
          ],
        };
      }
    );

    // Register other tools (categories, media, users)
    // ... (나머지 도구 등록 코드는 비슷한 패턴으로 구현)

    // Start the server
    const transport = new StdioServerTransport();
    await server.connect(transport);
    
    console.error(`WordPress MCP Server started${isDemoMode ? ' (Demo Mode)' : ''}`);
  } catch (error) {
    console.error('Failed to start WordPress MCP Server:', error);
    process.exit(1);
  }
}

main();

7단계: WordPress 설정 및 서버 실행

WordPress Application Password 생성

  1. WordPress 관리자 페이지로 이동
  2. 사용자프로필 클릭
  3. 애플리케이션 비밀번호 섹션에서 새 비밀번호 생성
  4. 생성된 비밀번호를 저장해 둡니다

서버 실행 방법

현재 코드는 명령줄 인자를 통해 WordPress 설정을 받도록 되어 있습니다:

# 데모 모드로 실행 (WordPress 연결 없이)
npm run dev

# 실제 WordPress 연결하여 실행
npm run dev -- --url=https://your-wordpress-site.com --username=your_username --password=your_application_password

# 또는 환경 변수로 설정
WORDPRESS_URL=https://your-site.com WORDPRESS_USERNAME=your_username WORDPRESS_APPLICATION_PASSWORD=your_application_password

설정 방법 비교

1. 명령줄 인자 방식 (현재 코드)

npm start -- --url=https://your-wordpress-site.com --username=your_username --password=your_application_password

2. 환경 변수 방식

export WORDPRESS_URL=https://your-wordpress-site.com
export WORDPRESS_USERNAME=your_username
export WORDPRESS_APPLICATION_PASSWORD=your_application_password
npm start

3. .env 파일 방식 (선택사항) 만약 .env 파일을 사용하고 싶다면, 메인 파일에 다음을 추가하세요:

import dotenv from 'dotenv';
dotenv.config();

8단계: 빌드 및 실행

프로젝트 빌드

TypeScript를 JavaScript로 컴파일합니다:

npm run build

이 명령으로 dist/ 폴더에 컴파일된 JavaScript 파일들이 생성됩니다.

데모 모드로 테스트

WordPress 연결 없이 데모 모드로 테스트:

# 개발 모드
npm run dev

# 또는 빌드된 파일로 직접 실행
node dist/index.js

실제 WordPress 연결 테스트

빌드된 파일로 WordPress에 연결하여 테스트:

node dist/index.js --url=https://your-wordpress-site.com --username=your_username --password=your_application_password

또는 절대 경로로:

node /path/to/your/wordpress-mcp-server/dist/index.js --url=https://your-wordpress-site.com --username=your_username --password=your_application_password

연결 확인

서버가 성공적으로 시작되면 다음과 같은 메시지가 출력됩니다:

WordPress MCP Server started

데모 모드인 경우:

Running in DEMO MODE - no actual WordPress site connected
WordPress MCP Server started (Demo Mode)

Claude Desktop 연동

Claude Desktop 설정 파일에 서버 정보를 추가합니다. 빌드된 JavaScript 파일을 직접 실행하는 방식이 가장 안정적입니다:

{
  "mcpServers": {
    "wordpress": {
      "command": "node",
      "args": [
        "/path/to/your/wordpress-mcp-server/dist/index.js",
        "--url=https://your-wordpress-site.com",
        "--username=your_username",
        "--password=your_application_password"
      ]
    }
  }
}

주의사항:

  • 먼저 npm run build로 TypeScript를 컴파일해야 합니다
  • /path/to/your/wordpress-mcp-server/를 실제 프로젝트 경로로 변경하세요
  • WordPress URL, 사용자명, Application Password를 실제 값으로 변경하세요

실제 설정 예시:

{
  "mcpServers": {
    "wordpress": {
      "command": "node",
      "args": [
        "/path/to/your/wordpress-mcp-server/dist/index.js",
        "--url=https://your-wordpress-site.com",
        "--username=your_username",
        "--password=your_application_password"
      ]
    }
  }
}

대안으로 환경 변수를 사용하는 방법:

{
  "mcpServers": {
    "wordpress": {
      "command": "node",
      "args": ["/path/to/your/wordpress-mcp-server/dist/index.js"],
      "env": {
        "WORDPRESS_URL": "https://your-wordpress-site.com",
        "WORDPRESS_USERNAME": "your_username",
        "WORDPRESS_APPLICATION_PASSWORD": "your_application_password"
      }
    }
  }
}

9단계: 실제 사용 예시

포스트 생성하기

Claude에게 다음과 같이 요청할 수 있습니다:

"새로운 블로그 포스트를 작성해주세요. 
제목: 'MCP 서버로 WordPress 관리하기'
내용: MCP 서버를 사용하여 WordPress를 관리하는 방법에 대한 글
상태: draft"

카테고리 관리하기

"'기술' 카테고리를 생성해주세요. 설명은 '기술 관련 포스트'로 설정해주세요."

포스트 조회하기

"최근 5개의 포스트를 조회해주세요."

고급 기능 확장

커스텀 도구 추가

새로운 도구를 추가하려면:

  1. src/tools/ 폴더에 새 클래스 생성
  2. 메인 서버에 도구 등록
  3. 타입 정의 추가 (필요시)

에러 처리 개선

더 상세한 에러 처리를 위해:

private handleError(error: any, operation: string): never {
  console.error(`${operation} failed:`, error);
  throw new Error(`${operation} failed: ${this.getErrorMessage(error)}`);
}

로깅 추가

운영 환경에서는 로깅을 추가하세요:

import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

마무리

축하합니다! 완전히 작동하는 WordPress MCP 서버를 구축했습니다. 이제 AI 어시스턴트가 여러분의 WordPress 사이트를 자연어로 관리할 수 있습니다.




댓글 남기기