@esaio/esa-mcp-server 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,6 +11,11 @@ updates:
11
11
  time: "03:00"
12
12
  timezone: Asia/Tokyo
13
13
  open-pull-requests-limit: 10
14
+ groups:
15
+ vitest:
16
+ patterns:
17
+ - "vitest"
18
+ - "@vitest/*"
14
19
  ignore:
15
20
  - dependency-name: "zod"
16
21
  update-types: ["version-update:semver-major"]
@@ -33,7 +33,7 @@ jobs:
33
33
  uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # https://github.com/actions/checkout/tree/v5
34
34
 
35
35
  - name: Log in to Container Registry
36
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # https://github.com/docker/login-action/tree/v3
36
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # https://github.com/docker/login-action/tree/v3
37
37
  with:
38
38
  registry: ${{ env.REGISTRY }}
39
39
  username: ${{ github.actor }}
@@ -66,7 +66,7 @@ jobs:
66
66
  touch "/tmp/digests/${digest#sha256:}"
67
67
 
68
68
  - name: Upload digest
69
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # https://github.com/actions/upload-artifact/tree/v4
69
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # https://github.com/actions/upload-artifact/tree/v4
70
70
  with:
71
71
  name: digests-${{ env.PLATFORM_PAIR }}
72
72
  path: /tmp/digests/*
@@ -83,14 +83,14 @@ jobs:
83
83
 
84
84
  steps:
85
85
  - name: Download digests
86
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # https://github.com/actions/download-artifact/tree/v5
86
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # https://github.com/actions/download-artifact/tree/v5
87
87
  with:
88
88
  path: /tmp/digests
89
89
  pattern: digests-*
90
90
  merge-multiple: true
91
91
 
92
92
  - name: Log in to Container Registry
93
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # https://github.com/docker/login-action/tree/v3
93
+ uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # https://github.com/docker/login-action/tree/v3
94
94
  with:
95
95
  registry: ${{ env.REGISTRY }}
96
96
  username: ${{ github.actor }}
@@ -2,7 +2,7 @@ name: CI
2
2
  on:
3
3
  push:
4
4
  branches:
5
- - master
5
+ - main
6
6
  pull_request:
7
7
  types: [opened, synchronize, reopened]
8
8
 
@@ -14,10 +14,10 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  steps:
16
16
  - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # https://github.com/actions/checkout/tree/v5
17
- - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # https://github.com/actions/setup-node/tree/v5
17
+ - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # https://github.com/actions/setup-node/tree/v5
18
18
  with:
19
19
  node-version: ${{ env.NODE_VERSION }}
20
- cache: 'npm'
20
+ cache: "npm"
21
21
  - name: Install dependencies
22
22
  run: npm ci
23
23
  - name: Run linting
@@ -33,7 +33,7 @@ jobs:
33
33
  - name: Run tests
34
34
  run: npm run test:coverage
35
35
  - name: Upload coverage reports
36
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # https://github.com/actions/upload-artifact/tree/v4
36
+ uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # https://github.com/actions/upload-artifact/tree/v4
37
37
  with:
38
38
  name: coverage
39
39
  path: coverage/
package/README.en.md CHANGED
@@ -13,6 +13,7 @@ This MCP server provides seamless integration between AI assistants and [esa.io]
13
13
  ## Available Tools
14
14
 
15
15
  ### Team Management
16
+
16
17
  - `esa_get_teams` - Get user's accessible esa teams
17
18
  - `esa_get_team_stats` - Get team statistics (members, posts, comments, stars, watches, active users)
18
19
  - `esa_get_team_tags` - Get all tags used in team posts with count
@@ -26,11 +27,13 @@ This MCP server provides seamless integration between AI assistants and [esa.io]
26
27
  - `esa_update_post` - Update existing post (title, content, tags, category, WIP status)
27
28
 
28
29
  ### Post Actions
30
+
29
31
  - `esa_archive_post` - Archive a post by moving to Archived/ category
30
32
  - `esa_ship_post` - Ship a post (mark as complete by setting wip to false)
31
33
  - `esa_duplicate_post` - Prepare a post for duplication (retrieve name and body_md)
32
34
 
33
35
  ### Comment Management
36
+
34
37
  - `esa_get_comment` - Get a specific comment by ID
35
38
  - `esa_create_comment` - Create a new comment on a post
36
39
  - `esa_update_comment` - Update an existing comment
@@ -39,10 +42,21 @@ This MCP server provides seamless integration between AI assistants and [esa.io]
39
42
  - `esa_get_team_comments` - Get team comments with pagination
40
43
 
41
44
  ### Category Management
45
+
42
46
  - `esa_get_categories` - Get categories and subcategories for a specific path
43
47
  - `esa_get_top_categories` - Get all top-level categories for a team
48
+ - `esa_get_all_category_paths` - Get all category paths in a team (with post counts, supports filtering)
49
+ - Perfect for understanding category structure, organization planning, and cleanup
50
+ - Filter examples: `prefix: "dev"` for development-related, `match: "api"` for API-related categories
51
+
52
+ ### Attachments
53
+
54
+ - `esa_get_attachment` - Retrieve an attachment file from esa posts and comments
55
+ - Returns base64-encoded data for supported images (JPEG, PNG, GIF, WebP) under 30MB
56
+ - Returns signed URLs (valid for 5 minutes) for other file types, larger images, or when forceSignedUrl is specified
44
57
 
45
58
  ### Help & Documentation
59
+
46
60
  - `esa_get_search_options_help` - Get esa search syntax documentation
47
61
  - `esa_get_markdown_syntax_help` - Get esa Markdown syntax documentation
48
62
  - `esa_search_help` - Search esa documentation for features and terminology
@@ -66,8 +80,8 @@ Add to your MCP client configuration file:
66
80
  ### Required Environment Variables
67
81
 
68
82
  - ESA_ACCESS_TOKEN: Access Token
69
- - Required scopes: `read write` or `admin:comment read:post write:post read:category read:tag read:team read:member`
70
- - [PAT v2](https://docs.esa.io/posts/559) is recommended.
83
+ - Required scopes: `read write` or `admin:comment read:post write:post read:category read:tag read:attachment read:team read:member`
84
+ - [PAT v2](https://docs.esa.io/posts/559) is recommended.
71
85
  - LANG: Language for UI
72
86
 
73
87
  ### Claude Desktop Example
@@ -107,9 +121,7 @@ Add to `claude_desktop_config.json`:
107
121
  "mcpServers": {
108
122
  "esa": {
109
123
  "command": "/Users/your-username/.nodenv/shims/npx",
110
- "args": [
111
- "@esaio/esa-mcp-server"
112
- ],
124
+ "args": ["@esaio/esa-mcp-server"],
113
125
  "env": {
114
126
  "ESA_ACCESS_TOKEN": "your_personal_access_token",
115
127
  "LANG": "en"
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **日本語** | [English](README.en.md)
6
6
 
7
- esa.io の公式 MCP(Model Context Protocol)サーバー(STDIO Transport版)
7
+ esa.io の公式 MCP(Model Context Protocol)サーバー(STDIO Transport 版)
8
8
 
9
9
  ## 概要
10
10
 
@@ -13,6 +13,7 @@ AI アシスタントと情報共有サービス [esa](https://esa.io) をつな
13
13
  ## 使えるツール
14
14
 
15
15
  ### チーム管理
16
+
16
17
  - `esa_get_teams` - 所属している esa チームの一覧
17
18
  - `esa_get_team_stats` - チームの統計情報(メンバー数、記事数、コメント数など)
18
19
  - `esa_get_team_tags` - チーム内で使われているタグと使用回数
@@ -21,16 +22,18 @@ AI アシスタントと情報共有サービス [esa](https://esa.io) をつな
21
22
  ### 記事管理
22
23
 
23
24
  - `esa_search_posts` - 記事を検索
24
- - `esa_get_post` - 記事IDから記事を取得
25
+ - `esa_get_post` - 記事 ID から記事を取得
25
26
  - `esa_create_post` - 新しい記事を作成(タグ、カテゴリー、WIP ステータス付き)
26
27
  - `esa_update_post` - 記事を更新(タイトル、本文、タグ、カテゴリー、WIP ステータス)
27
28
 
28
29
  ### 記事の操作
30
+
29
31
  - `esa_archive_post` - 記事をアーカイブ(Archived/ カテゴリーへ移動)
30
32
  - `esa_ship_post` - 記事を Ship It!(WIP を外して公開)
31
33
  - `esa_duplicate_post` - 記事を複製するための準備(タイトルと本文を取得)
32
34
 
33
35
  ### コメント管理
36
+
34
37
  - `esa_get_comment` - コメント ID からコメントを取得
35
38
  - `esa_create_comment` - 記事にコメントを追加
36
39
  - `esa_update_comment` - コメントを編集
@@ -39,10 +42,21 @@ AI アシスタントと情報共有サービス [esa](https://esa.io) をつな
39
42
  - `esa_get_team_comments` - チーム全体のコメント一覧(ページング対応)
40
43
 
41
44
  ### カテゴリー管理
45
+
42
46
  - `esa_get_categories` - 指定パス配下のカテゴリー一覧
43
47
  - `esa_get_top_categories` - トップレベルのカテゴリー一覧
48
+ - `esa_get_all_category_paths` - チーム内の全カテゴリーパス一覧(記事数付き、フィルタリング対応)
49
+ - カテゴリ構造の把握、整理、統合の計画に最適
50
+ - フィルター例: `prefix: "dev"` で開発関連、`match: "api"` で API 関連を検索
51
+
52
+ ### 添付ファイル
53
+
54
+ - `esa_get_attachment` - 記事やコメントの添付ファイルを取得
55
+ - サポート形式(JPEG, PNG, GIF, WebP)で 30MB 以下の画像は base64 エンコードで返却
56
+ - その他のファイル、大きな画像、または forceSignedUrl 指定時は署名付き URL(有効期限 5 分)を返却
44
57
 
45
58
  ### ヘルプとドキュメント
59
+
46
60
  - `esa_get_search_options_help` - esa の検索構文ヘルプ
47
61
  - `esa_get_markdown_syntax_help` - esa の Markdown 記法ヘルプ
48
62
  - `esa_search_help` - esa のドキュメントから機能や用語を検索
@@ -56,7 +70,7 @@ AI アシスタントと情報共有サービス [esa](https://esa.io) をつな
56
70
  ## プロンプト
57
71
 
58
72
  - `esa_summarize_post` - esa の記事を要約
59
- - 入力: チーム名と記事ID
73
+ - 入力: チーム名と記事 ID
60
74
  - 出力: 記事の構造化された要約
61
75
 
62
76
  ## MCP クライアントの設定
@@ -66,8 +80,8 @@ MCP クライアントの設定ファイルに以下を追加します:
66
80
  ### 用意する環境変数
67
81
 
68
82
  - ESA_ACCESS_TOKEN: アクセストークン
69
- - 必要なスコープ: `read write` または `admin:comment read:post write:post read:category read:tag read:team read:member`
70
- - [PAT v2](https://docs.esa.io/posts/559)を推奨します。
83
+ - 必要なスコープ: `read write` または `admin:comment read:post write:post read:category read:tag read:attachment read:team read:member`
84
+ - [PAT v2](https://docs.esa.io/posts/559)を推奨します。
71
85
  - LANG: UI の言語設定
72
86
 
73
87
  ### Claude Desktop の例
@@ -107,9 +121,7 @@ MCP クライアントの設定ファイルに以下を追加します:
107
121
  "mcpServers": {
108
122
  "esa": {
109
123
  "command": "/Users/your-username/.nodenv/shims/npx",
110
- "args": [
111
- "@esaio/esa-mcp-server"
112
- ],
124
+ "args": ["@esaio/esa-mcp-server"],
113
125
  "env": {
114
126
  "ESA_ACCESS_TOKEN": "your_personal_access_token",
115
127
  "LANG": "ja"
package/bin/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import{McpServer as e,ResourceTemplate as t}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as n}from"@modelcontextprotocol/sdk/server/stdio.js";import r from"i18next";import i from"openapi-fetch";import{z as a}from"zod";var o={name:`@esaio/esa-mcp-server`,version:`0.1.0`,description:`Official MCP server for esa.io - STDIO transport version`,main:`bin/index.js`,type:`module`,bin:{"esa-mcp-server":`./bin/index.js`},scripts:{build:`tsdown`,test:`vitest`,"test:run":`vitest run`,"test:coverage":`vitest run --coverage`,lint:`biome check . --error-on-warnings`,"lint:fix":`biome check --write .`,"type-check":`tsc --noEmit`},keywords:[`mcp`,`model-context-protocol`,`esa`,`esa.io`,`documentation`,`knowledge-base`],author:`esa LLC`,license:`MIT`,dependencies:{"@modelcontextprotocol/sdk":`^1.17.5`,axios:`^1.11.0`,i18next:`^25.5.2`,"openapi-fetch":`^0.14.0`,zod:`^3.23.8`},devDependencies:{"@biomejs/biome":`^2.2.4`,"@types/node":`^20.19.13`,"@vitest/coverage-v8":`^3.2.4`,"openapi-typescript":`^7.9.1`,tsdown:`^0.15.0`,tsx:`^4.20.5`,typescript:`^5.9.2`,vitest:`^3.2.4`},engines:{node:`>=20.19.4`}};const s={esa:{apiAccessToken:process.env.ESA_ACCESS_TOKEN||``,apiBaseUrl:process.env.ESA_API_BASE_URL||`https://api.esa.io`},server:{name:`esa-mcp-server`,version:o.version,description:`Official MCP server for esa.io`}};function c(){if(!s.esa.apiAccessToken)throw Error(`ESA_ACCESS_TOKEN environment variable is required`)}var ee={prompts:{summarize_post:{title:`Summarize esa post`,description:`Summarize an esa post in various formats (bullet points, paragraph, or keywords)`,args:{team_name:`The name of the esa team`,post_number:`The post number to summarize`,format:`Summary format (bullet/paragraph/keywords)`}}}},te={prompts:{summarize_post:{title:`esaの記事の要約`,description:`esa記事を指定した形式で要約します(bullet: 箇条書き, paragraph: 文章, keywords: キーワード)`,args:{team_name:`esaチーム名`,post_number:`要約する記事番号`,format:`要約形式 (bullet/paragraph/keywords)`}}}};async function ne(){let e=process.env.LC_ALL?.split(/[-_.]/)[0]||process.env.LC_MESSAGES?.split(/[-_.]/)[0]||process.env.LANG?.split(/[-_.]/)[0]||process.env.LANGUAGE?.split(/[-_.]/)[0]||`en`;return await r.init({lng:e,fallbackLng:`en`,resources:{ja:{translation:te},en:{translation:ee}},interpolation:{escapeValue:!1}}),r}function l(e,t){return r.t(e,t)}function re(e){return{async onRequest({request:t}){return t.headers.set(`Authorization`,`Bearer ${e}`),t},async onResponse({response:e}){let t=e.headers.get(`x-ratelimit-limit`),n=e.headers.get(`x-ratelimit-remaining`);return t&&n&&console.error(`Rate limit: ${n}/${t}`),e},async onError({error:e}){console.error(`Network Error:`,e)}}}const ie=o.version;function u(e){return{async onRequest({request:t}){return t.headers.set(`User-Agent`,`esa-mcp-server/${e} (official)`),t}}}function d(e,t=`https://api.esa.io`){let n=i({baseUrl:t});return n.use(u(ie)),n.use(re(e)),n}async function f(e,t,...n){let r;if(`apiAccessToken`in e&&`apiBaseUrl`in e)r=d(e.apiAccessToken,e.apiBaseUrl);else throw Error(`Unsupported context type. Only StdioContext is currently supported.`);return t(r,...n)}var p=class extends Error{constructor(){super(`Missing required parameter 'teamName'. Use esa_get_teams to list available teams, then retry with teamName specified.`),this.name=`MissingTeamNameError`}};function m(e){return e instanceof Error?`Error: ${e.message}`:typeof e==`number`&&e!==null?`Error: API Response(status: ${e})`:typeof e==`object`&&e?`Error: ${JSON.stringify(e,null,2)}`:`Error: ${String(e)}`}function h(e){return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}function g(e,t){return{contents:[{uri:t,mimeType:`application/json`,text:JSON.stringify(e,null,2)}]}}function _(e){return{messages:[{role:`user`,content:{type:`text`,text:e}}]}}function v(e){return{content:[{type:`text`,text:m(e)}]}}function y(e,t){return{contents:[{uri:t,mimeType:`application/json`,text:m(e)}]}}function b(e){return{messages:[{role:`user`,content:{type:`text`,text:m(e)}}]}}const x=()=>a.object({teamName:a.string().describe(l(`prompts.summarize_post.args.team_name`)),postNumber:a.string().describe(l(`prompts.summarize_post.args.post_number`)),format:a.enum([`bullet`,`paragraph`,`keywords`]).optional().describe(l(`prompts.summarize_post.args.format`))});async function ae(e,t){let{teamName:n,postNumber:r,format:i=`bullet`}=t;if(!n)throw new p;let a=Number.parseInt(r,10);if(Number.isNaN(a)||a<=0)return b(`Post number must be a positive integer`);try{let{data:t,error:r,response:o}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:n,post_number:a}}});if(r||!o.ok)return b(r||o.status);let s=t,c=`Please summarize the following post:
2
+ import{McpServer as e,ResourceTemplate as t}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as n}from"@modelcontextprotocol/sdk/server/stdio.js";import r from"i18next";import i from"openapi-fetch";import{z as a}from"zod";var o=`0.1.0`;const s={esa:{apiAccessToken:process.env.ESA_ACCESS_TOKEN||``,apiBaseUrl:process.env.ESA_API_BASE_URL||`https://api.esa.io`},server:{name:`esa-mcp-server`,version:o,description:`Official MCP server for esa.io`}};function c(){if(!s.esa.apiAccessToken)throw Error(`ESA_ACCESS_TOKEN environment variable is required`)}var l={prompts:{summarize_post:{title:`Summarize esa post`,description:`Summarize an esa post in various formats (bullet points, paragraph, or keywords)`,args:{team_name:`The name of the esa team`,post_number:`The post number to summarize`,format:`Summary format (bullet/paragraph/keywords)`}}}},ee={prompts:{summarize_post:{title:`esaの記事の要約`,description:`esa記事を指定した形式で要約します(bullet: 箇条書き, paragraph: 文章, keywords: キーワード)`,args:{team_name:`esaチーム名`,post_number:`要約する記事番号`,format:`要約形式 (bullet/paragraph/keywords)`}}}};async function te(){let e=process.env.LC_ALL?.split(/[-_.]/)[0]||process.env.LC_MESSAGES?.split(/[-_.]/)[0]||process.env.LANG?.split(/[-_.]/)[0]||process.env.LANGUAGE?.split(/[-_.]/)[0]||`en`;return await r.init({lng:e,fallbackLng:`en`,resources:{ja:{translation:ee},en:{translation:l}},interpolation:{escapeValue:!1}}),r}function u(e,t){return r.t(e,t)}function d(e){return{async onRequest({request:t}){return t.headers.set(`Authorization`,`Bearer ${e}`),t},async onResponse({response:e}){let t=e.headers.get(`x-ratelimit-limit`),n=e.headers.get(`x-ratelimit-remaining`);return t&&n&&console.error(`Rate limit: ${n}/${t}`),e},async onError({error:e}){console.error(`Network Error:`,e)}}}const ne=o;function re(e){return{async onRequest({request:t}){return t.headers.set(`User-Agent`,`esa-mcp-server/${e} (official)`),t}}}function ie(e,t=`https://api.esa.io`){let n=i({baseUrl:t});return n.use(re(ne)),n.use(d(e)),n}async function f(e,t,...n){let r;if(`apiAccessToken`in e&&`apiBaseUrl`in e)r=ie(e.apiAccessToken,e.apiBaseUrl);else throw Error(`Unsupported context type. Only StdioContext is currently supported.`);return t(r,...n)}var p=class extends Error{constructor(){super(`Missing required parameter 'teamName'. Use esa_get_teams to list available teams, then retry with teamName specified.`),this.name=`MissingTeamNameError`}};function m(e){return e instanceof Error?`Error: ${e.message}`:typeof e==`number`&&e!==null?`Error: API Response(status: ${e})`:typeof e==`object`&&e?`Error: ${JSON.stringify(e,null,2)}`:`Error: ${String(e)}`}function h(e){return{content:[{type:`text`,text:JSON.stringify(e,null,2)}]}}function g(e,t){return{contents:[{uri:t,mimeType:`application/json`,text:JSON.stringify(e,null,2)}]}}function _(e){return{messages:[{role:`user`,content:{type:`text`,text:e}}]}}function v(e){return{content:[{type:`text`,text:m(e)}]}}function y(e,t){return{contents:[{uri:t,mimeType:`application/json`,text:m(e)}]}}function b(e){return{messages:[{role:`user`,content:{type:`text`,text:m(e)}}]}}const ae=()=>a.object({teamName:a.string().describe(u(`prompts.summarize_post.args.team_name`)),postNumber:a.string().describe(u(`prompts.summarize_post.args.post_number`)),format:a.enum([`bullet`,`paragraph`,`keywords`]).optional().describe(u(`prompts.summarize_post.args.format`))});async function oe(e,t){let{teamName:n,postNumber:r,format:i=`bullet`}=t;if(!n)throw new p;let a=Number.parseInt(r,10);if(Number.isNaN(a)||a<=0)return b(`Post number must be a positive integer`);try{let{data:t,error:r,response:o}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:n,post_number:a}}});if(r||!o.ok)return b(r||o.status);let s=t,c=`Please summarize the following post:
3
3
 
4
4
  `;switch(c+=`Title: ${s.name}\n`,c+=`URL: ${s.url}\n`,c+=`Author: ${s.created_by.name}\n`,c+=`Created: ${s.created_at}\n`,c+=`Updated: ${s.updated_at}\n`,s.category&&(c+=`Category: ${s.category}\n`),s.tags&&s.tags.length>0&&(c+=`Tags: ${s.tags.join(`, `)}\n`),c+=`
5
5
  ---
6
6
 
7
- `,s.body_md&&(c+=`Content:\n${s.body_md}\n\n---\n\n`),i){case`bullet`:c+=`Please provide a summary in bullet points (3-5 main points).`;break;case`paragraph`:c+=`Please provide a summary in 2-3 paragraphs.`;break;case`keywords`:c+=`Please extract and list 10-15 important keywords from this post.`;break}return _(c)}catch(e){return b(e)}}function oe(e,t){console.error(`Setting up MCP prompts...`),e.registerPrompt(`esa_summarize_post`,{title:l(`prompts.summarize_post.title`),description:l(`prompts.summarize_post.description`),argsSchema:x().shape},async e=>f(t,ae,e))}function S(e,t={}){let{truncateBody:n}=t,r=e.body_md;return n&&r&&r.length>n&&(r=`${r.slice(0,n)}...`),{url:e.url,wip:e.wip?`WIP`:`Shipped`,kind:e.kind,category_and_title_and_tags:e.full_name,body_md:r,created_at:e.created_at,updated_at:e.updated_at,created_by:e.created_by,updated_by:e.updated_by,stats:{tasks_count:e.tasks_count,done_tasks_count:e.done_tasks_count,comments_count:e.comments_count,stargazers_count:e.stargazers_count,watchers_count:e.watchers_count}}}const se={sort:`updated`,order:`desc`};async function ce(e,t){let{teamName:n,uri:r}=t;try{let{data:t,error:i,response:a}=await e.GET(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:n},query:se}});if(i||!a.ok)return y(i||a.status,r);let o={...t,posts:t.posts?.map(e=>S(e,{truncateBody:500}))};return g(o,r)}catch(e){return y(e,r)}}function C(e){let t=e.indexOf(`.`);return t>=0?e.substring(0,t):e}const w=a.string().default(``).describe(`Team name (required). Use esa_get_teams first to see available teams.`).transform(C);function T(e){return a.object({teamName:w,...e})}const E=a.object({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`),role:a.enum([`member`,`owner`]).optional().describe(`Filter by role`)});async function D(e,t={}){try{let{data:n,error:r,response:i}=await e.GET(`/v1/teams`,{params:{query:{page:t.page,per_page:t.perPage,role:t.role}}});if(r||!i.ok)return v(r||i.status);let a={...n,teams:n.teams?.map(e=>({url:e.url,name:e.name,description:e.description}))};return h(a)}catch(e){return v(e)}}const O=T({});async function k(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/stats`,{params:{path:{team_name:t.teamName}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}const A=T({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function j(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/tags`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}const M=T({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`),sort:a.enum([`posts_count`,`joined`,`last_accessed`]).optional().describe(`Sort criteria`),order:a.enum([`desc`,`asc`]).optional().describe(`Sort order`)});async function N(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/members`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage,sort:t.sort,order:t.order}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}async function P(e){try{let t=await f(e,D,{});return JSON.parse(t.content[0].text).teams?.map(e=>({uri:`esa://teams/${e.name}/posts/recent`,name:`Recent posts from ${e.name}`,description:`Recent posts from ${e.name}${e.description?` (${e.description})`:``}`,mimeType:`application/json`}))||[]}catch(e){return console.error(`Failed to list teams:`,e),[]}}function F(e,n){console.error(`Setting up MCP resources...`),e.registerResource(`esa_recent_posts`,new t(`esa://teams/{teamName}/posts/recent`,{list:async()=>({resources:await P(n)})}),{title:`Recent Posts`,description:`Fetch recent updated posts from esa team`,mimeType:`application/json`},async(e,t)=>f(n,ce,{...t,uri:e.href}))}function I(e){return{full_name:e.full_name,count:e.count,has_child:e.has_child||!1}}function L(e){return{current_category:e.current_category,categories:e.categories?.map(I)||[],parent_categories:e.parent_categories?.map(e=>({current_category:e.current_category,categories:e.categories?.map(I)||[]})),readme:e.readme,no_category:e.no_category?I(e.no_category):void 0,descendant_posts:e.descendant_posts,posts:e.posts,total_count:e.total_count,per_page:e.per_page,page:e.page,prev_page:e.prev_page,next_page:e.next_page,max_per_page:e.max_per_page}}const R=T({select:a.string().describe(`Category path to retrieve`),include:a.enum([`posts`,`parent_categories`]).optional().describe(`Additional information to include`),descendantPosts:a.boolean().optional().describe(`Include descendant posts (only effective with include=posts)`),page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function z(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/categories`,{params:{path:{team_name:t.teamName},query:{select:t.select,include:t.include,descendant_posts:t.descendantPosts,page:t.page,per_page:t.perPage}}});if(r||!i.ok)return v(r||i.status);let a=L(n);return h(a)}catch(e){return v(e)}}const B=T({});async function V(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/categories/top`,{params:{path:{team_name:t.teamName}}});if(r||!i.ok)return v(r||i.status);let a=L(n);return h(a)}catch(e){return v(e)}}function H(e,t={}){let{truncateBody:n}=t,r=e.body_md;return n&&r&&r.length>n&&(r=`${r.slice(0,n)}...`),{id:e.id,post_number:e.post_number,url:e.url,body_md:r,created_at:e.created_at,updated_at:e.updated_at,created_by:e.created_by,stats:{stargazers_count:e.stargazers_count,star:e.star},stargazers:e.stargazers}}const U=T({commentId:a.number().describe(`The comment ID to retrieve`),include:a.enum([`stargazers`]).optional().describe(`Specify 'stargazers' to include stargazers in the response`)});async function W(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId},query:{include:t.include}}});if(r||!i.ok)return v(r||i.status);let a=H(n);return h(a)}catch(e){return v(e)}}const G=T({postNumber:a.number().describe(`The post number to comment on`),bodyMd:a.string().describe(`The comment content in Markdown format`),user:a.string().optional().describe(`Comment author's screen_name (owner permission required)`)});async function K(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.POST(`/v1/teams/{team_name}/posts/{post_number}/comments`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}},body:{comment:{body_md:t.bodyMd,user:t.user}}});if(r||!i.ok)return v(r||i.status);let a=H(n,{truncateBody:300});return h(a)}catch(e){return v(e)}}const le=T({commentId:a.number().describe(`The comment ID to update`),bodyMd:a.string().describe(`The updated comment content in Markdown format`),user:a.string().optional().describe(`Comment author's screen_name (owner permission required)`)});async function ue(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.PATCH(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId}},body:{comment:{body_md:t.bodyMd,user:t.user}}});if(r||!i.ok)return v(r||i.status);let a=H(n);return h(a)}catch(e){return v(e)}}const de=T({commentId:a.number().describe(`The comment ID to delete`)});async function fe(e,t){try{if(!t.teamName)throw new p;let{error:n,response:r}=await e.DELETE(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId}}});return n||!r.ok?v(n||r.status):h({success:!0,message:`Comment deleted successfully`})}catch(e){return v(e)}}const pe=T({postNumber:a.number().describe(`The post number to get comments for`),page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function me(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}/comments`,{params:{path:{team_name:t.teamName,post_number:t.postNumber},query:{page:t.page,per_page:t.perPage}}});if(r||!i.ok)return v(r||i.status);let a=n.comments.map(e=>H(e,{truncateBody:300}));return h({...n,comments:a})}catch(e){return v(e)}}const he=T({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function ge(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/comments`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage}}});if(r||!i.ok)return v(r||i.status);let a=n.comments.map(e=>H(e,{truncateBody:300}));return h({...n,comments:a})}catch(e){return v(e)}}function q(e,t){if(!e||t!==void 0)return{name:e,category:t};if(e.includes(`/`)){let t=e.split(`/`),n=t.pop(),r=t.join(`/`);return{name:n||void 0,category:r}}return{name:e,category:t}}const _e=T({postNumber:a.number().describe(`The post number to retrieve`),include:a.enum([`comments`]).optional().describe(`Specify 'comments' to include comments in the response`)});async function J(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber},query:{include:t.include}}});if(r||!i.ok)return v(r||i.status);let a=S(n);return h(a)}catch(e){return v(e)}}const ve=T({name:a.string().describe(`The post name (title)`),bodyMd:a.string().optional().describe(`The post content in Markdown format`),tags:a.array(a.string()).optional().describe(`Tags for the post`),category:a.string().optional().describe(`Category path (e.g., 'dev/docs')`),wip:a.boolean().default(!0).describe(`Whether the post is Work In Progress. Set to false to ship it (mark as complete and ready to be published)`),message:a.string().optional().describe(`Update message for the post`)});async function Y(e,t){try{if(!t.teamName)throw new p;let{name:n,category:r}=q(t.name,t.category),{data:i,error:a,response:o}=await e.POST(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:t.teamName}},body:{post:{name:n,body_md:t.bodyMd,tags:t.tags,category:r,wip:t.wip,message:t.message}}});if(a||!o.ok)return v(a||o.status);let s=S(i);return h(s)}catch(e){return v(e)}}const ye=T({postNumber:a.number().describe(`The post number to update`),name:a.string().optional().describe(`The post name (title)`),bodyMd:a.string().optional().describe(`The post content in Markdown format`),tags:a.array(a.string()).optional().describe(`Tags for the post`),category:a.string().optional().describe(`Category path (e.g., 'dev/docs')`),wip:a.boolean().optional().describe(`Whether the post is Work In Progress. Set to false to ship it (mark as complete and ready to be published)`),message:a.string().optional().describe(`Update message for the post`),originalRevision:a.object({bodyMd:a.string(),number:a.number(),user:a.string()}).optional().describe(`Original revision to check for conflicts`)});async function X(e,t){try{if(!t.teamName)throw new p;let{name:n,category:r}=q(t.name,t.category),{data:i,error:a,response:o}=await e.PATCH(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}},body:{post:{name:n,body_md:t.bodyMd,tags:t.tags,category:r,wip:t.wip,message:t.message,original_revision:t.originalRevision?{body_md:t.originalRevision.bodyMd,number:t.originalRevision.number,user:t.originalRevision.user}:void 0}}});if(a||!o.ok)return v(a||o.status);let s=S(i);return h(s)}catch(e){return v(e)}}function be(e){let t=e;return t===`*`&&(t=``),t=t.replace(/\bafter:(\d{4}-\d{2}-\d{2})\b/gi,`created:>$1`),t=t.replace(/\bbefore:(\d{4}-\d{2}-\d{2})\b/gi,`created:<$1`),t=t.replace(/\bsince:(\d{4}-\d{2}-\d{2})\b/gi,`created:>$1`),t=t.replace(/\buntil:(\d{4}-\d{2}-\d{2})\b/gi,`created:<$1`),t}const Z=T({query:a.string().describe(`Search query string. Use specific terms, not wildcards like "*". Empty string returns all posts.
7
+ `,s.body_md&&(c+=`Content:\n${s.body_md}\n\n---\n\n`),i){case`bullet`:c+=`Please provide a summary in bullet points (3-5 main points).`;break;case`paragraph`:c+=`Please provide a summary in 2-3 paragraphs.`;break;case`keywords`:c+=`Please extract and list 10-15 important keywords from this post.`;break}return _(c)}catch(e){return b(e)}}function se(e,t){console.error(`Setting up MCP prompts...`),e.registerPrompt(`esa_summarize_post`,{title:u(`prompts.summarize_post.title`),description:u(`prompts.summarize_post.description`),argsSchema:ae().shape},async e=>f(t,oe,e))}function x(e,t={}){let{truncateBody:n}=t,r=e.body_md;return n&&r&&r.length>n&&(r=`${r.slice(0,n)}...`),{url:e.url,wip:e.wip?`WIP`:`Shipped`,kind:e.kind,category_and_title_and_tags:e.full_name,body_md:r,created_at:e.created_at,updated_at:e.updated_at,created_by:e.created_by,updated_by:e.updated_by,stats:{tasks_count:e.tasks_count,done_tasks_count:e.done_tasks_count,comments_count:e.comments_count,stargazers_count:e.stargazers_count,watchers_count:e.watchers_count}}}const ce={sort:`updated`,order:`desc`};async function le(e,t){let{teamName:n,uri:r}=t;try{let{data:t,error:i,response:a}=await e.GET(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:n},query:ce}});return i||!a.ok?y(i||a.status,r):g({...t,posts:t.posts?.map(e=>x(e,{truncateBody:500}))},r)}catch(e){return y(e,r)}}function S(e){let t=e.indexOf(`.`);return t>=0?e.substring(0,t):e}const C=a.string().default(``).describe(`Team name (required). Use esa_get_teams first to see available teams.`).transform(S);function w(e){return a.object({teamName:C,...e})}const T=a.object({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`),role:a.enum([`member`,`owner`]).optional().describe(`Filter by role`)});async function E(e,t={}){try{let{data:n,error:r,response:i}=await e.GET(`/v1/teams`,{params:{query:{page:t.page,per_page:t.perPage,role:t.role}}});return r||!i.ok?v(r||i.status):h({...n,teams:n.teams?.map(e=>({url:e.url,name:e.name,description:e.description}))})}catch(e){return v(e)}}const D=w({});async function O(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/stats`,{params:{path:{team_name:t.teamName}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}const k=w({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function A(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/tags`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}const j=w({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`),sort:a.enum([`posts_count`,`joined`,`last_accessed`]).optional().describe(`Sort criteria`),order:a.enum([`desc`,`asc`]).optional().describe(`Sort order`)});async function M(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/members`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage,sort:t.sort,order:t.order}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}async function N(e){try{let t=await f(e,E,{});return JSON.parse(t.content[0].text).teams?.map(e=>({uri:`esa://teams/${e.name}/posts/recent`,name:`Recent posts from ${e.name}`,description:`Recent posts from ${e.name}${e.description?` (${e.description})`:``}`,mimeType:`application/json`}))||[]}catch(e){return console.error(`Failed to list teams:`,e),[]}}function P(e,n){console.error(`Setting up MCP resources...`),e.registerResource(`esa_recent_posts`,new t(`esa://teams/{teamName}/posts/recent`,{list:async()=>({resources:await N(n)})}),{title:`Recent Posts`,description:`Fetch recent updated posts from esa team`,mimeType:`application/json`},async(e,t)=>f(n,le,{...t,uri:e.href}))}const F=[`image/jpeg`,`image/png`,`image/gif`,`image/webp`],I=w({url:a.string().describe(`Attachment URL. Can be a full URL (https://files.esa.io/..., https://dl.esa.io/...) or a path (/uploads/...)`),forceSignedUrl:a.boolean().optional().describe(`If true, always return signed URLs instead of base64-encoded images. Default is false.`)});function L(e){if(e.startsWith(`/`))return e;try{return new URL(e).pathname}catch{return e}}function R(e){return F.includes(e)}async function z(e,t){if(t)return{type:`text`,text:e};let n=await fetch(e);if(!n.ok)throw Error(`Failed to fetch attachment: ${n.status} ${n.statusText}`);let r=n.headers.get(`content-type`)||``,i=n.headers.get(`content-length`),a=i?Number.parseInt(i,10):0;if(R(r)&&a>0&&a<=31457280){let e=await n.arrayBuffer();return{type:`image`,data:Buffer.from(e).toString(`base64`),mimeType:r}}return{type:`text`,text:e}}async function B(e,t){try{if(!t.teamName)throw new p;let n=L(t.url),{data:r,error:i,response:a}=await e.GET(`/v1/teams/{team_name}/signed_urls`,{params:{path:{team_name:t.teamName},query:{urls:n,v:2,expires_in:300}}});if(i||!a.ok)return v(i||a.status);if(!r.signed_urls||r.signed_urls.length===0)throw Error(`No signed URLs returned from API`);let[o,s]=r.signed_urls[0];if(s===null)throw Error(`File not found: ${o}`);let c=t.forceSignedUrl??!1;try{return{content:[await z(s,c)]}}catch(e){throw Error(`Failed to fetch attachment for ${o}: ${e instanceof Error?e.message:String(e)}`)}}catch(e){return v(e)}}function V(e){return{full_name:e.full_name,count:e.count,has_child:e.has_child||!1}}function H(e){return{current_category:e.current_category,categories:e.categories?.map(V)||[],parent_categories:e.parent_categories?.map(e=>({current_category:e.current_category,categories:e.categories?.map(V)||[]})),readme:e.readme,no_category:e.no_category?V(e.no_category):void 0,descendant_posts:e.descendant_posts,posts:e.posts,total_count:e.total_count,per_page:e.per_page,page:e.page,prev_page:e.prev_page,next_page:e.next_page,max_per_page:e.max_per_page}}const U=w({select:a.string().describe(`Category path to retrieve`),include:a.enum([`posts`,`parent_categories`]).optional().describe(`Additional information to include`),descendantPosts:a.boolean().optional().describe(`Include descendant posts (only effective with include=posts)`),page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function W(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/categories`,{params:{path:{team_name:t.teamName},query:{select:t.select,include:t.include,descendant_posts:t.descendantPosts,page:t.page,per_page:t.perPage}}});return r||!i.ok?v(r||i.status):h(H(n))}catch(e){return v(e)}}const ue=w({});async function de(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/categories/top`,{params:{path:{team_name:t.teamName}}});return r||!i.ok?v(r||i.status):h(H(n))}catch(e){return v(e)}}const fe=w({prefix:a.string().optional().describe(`Filter paths starting with specified string (e.g., 'dev' finds 'dev', 'dev/api', 'dev/docs')`),suffix:a.string().optional().describe(`Filter paths ending with specified string (e.g., 'api' finds 'dev/api', 'backend/api')`),match:a.string().optional().describe(`Filter paths containing specified substring anywhere (e.g., 'doc' finds 'docs', 'dev/docs', 'documentation')`),exactMatch:a.string().optional().describe(`Filter paths matching exactly (e.g., 'dev/api' matches only 'dev/api', ignores leading/trailing slashes)`)});async function pe(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/categories/paths`,{params:{path:{team_name:t.teamName},query:{prefix:t.prefix,suffix:t.suffix,match:t.match,exact_match:t.exactMatch}}});return r||!i.ok?v(r||i.status):h(n)}catch(e){return v(e)}}function G(e,t={}){let{truncateBody:n}=t,r=e.body_md;return n&&r&&r.length>n&&(r=`${r.slice(0,n)}...`),{id:e.id,post_number:e.post_number,url:e.url,body_md:r,created_at:e.created_at,updated_at:e.updated_at,created_by:e.created_by,stats:{stargazers_count:e.stargazers_count,star:e.star},stargazers:e.stargazers}}const me=w({commentId:a.number().describe(`The comment ID to retrieve`),include:a.enum([`stargazers`]).optional().describe(`Specify 'stargazers' to include stargazers in the response`)});async function he(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId},query:{include:t.include}}});return r||!i.ok?v(r||i.status):h(G(n))}catch(e){return v(e)}}const ge=w({postNumber:a.number().describe(`The post number to comment on`),bodyMd:a.string().describe(`The comment content in Markdown format`),user:a.string().optional().describe(`Comment author's screen_name (owner permission required)`)});async function _e(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.POST(`/v1/teams/{team_name}/posts/{post_number}/comments`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}},body:{comment:{body_md:t.bodyMd,user:t.user}}});return r||!i.ok?v(r||i.status):h(G(n,{truncateBody:300}))}catch(e){return v(e)}}const ve=w({commentId:a.number().describe(`The comment ID to update`),bodyMd:a.string().describe(`The updated comment content in Markdown format`),user:a.string().optional().describe(`Comment author's screen_name (owner permission required)`)});async function ye(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.PATCH(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId}},body:{comment:{body_md:t.bodyMd,user:t.user}}});return r||!i.ok?v(r||i.status):h(G(n))}catch(e){return v(e)}}const be=w({commentId:a.number().describe(`The comment ID to delete`)});async function xe(e,t){try{if(!t.teamName)throw new p;let{error:n,response:r}=await e.DELETE(`/v1/teams/{team_name}/comments/{comment_id}`,{params:{path:{team_name:t.teamName,comment_id:t.commentId}}});return n||!r.ok?v(n||r.status):h({success:!0,message:`Comment deleted successfully`})}catch(e){return v(e)}}const Se=w({postNumber:a.number().describe(`The post number to get comments for`),page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function Ce(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}/comments`,{params:{path:{team_name:t.teamName,post_number:t.postNumber},query:{page:t.page,per_page:t.perPage}}});if(r||!i.ok)return v(r||i.status);let a=n.comments.map(e=>G(e,{truncateBody:300}));return h({...n,comments:a})}catch(e){return v(e)}}const we=w({page:a.number().optional().describe(`Page number (starts from 1)`),perPage:a.number().optional().describe(`Number of items per page`)});async function Te(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/comments`,{params:{path:{team_name:t.teamName},query:{page:t.page,per_page:t.perPage}}});if(r||!i.ok)return v(r||i.status);let a=n.comments.map(e=>G(e,{truncateBody:300}));return h({...n,comments:a})}catch(e){return v(e)}}function K(e,t){if(!e||t!==void 0)return{name:e,category:t};if(e.includes(`/`)){let t=e.split(`/`),n=t.pop(),r=t.join(`/`);return{name:n||void 0,category:r}}return{name:e,category:t}}const Ee=w({postNumber:a.number().describe(`The post number to retrieve`),include:a.enum([`comments`]).optional().describe(`Specify 'comments' to include comments in the response`)});async function q(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber},query:{include:t.include}}});return r||!i.ok?v(r||i.status):h(x(n))}catch(e){return v(e)}}const De=w({name:a.string().describe(`The post name (title)`),bodyMd:a.string().optional().describe(`The post content in Markdown format`),tags:a.array(a.string()).optional().describe(`Tags for the post`),category:a.string().optional().describe(`Category path (e.g., 'dev/docs')`),wip:a.boolean().default(!0).describe(`Whether the post is Work In Progress. Set to false to ship it (mark as complete and ready to be published)`),message:a.string().optional().describe(`Update message for the post`)});async function J(e,t){try{if(!t.teamName)throw new p;let{name:n,category:r}=K(t.name,t.category),{data:i,error:a,response:o}=await e.POST(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:t.teamName}},body:{post:{name:n,body_md:t.bodyMd,tags:t.tags,category:r,wip:t.wip,message:t.message}}});return a||!o.ok?v(a||o.status):h(x(i))}catch(e){return v(e)}}const Oe=w({postNumber:a.number().describe(`The post number to update`),name:a.string().optional().describe(`The post name (title)`),bodyMd:a.string().optional().describe(`The post content in Markdown format`),tags:a.array(a.string()).optional().describe(`Tags for the post`),category:a.string().optional().describe(`Category path (e.g., 'dev/docs')`),wip:a.boolean().optional().describe(`Whether the post is Work In Progress. Set to false to ship it (mark as complete and ready to be published)`),message:a.string().optional().describe(`Update message for the post`),originalRevision:a.object({bodyMd:a.string(),number:a.number(),user:a.string()}).optional().describe(`Original revision to check for conflicts`)});async function Y(e,t){try{if(!t.teamName)throw new p;let{name:n,category:r}=K(t.name,t.category),{data:i,error:a,response:o}=await e.PATCH(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}},body:{post:{name:n,body_md:t.bodyMd,tags:t.tags,category:r,wip:t.wip,message:t.message,original_revision:t.originalRevision?{body_md:t.originalRevision.bodyMd,number:t.originalRevision.number,user:t.originalRevision.user}:void 0}}});return a||!o.ok?v(a||o.status):h(x(i))}catch(e){return v(e)}}function ke(e){let t=e;return t===`*`&&(t=``),t=t.replace(/\bafter:(\d{4}-\d{2}-\d{2})\b/gi,`created:>$1`),t=t.replace(/\bbefore:(\d{4}-\d{2}-\d{2})\b/gi,`created:<$1`),t=t.replace(/\bsince:(\d{4}-\d{2}-\d{2})\b/gi,`created:>$1`),t=t.replace(/\buntil:(\d{4}-\d{2}-\d{2})\b/gi,`created:<$1`),t}const X=w({query:a.string().describe(`Search query string. Use specific terms, not wildcards like "*". Empty string returns all posts.
8
8
  ## Important Note for Date Queries:
9
9
  **WARNING: Do NOT use 'after:', 'before:', 'since:', or 'until:' syntax (these are from GitHub/Gmail/pplog).
10
10
  Use esa-specific date syntax: created:>YYYY-MM-DD, created:<YYYY-MM-DD, updated:>YYYY-MM-DD, updated:<YYYY-MM-DD
@@ -19,12 +19,12 @@ When searching, apply these strategies:
19
19
  IMPORTANT: Space-separated terms are treated as AND conditions. Use "OR" operator for alternative terms: "word-break OR word-wrap OR overflow-wrap".
20
20
  Advanced search: "tag:release", "category:dev", "wip:false", "keyword:API", "title:設計書".
21
21
  Category search: "on:category" (posts directly in category), "in:category" (posts in category and subcategories), "on:/" (uncategorized posts).
22
- For broader results, use OR between related terms rather than listing them with spaces.`).transform(be),sort:a.enum([`updated`,`created`,`number`,`stars`,`watches`,`comments`,`best_match`]).optional().describe(`Sort key`),order:a.enum([`desc`,`asc`]).optional().describe(`Sort direction`),page:a.number().int().positive().optional().describe(`Page number`),perPage:a.number().int().min(1).max(100).optional().describe(`Items per page`),include:a.enum([`comments`]).optional().describe(`Specify 'comments' to include comments in the response`)});async function Q(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:t.teamName},query:{q:t.query,sort:t.sort,order:t.order,page:t.page,per_page:t.perPage,include:t.include}}});if(r||!i.ok)return v(r||i.status);let a=n.posts.map(e=>S(e,{truncateBody:500}));return h(a)}catch(e){return v(e)}}const $={TEAM:`docs`,SEARCH_OPTIONS_POST_ID:104,MARKDOWN_SYNTAX_POST_ID:49},xe=Z.omit({teamName:!0,order:!0,include:!0,sort:!0});async function Se(e,t){return J(e,{teamName:$.TEAM,postNumber:$.SEARCH_OPTIONS_POST_ID})}async function Ce(e,t){return J(e,{teamName:$.TEAM,postNumber:$.MARKDOWN_SYNTAX_POST_ID})}async function we(e,t){return Q(e,{teamName:$.TEAM,sort:`best_match`,...t})}const Te=T({postNumber:a.number().describe(`The post number to archive`),message:a.string().optional().describe(`Archive message for the post`)});async function Ee(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}}});if(r||!i.ok)return v(r||i.status);let a=n.category||``;if(a.startsWith(`Archived/`))return h({message:`Post is already archived`,category:a});let o=a===``?`Archived`:`Archived/${a}`;return await X(e,{teamName:t.teamName,postNumber:t.postNumber,category:o,message:t.message||`Archive post`})}catch(e){return v(e)}}const De=T({postNumber:a.number().describe(`The post number to ship`)});async function Oe(e,t){try{if(!t.teamName)throw new p;return await X(e,{teamName:t.teamName,postNumber:t.postNumber,wip:!1,message:`Ship It!`})}catch(e){return v(e)}}const ke=T({postNumber:a.number().describe(`The source post number to prepare for duplication`),targetTeamName:a.string().optional().describe(`The name of the esa team`).transform(e=>e?C(e):void 0)});async function Ae(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/new`,{params:{path:{team_name:t.teamName},query:{parent_post_id:t.postNumber}}});if(r||!i.ok)return v(r||i.status);let a=n.post;return Y(e,{teamName:t.targetTeamName||t.teamName,name:a.name,bodyMd:a.body_md,wip:!0})}catch(e){return v(e)}}function je(e,t){console.error(`Setting up MCP tools...`),e.registerTool(`esa_get_teams`,{title:`Get user's accessible esa teams`,description:`Retrieves a list of esa teams that the user has access to.`,inputSchema:E.shape},async e=>f(t,D,e)),e.registerTool(`esa_get_team_stats`,{title:`Get team statistics`,description:`Retrieves team statistics including member count, posts count (total/WIP/shipped), comments, stars, watches, and daily/weekly/monthly active users`,inputSchema:O.shape},async e=>f(t,k,e)),e.registerTool(`esa_get_team_tags`,{title:`Get team tags`,description:`Retrieves all tags used in posts within a team, along with the count of posts for each tag`,inputSchema:A.shape},async e=>f(t,j,e)),e.registerTool(`esa_get_team_members`,{title:`Get team members`,description:`Retrieves all members of a team with their roles and profile information`,inputSchema:M.shape},async e=>f(t,N,e)),e.registerTool(`esa_get_post`,{title:`Get a specific esa post`,description:`Retrieves a specific post from an esa team by post number, with optional comments included.`,inputSchema:_e.shape},async e=>f(t,J,e)),e.registerTool(`esa_search_posts`,{title:`Search Posts`,description:`Search for posts in esa.io`,inputSchema:Z.shape},async e=>f(t,Q,e)),e.registerTool(`esa_create_post`,{title:`Create a new esa post`,description:`Creates a new post in an esa team with optional tags, category, and WIP status.`,inputSchema:ve.shape},async e=>f(t,Y,e)),e.registerTool(`esa_update_post`,{title:`Update an existing esa post`,description:`Updates an existing post in an esa team by post number. You can update the title, content, tags, category, and WIP status. To ship a post (mark as complete), set wip to false - this is preferred over using esa_ship_post when updating other fields simultaneously.`,inputSchema:ye.shape},async e=>f(t,X,e)),e.registerTool(`esa_get_comment`,{title:`Get a specific comment`,description:`Retrieves a specific comment by comment ID, with optional stargazers included.`,inputSchema:U.shape},async e=>f(t,W,e)),e.registerTool(`esa_create_comment`,{title:`Create a new comment on a post`,description:`Creates a new comment on an existing post in an esa team.`,inputSchema:G.shape},async e=>f(t,K,e)),e.registerTool(`esa_update_comment`,{title:`Update an existing comment`,description:`Updates an existing comment in an esa team by comment ID.`,inputSchema:le.shape},async e=>f(t,ue,e)),e.registerTool(`esa_delete_comment`,{title:`Delete a comment`,description:`Deletes a comment from an esa team by comment ID.`,inputSchema:de.shape},async e=>f(t,fe,e)),e.registerTool(`esa_get_post_comments`,{title:`Get comments for a specific post`,description:`Retrieves a list of comments for a specific post with pagination support.`,inputSchema:pe.shape},async e=>f(t,me,e)),e.registerTool(`esa_get_team_comments`,{title:`Get team comments`,description:`Retrieves a list of comments in a team with pagination support.`,inputSchema:he.shape},async e=>f(t,ge,e)),e.registerTool(`esa_get_categories`,{title:`Get categories for a specific path`,description:`Retrieves category information and subcategories for a specific category path, with optional posts and parent categories included`,inputSchema:R.shape},async e=>f(t,z,e)),e.registerTool(`esa_get_top_categories`,{title:`Get top-level categories`,description:`Retrieves all top-level categories for a team`,inputSchema:B.shape},async e=>f(t,V,e)),e.registerTool(`esa_archive_post`,{title:`Archive a post`,description:`Archives a post by moving it to the Archived/ category. If the post is in 'dev/docs', it becomes 'Archived/dev/docs'. Posts without category go to 'Archived'.`,inputSchema:Te.shape},async e=>f(t,Ee,e)),e.registerTool(`esa_ship_post`,{title:`Ship a post`,description:`Ships a post by setting wip to false. This marks the post as complete and ready to be published. Use this only when you need to ship without making other changes - if you're also updating title, content, or other fields, use esa_update_post with wip: false instead.`,inputSchema:De.shape},async e=>f(t,Oe,e)),e.registerTool(`esa_duplicate_post`,{title:`Prepare a post for duplication`,description:`Prepares a post for duplication by retrieving its name and body_md content. Returns the name and body_md that can be used with esa_create_post to create a duplicate of the original post.`,inputSchema:ke.shape},async e=>f(t,Ae,e)),e.registerTool(`esa_get_search_options_help`,{title:`Get esa search options documentation`,description:`Get esa search syntax documentation when you need to construct complex
22
+ For broader results, use OR between related terms rather than listing them with spaces.`).transform(ke),sort:a.enum([`updated`,`created`,`number`,`stars`,`watches`,`comments`,`best_match`]).optional().describe(`Sort key`),order:a.enum([`desc`,`asc`]).optional().describe(`Sort direction`),page:a.number().int().positive().optional().describe(`Page number`),perPage:a.number().int().min(1).max(100).optional().describe(`Items per page`),include:a.enum([`comments`]).optional().describe(`Specify 'comments' to include comments in the response`)});async function Z(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts`,{params:{path:{team_name:t.teamName},query:{q:t.query,sort:t.sort,order:t.order,page:t.page,per_page:t.perPage,include:t.include}}});return r||!i.ok?v(r||i.status):h(n.posts.map(e=>x(e,{truncateBody:500})))}catch(e){return v(e)}}const Q={TEAM:`docs`,SEARCH_OPTIONS_POST_ID:104,MARKDOWN_SYNTAX_POST_ID:49},Ae=X.omit({teamName:!0,order:!0,include:!0,sort:!0});async function $(e,t){return q(e,{teamName:Q.TEAM,postNumber:Q.SEARCH_OPTIONS_POST_ID})}async function je(e,t){return q(e,{teamName:Q.TEAM,postNumber:Q.MARKDOWN_SYNTAX_POST_ID})}async function Me(e,t){return Z(e,{teamName:Q.TEAM,sort:`best_match`,...t})}const Ne=w({postNumber:a.number().describe(`The post number to archive`),message:a.string().optional().describe(`Archive message for the post`)});async function Pe(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/{post_number}`,{params:{path:{team_name:t.teamName,post_number:t.postNumber}}});if(r||!i.ok)return v(r||i.status);let a=n.category||``;if(a.startsWith(`Archived/`))return h({message:`Post is already archived`,category:a});let o=a===``?`Archived`:`Archived/${a}`;return await Y(e,{teamName:t.teamName,postNumber:t.postNumber,category:o,message:t.message||`Archive post`})}catch(e){return v(e)}}const Fe=w({postNumber:a.number().describe(`The post number to ship`)});async function Ie(e,t){try{if(!t.teamName)throw new p;return await Y(e,{teamName:t.teamName,postNumber:t.postNumber,wip:!1,message:`Ship It!`})}catch(e){return v(e)}}const Le=w({postNumber:a.number().describe(`The source post number to prepare for duplication`),targetTeamName:a.string().optional().describe(`The name of the esa team`).transform(e=>e?S(e):void 0)});async function Re(e,t){try{if(!t.teamName)throw new p;let{data:n,error:r,response:i}=await e.GET(`/v1/teams/{team_name}/posts/new`,{params:{path:{team_name:t.teamName},query:{parent_post_id:t.postNumber}}});if(r||!i.ok)return v(r||i.status);let a=n.post;return J(e,{teamName:t.targetTeamName||t.teamName,name:a.name,bodyMd:a.body_md,wip:!0})}catch(e){return v(e)}}function ze(e,t){console.error(`Setting up MCP tools...`),e.registerTool(`esa_get_teams`,{title:`Get user's accessible esa teams`,description:`Retrieves a list of esa teams that the user has access to.`,inputSchema:T.shape},async e=>f(t,E,e)),e.registerTool(`esa_get_team_stats`,{title:`Get team statistics`,description:`Retrieves team statistics including member count, posts count (total/WIP/shipped), comments, stars, watches, and daily/weekly/monthly active users`,inputSchema:D.shape},async e=>f(t,O,e)),e.registerTool(`esa_get_team_tags`,{title:`Get team tags`,description:`Retrieves all tags used in posts within a team, along with the count of posts for each tag`,inputSchema:k.shape},async e=>f(t,A,e)),e.registerTool(`esa_get_team_members`,{title:`Get team members`,description:`Retrieves all members of a team with their roles and profile information`,inputSchema:j.shape},async e=>f(t,M,e)),e.registerTool(`esa_get_post`,{title:`Get a specific esa post`,description:`Retrieves a specific post from an esa team by post number, with optional comments included.`,inputSchema:Ee.shape},async e=>f(t,q,e)),e.registerTool(`esa_search_posts`,{title:`Search Posts`,description:`Search for posts in esa.io`,inputSchema:X.shape},async e=>f(t,Z,e)),e.registerTool(`esa_create_post`,{title:`Create a new esa post`,description:`Creates a new post in an esa team with optional tags, category, and WIP status.`,inputSchema:De.shape},async e=>f(t,J,e)),e.registerTool(`esa_update_post`,{title:`Update an existing esa post`,description:`Updates an existing post in an esa team by post number. You can update the title, content, tags, category, and WIP status. To ship a post (mark as complete), set wip to false - this is preferred over using esa_ship_post when updating other fields simultaneously.`,inputSchema:Oe.shape},async e=>f(t,Y,e)),e.registerTool(`esa_get_comment`,{title:`Get a specific comment`,description:`Retrieves a specific comment by comment ID, with optional stargazers included.`,inputSchema:me.shape},async e=>f(t,he,e)),e.registerTool(`esa_create_comment`,{title:`Create a new comment on a post`,description:`Creates a new comment on an existing post in an esa team.`,inputSchema:ge.shape},async e=>f(t,_e,e)),e.registerTool(`esa_update_comment`,{title:`Update an existing comment`,description:`Updates an existing comment in an esa team by comment ID.`,inputSchema:ve.shape},async e=>f(t,ye,e)),e.registerTool(`esa_delete_comment`,{title:`Delete a comment`,description:`Deletes a comment from an esa team by comment ID.`,inputSchema:be.shape},async e=>f(t,xe,e)),e.registerTool(`esa_get_post_comments`,{title:`Get comments for a specific post`,description:`Retrieves a list of comments for a specific post with pagination support.`,inputSchema:Se.shape},async e=>f(t,Ce,e)),e.registerTool(`esa_get_team_comments`,{title:`Get team comments`,description:`Retrieves a list of comments in a team with pagination support.`,inputSchema:we.shape},async e=>f(t,Te,e)),e.registerTool(`esa_get_categories`,{title:`Get categories for a specific path`,description:`Retrieves category information and subcategories for a specific category path, with optional posts and parent categories included`,inputSchema:U.shape},async e=>f(t,W,e)),e.registerTool(`esa_get_top_categories`,{title:`Get top-level categories`,description:`Retrieves all top-level categories for a team`,inputSchema:ue.shape},async e=>f(t,de,e)),e.registerTool(`esa_get_all_category_paths`,{title:`Get all category paths for organization and structure review`,description:`Retrieves all category paths in a team at once to understand the overall category structure. Perfect for category organization, cleanup, migration planning, or finding similar categories. Returns a simple list of paths with post counts, sorted in lexicographic order. Supports filtering (prefix/suffix/match/exact_match) to find categories by pattern. No pagination - gets all categories in one call.`,inputSchema:fe.shape},async e=>f(t,pe,e)),e.registerTool(`esa_archive_post`,{title:`Archive a post`,description:`Archives a post by moving it to the Archived/ category. If the post is in 'dev/docs', it becomes 'Archived/dev/docs'. Posts without category go to 'Archived'.`,inputSchema:Ne.shape},async e=>f(t,Pe,e)),e.registerTool(`esa_ship_post`,{title:`Ship a post`,description:`Ships a post by setting wip to false. This marks the post as complete and ready to be published. Use this only when you need to ship without making other changes - if you're also updating title, content, or other fields, use esa_update_post with wip: false instead.`,inputSchema:Fe.shape},async e=>f(t,Ie,e)),e.registerTool(`esa_duplicate_post`,{title:`Prepare a post for duplication`,description:`Prepares a post for duplication by retrieving its name and body_md content. Returns the name and body_md that can be used with esa_create_post to create a duplicate of the original post.`,inputSchema:Le.shape},async e=>f(t,Re,e)),e.registerTool(`esa_get_search_options_help`,{title:`Get esa search options documentation`,description:`Get esa search syntax documentation when you need to construct complex
23
23
  search queries. Use this BEFORE esa_search_posts if you're unsure how to
24
24
  translate user's search requirements into proper esa query syntax (e.g., date
25
- ranges, tag filters, category searches, advanced operators).`,inputSchema:{}},async e=>f(t,Se,e)),e.registerTool(`esa_get_markdown_syntax_help`,{title:`Get esa Markdown syntax documentation`,description:`Get esa Markdown and formatting documentation when unsure about syntax.
25
+ ranges, tag filters, category searches, advanced operators).`,inputSchema:{}},async e=>f(t,$,e)),e.registerTool(`esa_get_markdown_syntax_help`,{title:`Get esa Markdown syntax documentation`,description:`Get esa Markdown and formatting documentation when unsure about syntax.
26
26
  Use this BEFORE using any tools with *_md parameters (like esa_create_post,
27
27
  esa_update_post, esa_create_comment, esa_update_comment) if you need
28
- clarification on Markdown syntax, esa-specific extensions, or formatting options.`,inputSchema:{}},async e=>f(t,Ce,e)),e.registerTool(`esa_search_help`,{title:`Search esa documentation and help`,description:`Search esa documentation for features, terminology, and specifications.
28
+ clarification on Markdown syntax, esa-specific extensions, or formatting options.`,inputSchema:{}},async e=>f(t,je,e)),e.registerTool(`esa_search_help`,{title:`Search esa documentation and help`,description:`Search esa documentation for features, terminology, and specifications.
29
29
  Use this when users mention esa-specific terms, ask about esa functionality,
30
- or request help with esa workflows that you're not familiar with.`,inputSchema:xe.shape},async e=>f(t,we,e))}try{c()}catch(e){console.error(`Configuration error:`,e),process.exit(1)}async function Me(){await ne();let t=new e({name:s.server.name,version:s.server.version});je(t,s.esa),F(t,s.esa),oe(t,s.esa);let r=new n;r.onclose=()=>{console.error(`Transport closed`)},r.onerror=e=>{console.error(`Transport error:`,e)},await t.connect(r),console.error(`${s.server.name} v${s.server.version} started`)}await Me().catch(e=>{console.error(`Server startup error:`,e),process.exit(1)});export{};
30
+ or request help with esa workflows that you're not familiar with.`,inputSchema:Ae.shape},async e=>f(t,Me,e)),e.registerTool(`esa_get_attachment`,{title:`Get attachment file from esa`,description:`Retrieves an attachment file from esa with signed URLs. For supported images (JPEG, PNG, GIF, WebP) under 30MB, returns base64-encoded data. For other file types, larger images, or when forceSignedUrl is true, returns signed URLs.`,inputSchema:I.shape},async e=>f(t,B,e))}try{c()}catch(e){console.error(`Configuration error:`,e),process.exit(1)}async function Be(){await te();let t=new e({name:s.server.name,version:s.server.version});ze(t,s.esa),P(t,s.esa),se(t,s.esa);let r=new n;r.onclose=()=>{console.error(`Transport closed`)},r.onerror=e=>{console.error(`Transport error:`,e)},await t.connect(r),console.error(`${s.server.name} v${s.server.version} started`)}await Be().catch(e=>{console.error(`Server startup error:`,e),process.exit(1)});export{};
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.2.4/schema.json",
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
3
  "assist": {
4
4
  "actions": {
5
5
  "source": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esaio/esa-mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Official MCP server for esa.io - STDIO transport version",
5
5
  "main": "bin/index.js",
6
6
  "type": "module",
@@ -14,7 +14,8 @@
14
14
  "test:coverage": "vitest run --coverage",
15
15
  "lint": "biome check . --error-on-warnings",
16
16
  "lint:fix": "biome check --write .",
17
- "type-check": "tsc --noEmit"
17
+ "type-check": "tsc --noEmit",
18
+ "update-esa-api": "openapi-typescript ../esa/api/openapi.yaml --output src/generated/api-types.ts && npm run lint:fix"
18
19
  },
19
20
  "keywords": [
20
21
  "mcp",
@@ -29,20 +30,21 @@
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/sdk": "^1.18.0",
31
32
  "i18next": "^25.5.2",
32
- "openapi-fetch": "^0.14.0",
33
+ "openapi-fetch": "^0.15.0",
33
34
  "zod": "^3.23.8"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@biomejs/biome": "^2.2.4",
37
- "@types/node": "^20.19.14",
38
- "@vitest/coverage-v8": "^3.2.4",
38
+ "@types/node": "^20.19.15",
39
+ "@vitest/coverage-v8": "^4.0.5",
39
40
  "openapi-typescript": "^7.9.1",
40
41
  "tsdown": "^0.15.1",
41
42
  "tsx": "^4.20.5",
42
43
  "typescript": "^5.9.2",
43
- "vitest": "^3.2.4"
44
+ "vitest": "^4.0.5"
44
45
  },
45
46
  "engines": {
46
- "node": ">=20.19.4"
47
+ "node": ">=20.19.4",
48
+ "npm": ">=11.6.1"
47
49
  }
48
50
  }
@@ -52,8 +52,15 @@ describe("MCP Server", () => {
52
52
  initI18n?: ReturnType<typeof vi.fn>;
53
53
  },
54
54
  ) => {
55
- const MockMcpServer = vi.fn().mockReturnValue(mockServer);
56
- const MockStdioServerTransport = vi.fn().mockReturnValue(mockTransport);
55
+ // Use function keyword for constructor mocks (required in vitest v4)
56
+ // biome-ignore lint/complexity/useArrowFunction: Constructor mocks in vitest v4 require function keyword, not arrow functions
57
+ const MockMcpServer = vi.fn(function (..._args: unknown[]) {
58
+ return mockServer;
59
+ });
60
+ // biome-ignore lint/complexity/useArrowFunction: Constructor mocks in vitest v4 require function keyword, not arrow functions
61
+ const MockStdioServerTransport = vi.fn(function (..._args: unknown[]) {
62
+ return mockTransport;
63
+ });
57
64
 
58
65
  vi.doMock("../config/index.js", () => mockConfig);
59
66
  vi.doMock("@modelcontextprotocol/sdk/server/mcp.js", () => ({
@@ -1,9 +1,10 @@
1
+ import type { MockInstance } from "vitest";
1
2
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
3
  import { createAuthMiddleware } from "../middleware.js";
3
4
 
4
5
  describe("createAuthMiddleware", () => {
5
6
  // Mock console.error to verify rate limit and error logging without console output
6
- let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
7
+ let consoleErrorSpy: MockInstance<typeof console.error>;
7
8
 
8
9
  // Create test request for authorization header testing
9
10
  const createTestRequest = (url = "https://api.esa.example.com/test") =>