#
tokens: 6922/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── Dockerfile
├── index.ts
├── LICENSE
├── package.json
├── README.ja.md
├── README.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# Package lock files
package-lock.json
yarn.lock

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

# MacOS
.DS_Store

# IDE settings
.idea/
.vscode/

# MCP specific
gcp-oauth.keys.json
.*-server-credentials.json

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# esa MCP Server

<img width="775" alt="スクリーンショット 2025-03-27 午後1 14 09" src="https://github.com/user-attachments/assets/e5f8f308-ed7a-4774-b3a3-9cc284ea7422" />


*Read this in [Japanese](README.ja.md)*

## Overview

This server is an interface that uses the [Model Context Protocol (MCP)](https://github.com/anthropics/anthropic-cookbook/tree/main/model_context_protocol) to enable Claude AI to interact with the [esa API](https://docs.esa.io/posts/102).

With this MCP server, Claude AI can perform operations such as searching, creating, and updating esa documents.

<a href="https://glama.ai/mcp/servers/@kajirita2002/esa-mcp-server">
  <img width="380" height="200" src="https://glama.ai/mcp/servers/@kajirita2002/esa-mcp-server/badge" alt="esa Server MCP server" />
</a>

## About the Repository

This repository provides a standalone implementation of the esa MCP server. It integrates Claude AI with esa to streamline document management.

## Setup

### Prerequisites

- Node.js 18 or higher
- esa API access token
- esa team name

### Installation

```bash
# Install globally
npm install -g @kajirita2002/esa-mcp-server

# Or use directly with npx
npx @kajirita2002/esa-mcp-server
```

### Setting Environment Variables

```bash
# Set environment variables
export ESA_ACCESS_TOKEN="your_esa_access_token"
export ESA_TEAM="your_team_name"
```

### MCP Configuration Example

If you're using this MCP server, add the following configuration to your `mcp_config.json` file:

```json
"esa": {
  "command": "npx",
  "args": ["-y", "@kajirita2002/esa-mcp-server"],
  "env": {
    "ESA_ACCESS_TOKEN": "your_esa_access_token",
    "ESA_TEAM": "your_team_name"
  }
}
```

### Starting the Server

```bash
# Start the server
npm start
```

## Available Tools

This MCP server provides the following tools:

### Post Related

1. `esa_list_posts`
   - Get a list of posts in the team
   - Input:
     - `q` (string, optional): Search query
     - `include` (string, optional): Related data to include in the response (e.g. 'comments,stargazers')
     - `sort` (string, optional): Sort method (updated, created, number, stars, watches, comments, best_match)
     - `order` (string, optional): Sort order (desc, asc)
     - `per_page` (number, optional): Number of results per page (max: 100)
     - `page` (number, optional): Page number to retrieve

2. `esa_get_post`
   - Get detailed information about a specific post
   - Input:
     - `post_number` (number, required): Post number to retrieve
     - `include` (string, optional): Related data to include in the response (e.g. 'comments,stargazers')

3. `esa_create_post`
   - Create a new post
   - Input:
     - `name` (string, required): Post title
     - `body_md` (string, optional): Post body (Markdown format)
     - `tags` (array of string, optional): List of tags for the post
     - `category` (string, optional): Post category
     - `wip` (boolean, optional, default: true): Whether to mark as WIP (Work In Progress)
     - `message` (string, optional): Change message
     - `user` (string, optional): Poster's screen_name (only team owners can specify)
     - `template_post_id` (number, optional): ID of the post to use as a template

4. `esa_update_post`
   - Update an existing post
   - Input:
     - `post_number` (number, required): Post number to update
     - `name` (string, optional): New title for the post
     - `body_md` (string, optional): New body for the post (Markdown format)
     - `tags` (array of string, optional): New list of tags for the post
     - `category` (string, optional): New category for the post
     - `wip` (boolean, optional): Whether to mark as WIP (Work In Progress)
     - `message` (string, optional): Change message
     - `created_by` (string, optional): Poster's screen_name (only team owners can specify)
     - `original_revision` (string, optional): Revision to base the update on

### Comment Related

1. `esa_list_comments`
   - Get a list of comments for a post
   - Input:
     - `post_number` (number, required): Post number to get comments for
     - `page` (number, optional): Page number to retrieve
     - `per_page` (number, optional): Number of results per page (max: 100)

2. `esa_get_comment`
   - Get a specific comment
   - Input:
     - `comment_id` (number, required): ID of the comment to retrieve
     - `include` (string, optional): Related data to include in the response (e.g. 'stargazers')

3. `esa_create_comment`
   - Post a comment to an article
   - Input:
     - `post_number` (number, required): Post number to comment on
     - `body_md` (string, required): Comment body (Markdown format)
     - `user` (string, optional): Poster's screen_name (only team owners can specify)

### Member Related

1. `esa_get_members`
   - Get a list of team members
   - Input:
     - `page` (number, optional): Page number to retrieve
     - `per_page` (number, optional): Number of results per page (max: 100)

2. `esa_get_member`
   - Get information about a specific team member
   - Input:
     - `screen_name_or_email` (string, required): Screen name or email of the member to retrieve

## Usage Example

Here's an example of Claude using this MCP server to create an esa post:

```
[Claude] Please create a new post in esa. The title should be "Project X Progress Report" and the body should include "# This Week's Progress\n\n- Implementation of Feature A completed\n- Testing of Feature B started\n\n## Next Week's Plan\n\n- Start implementation of Feature C".

[MCP Server] Using the esa_create_post tool to create a new post.

[Result]
{
  "number": 123,
  "name": "Project X Progress Report",
  "body_md": "# This Week's Progress\n\n- Implementation of Feature A completed\n- Testing of Feature B started\n\n## Next Week's Plan\n\n- Start implementation of Feature C",
  "wip": false,
  "created_at": "2023-06-01T12:34:56+09:00",
  "updated_at": "2023-06-01T12:34:56+09:00",
  "url": "https://your-team.esa.io/posts/123"
}

[Claude] The post has been created successfully. The post number is 123, and you can access it at the following URL:
https://your-team.esa.io/posts/123
```

## Troubleshooting

### Access Token Issues

```
Error: Request failed with status code 401
```

If you see this error, your esa access token may be invalid or expired. Generate a new access token from the esa settings screen and update your environment variable.

### Permission Issues

```
Error: Request failed with status code 403
```

If you see this error, the current access token doesn't have the necessary permissions. Check the permissions for your access token in the esa settings screen and issue a new token if needed.

## License

Provided under the MIT License.

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": ".",
    "declaration": true,
    "sourceMap": true
  },
  "include": [
    "./**/*.ts"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
FROM node:22.12-alpine AS builder

# Must be entire project because `prepare` script is run during `npm install` and requires all files.
COPY src/esa /app
COPY tsconfig.json /tsconfig.json

WORKDIR /app

RUN --mount=type=cache,target=/root/.npm npm install

RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev

FROM node:22-alpine AS release

COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json

ENV NODE_ENV=production

WORKDIR /app

RUN npm ci --ignore-scripts --omit-dev

ENTRYPOINT ["node", "dist/index.js"]

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@kajirita2002/esa-mcp-server",
  "version": "1.1.0",
  "description": "MCP server for interacting with esa API",
  "license": "MIT",
  "author": "kajirita2002",
  "homepage": "https://github.com/kajirita2002/esa-mcp-server",
  "bugs": "https://github.com/kajirita2002/esa-mcp-server/issues",
  "type": "module",
  "bin": {
    "esa-mcp-server": "dist/index.js"
  },
  "main": "dist/index.js",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch",
    "start": "node dist/index.js"
  },
  "publishConfig": {
    "access": "public"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.0.1"
  },
  "devDependencies": {
    "@types/node": "^22",
    "shx": "^0.3.4",
    "typescript": "^5.6.2"
  }
}
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequest,
  CallToolRequestSchema,
  ListToolsRequestSchema,
  Tool,
} from "@modelcontextprotocol/sdk/types.js";

// Argument type definitions for esa tools
interface ListPostsArgs {
  q?: string;
  include?: string;
  sort?: string;
  order?: string;
  per_page?: number;
  page?: number;
}

interface GetPostArgs {
  post_number: number;
  include?: string;
}

interface CreatePostArgs {
  name: string;
  body_md?: string;
  tags?: string[];
  category?: string;
  wip?: boolean;
  message?: string;
  user?: string;
  template_post_id?: number;
}

interface UpdatePostArgs {
  post_number: number;
  name?: string;
  body_md?: string;
  tags?: string[];
  category?: string;
  wip?: boolean;
  message?: string;
  created_by?: string;
  original_revision?: string;
}

interface ListCommentsArgs {
  post_number: number;
  page?: number;
  per_page?: number;
}

interface GetCommentArgs {
  comment_id: number;
  include?: string;
}

interface CreateCommentArgs {
  post_number: number;
  body_md: string;
  user?: string;
}

interface GetMembersArgs {
  page?: number;
  per_page?: number;
}

interface GetMemberArgs {
  screen_name_or_email: string;
}

// ツール定義
const listPostsTool: Tool = {
  name: "esa_list_posts",
  description: "Get a list of posts in the team (with pagination support)",
  inputSchema: {
    type: "object",
    properties: {
      q: {
        type: "string",
        description: "Search query (see esa API documentation for details)",
      },
      include: {
        type: "string",
        description: "Related data to include in the response (e.g. 'comments,stargazers')",
      },
      sort: {
        type: "string",
        description: "Sort method (updated, created, number, stars, watches, comments, best_match)",
        default: "updated",
      },
      order: {
        type: "string",
        description: "Sort order (desc, asc)",
        default: "desc",
      },
      per_page: {
        type: "number",
        description: "Number of results per page (default: 20, max: 100)",
        default: 20,
      },
      page: {
        type: "number",
        description: "Page number to retrieve",
        default: 1,
      },
    },
  },
};

const getPostTool: Tool = {
  name: "esa_get_post",
  description: "Get detailed information about a specific post",
  inputSchema: {
    type: "object",
    properties: {
      post_number: {
        type: "number",
        description: "Post number to retrieve",
      },
      include: {
        type: "string",
        description: "Related data to include in the response (e.g. 'comments,stargazers')",
      },
    },
    required: ["post_number"],
  },
};

const createPostTool: Tool = {
  name: "esa_create_post",
  description: "Create a new post",
  inputSchema: {
    type: "object",
    properties: {
      name: {
        type: "string",
        description: "Post title",
      },
      body_md: {
        type: "string",
        description: "Post body (Markdown format)",
      },
      tags: {
        type: "array",
        items: { type: "string" },
        description: "List of tags for the post",
      },
      category: {
        type: "string",
        description: "Post category",
      },
      wip: {
        type: "boolean",
        description: "Whether to mark as WIP (Work In Progress)",
        default: true,
      },
      message: {
        type: "string",
        description: "Change message",
      },
      user: {
        type: "string",
        description: "Poster's screen_name (only team owners can specify)",
      },
      template_post_id: {
        type: "number",
        description: "ID of the post to use as a template",
      },
    },
    required: ["name"],
  },
};

const updatePostTool: Tool = {
  name: "esa_update_post",
  description: "Update an existing post",
  inputSchema: {
    type: "object",
    properties: {
      post_number: {
        type: "number",
        description: "Post number to update",
      },
      name: {
        type: "string",
        description: "New title for the post",
      },
      body_md: {
        type: "string",
        description: "New body for the post (Markdown format)",
      },
      tags: {
        type: "array",
        items: { type: "string" },
        description: "New list of tags for the post",
      },
      category: {
        type: "string",
        description: "New category for the post",
      },
      wip: {
        type: "boolean",
        description: "Whether to mark as WIP (Work In Progress)",
      },
      message: {
        type: "string",
        description: "Change message",
      },
      created_by: {
        type: "string",
        description: "Poster's screen_name (only team owners can specify)",
      },
      original_revision: {
        type: "string",
        description: "Revision to base the update on",
      },
    },
    required: ["post_number"],
  },
};

const listCommentsTool: Tool = {
  name: "esa_list_comments",
  description: "Get a list of comments for a post",
  inputSchema: {
    type: "object",
    properties: {
      post_number: {
        type: "number",
        description: "Post number to get comments for",
      },
      page: {
        type: "number",
        description: "Page number to retrieve",
        default: 1,
      },
      per_page: {
        type: "number",
        description: "Number of results per page (default: 20, max: 100)",
        default: 20,
      },
    },
    required: ["post_number"],
  },
};

const getCommentTool: Tool = {
  name: "esa_get_comment",
  description: "Get a specific comment",
  inputSchema: {
    type: "object",
    properties: {
      comment_id: {
        type: "number",
        description: "ID of the comment to retrieve",
      },
      include: {
        type: "string",
        description: "レスポンスに含める関連データ (例: 'stargazers')",
      },
    },
    required: ["comment_id"],
  },
};

const createCommentTool: Tool = {
  name: "esa_create_comment",
  description: "Post a comment to an article",
  inputSchema: {
    type: "object",
    properties: {
      post_number: {
        type: "number",
        description: "Post number to comment on",
      },
      body_md: {
        type: "string",
        description: "Comment body (Markdown format)",
      },
      user: {
        type: "string",
        description: "Poster's screen_name (only team owners can specify)",
      },
    },
    required: ["post_number", "body_md"],
  },
};

const getMembersTool: Tool = {
  name: "esa_get_members",
  description: "Get a list of team members",
  inputSchema: {
    type: "object",
    properties: {
      page: {
        type: "number",
        description: "Page number to retrieve",
        default: 1,
      },
      per_page: {
        type: "number",
        description: "Number of results per page (default: 20, max: 100)",
        default: 20,
      },
    },
  },
};

const getMemberTool: Tool = {
  name: "esa_get_member",
  description: "Get information about a specific team member",
  inputSchema: {
    type: "object",
    properties: {
      screen_name_or_email: {
        type: "string",
        description: "Screen name or email of the member to retrieve",
      },
    },
    required: ["screen_name_or_email"],
  },
};

class EsaClient {
  private baseUrl: string;
  private headers: { Authorization: string; "Content-Type": string };

  constructor(accessToken: string, teamName: string) {
    this.baseUrl = `https://api.esa.io/v1/teams/${teamName}`;
    this.headers = {
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    };
  }

  async listPosts(args: ListPostsArgs = {}): Promise<any> {
    const params = new URLSearchParams();
    
    if (args.q) params.append("q", args.q);
    if (args.include) params.append("include", args.include);
    if (args.sort) params.append("sort", args.sort);
    if (args.order) params.append("order", args.order);
    if (args.per_page) params.append("per_page", args.per_page.toString());
    if (args.page) params.append("page", args.page.toString());

    const url = `${this.baseUrl}/posts${params.toString() ? `?${params}` : ""}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }

  async getPost(post_number: number, include?: string): Promise<any> {
    const params = new URLSearchParams();
    
    if (include) params.append("include", include);

    const url = `${this.baseUrl}/posts/${post_number}${params.toString() ? `?${params}` : ""}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }

  async createPost(postData: Omit<CreatePostArgs, 'template_post_id'> & { template_post_id?: number }): Promise<any> {
    const url = `${this.baseUrl}/posts`;
    const response = await fetch(url, {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify({ post: postData }),
    });

    return response.json();
  }

  async updatePost(post_number: number, postData: Omit<UpdatePostArgs, 'post_number'>): Promise<any> {
    const url = `${this.baseUrl}/posts/${post_number}`;
    const response = await fetch(url, {
      method: "PATCH",
      headers: this.headers,
      body: JSON.stringify({ post: postData }),
    });

    return response.json();
  }

  async listComments(post_number: number, page?: number, per_page?: number): Promise<any> {
    const params = new URLSearchParams();
    
    if (page) params.append("page", page.toString());
    if (per_page) params.append("per_page", per_page.toString());

    const url = `${this.baseUrl}/posts/${post_number}/comments${params.toString() ? `?${params}` : ""}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }

  async getComment(comment_id: number, include?: string): Promise<any> {
    const params = new URLSearchParams();
    
    if (include) params.append("include", include);

    const url = `${this.baseUrl}/comments/${comment_id}${params.toString() ? `?${params}` : ""}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }

  async createComment(post_number: number, body_md: string, user?: string): Promise<any> {
    const url = `${this.baseUrl}/posts/${post_number}/comments`;
    const commentData: { body_md: string; user?: string } = { body_md };
    
    if (user) commentData.user = user;

    const response = await fetch(url, {
      method: "POST",
      headers: this.headers,
      body: JSON.stringify({ comment: commentData }),
    });

    return response.json();
  }

  async getMembers(page?: number, per_page?: number): Promise<any> {
    const params = new URLSearchParams();
    
    if (page) params.append("page", page.toString());
    if (per_page) params.append("per_page", per_page.toString());

    const url = `${this.baseUrl}/members${params.toString() ? `?${params}` : ""}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }

  async getMember(screen_name_or_email: string): Promise<any> {
    const url = `${this.baseUrl}/members/${screen_name_or_email}`;
    const response = await fetch(url, { headers: this.headers });

    return response.json();
  }
}

async function main() {
  const accessToken = process.env.ESA_ACCESS_TOKEN;
  const teamName = process.env.ESA_TEAM;

  if (!accessToken || !teamName) {
    console.error(
      "Please set the ESA_ACCESS_TOKEN and ESA_TEAM environment variables",
    );
    process.exit(1);
  }

  console.error("Starting esa MCP Server...");
  const server = new Server(
    {
      name: "esa MCP Server",
      version: "1.0.0",
    },
    {
      capabilities: {
        tools: {},
      },
    },
  );

  const esaClient = new EsaClient(accessToken, teamName);

  server.setRequestHandler(
    CallToolRequestSchema,
    async (request: CallToolRequest) => {
      console.error("CallToolRequest received:", request);
      try {
        if (!request.params.arguments) {
          throw new Error("No arguments provided");
        }

        switch (request.params.name) {
          case "esa_list_posts": {
            const args = request.params.arguments as unknown as ListPostsArgs;
            const response = await esaClient.listPosts(args);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_get_post": {
            const args = request.params.arguments as unknown as GetPostArgs;
            if (!args.post_number) {
              throw new Error("post_number is required");
            }
            const response = await esaClient.getPost(args.post_number, args.include);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_create_post": {
            const args = request.params.arguments as unknown as CreatePostArgs;
            if (!args.name) {
              throw new Error("name is required");
            }
            const response = await esaClient.createPost(args);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_update_post": {
            const args = request.params.arguments as unknown as UpdatePostArgs;
            if (!args.post_number) {
              throw new Error("post_number is required");
            }
            const { post_number, ...postData } = args;
            const response = await esaClient.updatePost(post_number, postData);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_list_comments": {
            const args = request.params.arguments as unknown as ListCommentsArgs;
            if (!args.post_number) {
              throw new Error("post_number is required");
            }
            const response = await esaClient.listComments(
              args.post_number,
              args.page,
              args.per_page
            );
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_get_comment": {
            const args = request.params.arguments as unknown as GetCommentArgs;
            if (!args.comment_id) {
              throw new Error("comment_id is required");
            }
            const response = await esaClient.getComment(args.comment_id, args.include);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_create_comment": {
            const args = request.params.arguments as unknown as CreateCommentArgs;
            if (!args.post_number || !args.body_md) {
              throw new Error("post_number and body_md are required");
            }
            const response = await esaClient.createComment(
              args.post_number,
              args.body_md,
              args.user
            );
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_get_members": {
            const args = request.params.arguments as unknown as GetMembersArgs;
            const response = await esaClient.getMembers(args.page, args.per_page);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          case "esa_get_member": {
            const args = request.params.arguments as unknown as GetMemberArgs;
            if (!args.screen_name_or_email) {
              throw new Error("screen_name_or_email is required");
            }
            const response = await esaClient.getMember(args.screen_name_or_email);
            return {
              content: [{ type: "text", text: JSON.stringify(response) }],
            };
          }

          default:
            throw new Error(`Unknown tool: ${request.params.name}`);
        }
      } catch (error) {
        console.error("Tool execution error:", error);
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({
                error: error instanceof Error ? error.message : String(error),
              }),
            },
          ],
        };
      }
    },
  );

  server.setRequestHandler(ListToolsRequestSchema, async () => {
    console.error("ListToolsRequest received");
    return {
      tools: [
        listPostsTool,
        getPostTool,
        createPostTool,
        updatePostTool,
        listCommentsTool,
        getCommentTool,
        createCommentTool,
        getMembersTool,
        getMemberTool,
      ],
    };
  });

  const transport = new StdioServerTransport();
  console.error("Connecting server to transport...");
  await server.connect(transport);

  console.error("esa MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

```