@esaio/esa-mcp-server 0.2.0 → 0.2.2

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.
Files changed (78) hide show
  1. package/README.en.md +10 -6
  2. package/README.md +16 -12
  3. package/bin/{index.js → index.mjs} +1 -1
  4. package/package.json +22 -3
  5. package/.dockerignore +0 -36
  6. package/.github/dependabot.yml +0 -23
  7. package/.github/workflows/docker-publish.yml +0 -120
  8. package/.github/workflows/main.yml +0 -41
  9. package/CLAUDE.md +0 -94
  10. package/Dockerfile +0 -34
  11. package/biome.json +0 -57
  12. package/src/__tests__/fixtures/mock-comment.ts +0 -90
  13. package/src/__tests__/fixtures/mock-post.ts +0 -79
  14. package/src/__tests__/index.test.ts +0 -216
  15. package/src/api_client/__tests__/index.test.ts +0 -149
  16. package/src/api_client/__tests__/middleware.test.ts +0 -120
  17. package/src/api_client/__tests__/with-context.test.ts +0 -98
  18. package/src/api_client/index.ts +0 -29
  19. package/src/api_client/middleware.ts +0 -21
  20. package/src/api_client/with-context.ts +0 -26
  21. package/src/config/__tests__/index.test.ts +0 -65
  22. package/src/config/index.ts +0 -20
  23. package/src/context/mcp-context.ts +0 -1
  24. package/src/context/stdio-context.ts +0 -6
  25. package/src/errors/missing-team-name-error.ts +0 -8
  26. package/src/formatters/__tests__/mcp-response.test.ts +0 -106
  27. package/src/formatters/mcp-response.ts +0 -95
  28. package/src/generated/api-types.ts +0 -2968
  29. package/src/i18n/__tests__/index.test.ts +0 -53
  30. package/src/i18n/index.ts +0 -39
  31. package/src/index.ts +0 -47
  32. package/src/locales/en.json +0 -13
  33. package/src/locales/ja.json +0 -13
  34. package/src/prompts/__tests__/index.test.ts +0 -48
  35. package/src/prompts/__tests__/summarize-post.test.ts +0 -291
  36. package/src/prompts/index.ts +0 -21
  37. package/src/prompts/summarize-post.ts +0 -94
  38. package/src/resources/__tests__/index.test.ts +0 -50
  39. package/src/resources/__tests__/recent-posts-list.test.ts +0 -92
  40. package/src/resources/__tests__/recent-posts.test.ts +0 -270
  41. package/src/resources/index.ts +0 -33
  42. package/src/resources/recent-posts-list.ts +0 -22
  43. package/src/resources/recent-posts.ts +0 -45
  44. package/src/schemas/team-name-schema.ts +0 -19
  45. package/src/tools/__tests__/attachments.test.ts +0 -460
  46. package/src/tools/__tests__/categories.test.ts +0 -402
  47. package/src/tools/__tests__/comments.test.ts +0 -970
  48. package/src/tools/__tests__/helps.test.ts +0 -222
  49. package/src/tools/__tests__/index.test.ts +0 -48
  50. package/src/tools/__tests__/post-actions.test.ts +0 -445
  51. package/src/tools/__tests__/posts.test.ts +0 -917
  52. package/src/tools/__tests__/search.test.ts +0 -339
  53. package/src/tools/__tests__/teams.test.ts +0 -615
  54. package/src/tools/attachments.ts +0 -167
  55. package/src/tools/categories.ts +0 -153
  56. package/src/tools/comments.ts +0 -258
  57. package/src/tools/helps.ts +0 -50
  58. package/src/tools/index.ts +0 -351
  59. package/src/tools/post-actions.ts +0 -132
  60. package/src/tools/posts.ts +0 -179
  61. package/src/tools/search.ts +0 -98
  62. package/src/tools/teams.ts +0 -157
  63. package/src/transformers/__tests__/category-transformer.test.ts +0 -161
  64. package/src/transformers/__tests__/comment-transformer.test.ts +0 -129
  65. package/src/transformers/__tests__/post-name-normalizer.test.ts +0 -53
  66. package/src/transformers/__tests__/post-transformer.test.ts +0 -70
  67. package/src/transformers/__tests__/query-normalizer.test.ts +0 -98
  68. package/src/transformers/__tests__/team-name-normalizer.test.ts +0 -21
  69. package/src/transformers/category-transformer.ts +0 -36
  70. package/src/transformers/comment-transformer.ts +0 -34
  71. package/src/transformers/post-name-normalizer.ts +0 -30
  72. package/src/transformers/post-transformer.ts +0 -38
  73. package/src/transformers/query-normalizer.ts +0 -36
  74. package/src/transformers/team-name-normalizer.ts +0 -7
  75. package/tsconfig.build.json +0 -4
  76. package/tsconfig.json +0 -30
  77. package/tsdown.config.ts +0 -13
  78. package/vitest.config.ts +0 -24
@@ -1,167 +0,0 @@
1
- import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
- import { z } from "zod";
3
- import type { createEsaClient } from "../api_client/index.js";
4
- import { MissingTeamNameError } from "../errors/missing-team-name-error.js";
5
- import { formatToolError } from "../formatters/mcp-response.js";
6
- import { createSchemaWithTeamName } from "../schemas/team-name-schema.js";
7
-
8
- // Maximum file size for base64 encoding (30MB)
9
- const MAX_IMAGE_SIZE = 30 * 1024 * 1024;
10
-
11
- // Supported image MIME types for base64 encoding
12
- const SUPPORTED_IMAGE_TYPES = [
13
- "image/jpeg",
14
- "image/png",
15
- "image/gif",
16
- "image/webp",
17
- ] as const;
18
-
19
- export const getAttachmentSchema = createSchemaWithTeamName({
20
- url: z
21
- .string()
22
- .describe(
23
- "Attachment URL. Can be a full URL (https://files.esa.io/..., https://dl.esa.io/...) or a path (/uploads/...)",
24
- ),
25
- forceSignedUrl: z
26
- .boolean()
27
- .optional()
28
- .describe(
29
- "If true, always return signed URLs instead of base64-encoded images. Default is false.",
30
- ),
31
- });
32
-
33
- /**
34
- * Extracts the path from a full URL or returns the input if it's already a path
35
- */
36
- function normalizeUrl(url: string): string {
37
- // If it's already a path (starts with /), return as-is
38
- if (url.startsWith("/")) {
39
- return url;
40
- }
41
-
42
- try {
43
- // Try to parse as URL and extract pathname
44
- const urlObj = new URL(url);
45
- return urlObj.pathname;
46
- } catch {
47
- // If URL parsing fails, assume it's already a path
48
- return url;
49
- }
50
- }
51
-
52
- /**
53
- * Checks if the MIME type is a supported image format
54
- */
55
- function isSupportedImage(mimeType: string): boolean {
56
- return SUPPORTED_IMAGE_TYPES.includes(
57
- mimeType as (typeof SUPPORTED_IMAGE_TYPES)[number],
58
- );
59
- }
60
-
61
- /**
62
- * Fetches content and returns base64-encoded data if it's a supported image under size limit
63
- */
64
- async function fetchAttachment(
65
- signedUrl: string,
66
- forceSignedUrl: boolean,
67
- ): Promise<
68
- | { type: "image"; data: string; mimeType: string }
69
- | { type: "text"; text: string }
70
- > {
71
- // If forceSignedUrl is true, always return signed URL
72
- if (forceSignedUrl) {
73
- return {
74
- type: "text" as const,
75
- text: signedUrl,
76
- };
77
- }
78
-
79
- const response = await fetch(signedUrl);
80
-
81
- if (!response.ok) {
82
- throw new Error(
83
- `Failed to fetch attachment: ${response.status} ${response.statusText}`,
84
- );
85
- }
86
-
87
- const contentType = response.headers.get("content-type") || "";
88
- const contentLength = response.headers.get("content-length");
89
- const size = contentLength ? Number.parseInt(contentLength, 10) : 0;
90
-
91
- // Check if it's a supported image and within size limit
92
- if (isSupportedImage(contentType) && size > 0 && size <= MAX_IMAGE_SIZE) {
93
- const arrayBuffer = await response.arrayBuffer();
94
- const buffer = Buffer.from(arrayBuffer);
95
- const base64 = buffer.toString("base64");
96
-
97
- return {
98
- type: "image" as const,
99
- data: base64,
100
- mimeType: contentType,
101
- };
102
- }
103
-
104
- // For non-images or oversized images, return the signed URL
105
- return {
106
- type: "text" as const,
107
- text: signedUrl,
108
- };
109
- }
110
-
111
- export async function getAttachment(
112
- client: ReturnType<typeof createEsaClient>,
113
- args: z.infer<typeof getAttachmentSchema>,
114
- ): Promise<CallToolResult> {
115
- try {
116
- if (!args.teamName) {
117
- throw new MissingTeamNameError();
118
- }
119
-
120
- // Normalize URL to path
121
- const normalizedUrl = normalizeUrl(args.url);
122
-
123
- // Get signed URL from esa API
124
- const { data, error, response } = await client.GET(
125
- "/v1/teams/{team_name}/signed_urls",
126
- {
127
- params: {
128
- path: { team_name: args.teamName },
129
- query: {
130
- urls: normalizedUrl,
131
- v: 2,
132
- expires_in: 300, // 5 minutes
133
- },
134
- },
135
- },
136
- );
137
-
138
- if (error || !response.ok) {
139
- return formatToolError(error || response.status);
140
- }
141
-
142
- if (!data.signed_urls || data.signed_urls.length === 0) {
143
- throw new Error("No signed URLs returned from API");
144
- }
145
-
146
- const [originalUrl, signedUrl] = data.signed_urls[0];
147
-
148
- if (signedUrl === null) {
149
- throw new Error(`File not found: ${originalUrl}`);
150
- }
151
-
152
- const forceSignedUrl = args.forceSignedUrl ?? false;
153
-
154
- try {
155
- const result = await fetchAttachment(signedUrl, forceSignedUrl);
156
- return { content: [result] };
157
- } catch (err) {
158
- throw new Error(
159
- `Failed to fetch attachment for ${originalUrl}: ${
160
- err instanceof Error ? err.message : String(err)
161
- }`,
162
- );
163
- }
164
- } catch (err) {
165
- return formatToolError(err);
166
- }
167
- }
@@ -1,153 +0,0 @@
1
- import { z } from "zod";
2
- import type { createEsaClient } from "../api_client/index.js";
3
- import { MissingTeamNameError } from "../errors/missing-team-name-error.js";
4
- import {
5
- formatToolError,
6
- formatToolResponse,
7
- } from "../formatters/mcp-response.js";
8
- import type { components } from "../generated/api-types.js";
9
- import { createSchemaWithTeamName } from "../schemas/team-name-schema.js";
10
- import { transformCategoryList } from "../transformers/category-transformer.js";
11
-
12
- export const getCategoriesSchema = createSchemaWithTeamName({
13
- select: z.string().describe("Category path to retrieve"),
14
- include: z
15
- .enum(["posts", "parent_categories"])
16
- .optional()
17
- .describe("Additional information to include"),
18
- descendantPosts: z
19
- .boolean()
20
- .optional()
21
- .describe("Include descendant posts (only effective with include=posts)"),
22
- page: z.number().optional().describe("Page number (starts from 1)"),
23
- perPage: z.number().optional().describe("Number of items per page"),
24
- });
25
-
26
- export async function getCategories(
27
- client: ReturnType<typeof createEsaClient>,
28
- args: z.infer<typeof getCategoriesSchema>,
29
- ) {
30
- try {
31
- if (!args.teamName) {
32
- throw new MissingTeamNameError();
33
- }
34
- const { data, error, response } = await client.GET(
35
- "/v1/teams/{team_name}/categories",
36
- {
37
- params: {
38
- path: { team_name: args.teamName },
39
- query: {
40
- select: args.select,
41
- include: args.include,
42
- descendant_posts: args.descendantPosts,
43
- page: args.page,
44
- per_page: args.perPage,
45
- },
46
- },
47
- },
48
- );
49
-
50
- if (error || !response.ok) {
51
- return formatToolError(error || response.status);
52
- }
53
-
54
- const categoryList: components["schemas"]["CategoryList"] = data;
55
- const transformed = transformCategoryList(categoryList);
56
-
57
- return formatToolResponse(transformed);
58
- } catch (error) {
59
- return formatToolError(error);
60
- }
61
- }
62
-
63
- export const getTopCategoriesSchema = createSchemaWithTeamName({});
64
-
65
- export async function getTopCategories(
66
- client: ReturnType<typeof createEsaClient>,
67
- args: z.infer<typeof getTopCategoriesSchema>,
68
- ) {
69
- try {
70
- if (!args.teamName) {
71
- throw new MissingTeamNameError();
72
- }
73
- const { data, error, response } = await client.GET(
74
- "/v1/teams/{team_name}/categories/top",
75
- {
76
- params: {
77
- path: { team_name: args.teamName },
78
- },
79
- },
80
- );
81
-
82
- if (error || !response.ok) {
83
- return formatToolError(error || response.status);
84
- }
85
-
86
- const categoryList: components["schemas"]["CategoryList"] = data;
87
- const transformed = transformCategoryList(categoryList);
88
-
89
- return formatToolResponse(transformed);
90
- } catch (error) {
91
- return formatToolError(error);
92
- }
93
- }
94
-
95
- export const getAllCategoryPathsSchema = createSchemaWithTeamName({
96
- prefix: z
97
- .string()
98
- .optional()
99
- .describe(
100
- "Filter paths starting with specified string (e.g., 'dev' finds 'dev', 'dev/api', 'dev/docs')",
101
- ),
102
- suffix: z
103
- .string()
104
- .optional()
105
- .describe(
106
- "Filter paths ending with specified string (e.g., 'api' finds 'dev/api', 'backend/api')",
107
- ),
108
- match: z
109
- .string()
110
- .optional()
111
- .describe(
112
- "Filter paths containing specified substring anywhere (e.g., 'doc' finds 'docs', 'dev/docs', 'documentation')",
113
- ),
114
- exactMatch: z
115
- .string()
116
- .optional()
117
- .describe(
118
- "Filter paths matching exactly (e.g., 'dev/api' matches only 'dev/api', ignores leading/trailing slashes)",
119
- ),
120
- });
121
-
122
- export async function getAllCategoryPaths(
123
- client: ReturnType<typeof createEsaClient>,
124
- args: z.infer<typeof getAllCategoryPathsSchema>,
125
- ) {
126
- try {
127
- if (!args.teamName) {
128
- throw new MissingTeamNameError();
129
- }
130
- const { data, error, response } = await client.GET(
131
- "/v1/teams/{team_name}/categories/paths",
132
- {
133
- params: {
134
- path: { team_name: args.teamName },
135
- query: {
136
- prefix: args.prefix,
137
- suffix: args.suffix,
138
- match: args.match,
139
- exact_match: args.exactMatch,
140
- },
141
- },
142
- },
143
- );
144
-
145
- if (error || !response.ok) {
146
- return formatToolError(error || response.status);
147
- }
148
-
149
- return formatToolResponse(data);
150
- } catch (error) {
151
- return formatToolError(error);
152
- }
153
- }
@@ -1,258 +0,0 @@
1
- import { z } from "zod";
2
- import type { createEsaClient } from "../api_client/index.js";
3
- import { MissingTeamNameError } from "../errors/missing-team-name-error.js";
4
- import {
5
- formatToolError,
6
- formatToolResponse,
7
- } from "../formatters/mcp-response.js";
8
- import type { components } from "../generated/api-types.js";
9
- import { createSchemaWithTeamName } from "../schemas/team-name-schema.js";
10
- import { transformComment } from "../transformers/comment-transformer.js";
11
-
12
- export const getCommentSchema = createSchemaWithTeamName({
13
- commentId: z.number().describe("The comment ID to retrieve"),
14
- include: z
15
- .enum(["stargazers"])
16
- .optional()
17
- .describe("Specify 'stargazers' to include stargazers in the response"),
18
- });
19
-
20
- export async function getComment(
21
- client: ReturnType<typeof createEsaClient>,
22
- args: z.infer<typeof getCommentSchema>,
23
- ) {
24
- try {
25
- if (!args.teamName) {
26
- throw new MissingTeamNameError();
27
- }
28
- const { data, error, response } = await client.GET(
29
- "/v1/teams/{team_name}/comments/{comment_id}",
30
- {
31
- params: {
32
- path: { team_name: args.teamName, comment_id: args.commentId },
33
- query: {
34
- include: args.include,
35
- },
36
- },
37
- },
38
- );
39
-
40
- if (error || !response.ok) {
41
- return formatToolError(error || response.status);
42
- }
43
-
44
- const comment: components["schemas"]["Comment"] = data;
45
- const transformed = transformComment(comment);
46
-
47
- return formatToolResponse(transformed);
48
- } catch (error) {
49
- return formatToolError(error);
50
- }
51
- }
52
-
53
- export const createCommentSchema = createSchemaWithTeamName({
54
- postNumber: z.number().describe("The post number to comment on"),
55
- bodyMd: z.string().describe("The comment content in Markdown format"),
56
- user: z
57
- .string()
58
- .optional()
59
- .describe("Comment author's screen_name (owner permission required)"),
60
- });
61
-
62
- export async function createComment(
63
- client: ReturnType<typeof createEsaClient>,
64
- args: z.infer<typeof createCommentSchema>,
65
- ) {
66
- try {
67
- if (!args.teamName) {
68
- throw new MissingTeamNameError();
69
- }
70
- const { data, error, response } = await client.POST(
71
- "/v1/teams/{team_name}/posts/{post_number}/comments",
72
- {
73
- params: {
74
- path: { team_name: args.teamName, post_number: args.postNumber },
75
- },
76
- body: {
77
- comment: {
78
- body_md: args.bodyMd,
79
- user: args.user,
80
- } as components["schemas"]["CommentInput"],
81
- },
82
- },
83
- );
84
-
85
- if (error || !response.ok) {
86
- return formatToolError(error || response.status);
87
- }
88
-
89
- const comment: components["schemas"]["Comment"] = data;
90
- const transformed = transformComment(comment, { truncateBody: 300 });
91
-
92
- return formatToolResponse(transformed);
93
- } catch (error) {
94
- return formatToolError(error);
95
- }
96
- }
97
-
98
- export const updateCommentSchema = createSchemaWithTeamName({
99
- commentId: z.number().describe("The comment ID to update"),
100
- bodyMd: z.string().describe("The updated comment content in Markdown format"),
101
- user: z
102
- .string()
103
- .optional()
104
- .describe("Comment author's screen_name (owner permission required)"),
105
- });
106
-
107
- export async function updateComment(
108
- client: ReturnType<typeof createEsaClient>,
109
- args: z.infer<typeof updateCommentSchema>,
110
- ) {
111
- try {
112
- if (!args.teamName) {
113
- throw new MissingTeamNameError();
114
- }
115
- const { data, error, response } = await client.PATCH(
116
- "/v1/teams/{team_name}/comments/{comment_id}",
117
- {
118
- params: {
119
- path: { team_name: args.teamName, comment_id: args.commentId },
120
- },
121
- body: {
122
- comment: {
123
- body_md: args.bodyMd,
124
- user: args.user,
125
- } as components["schemas"]["CommentInput"],
126
- },
127
- },
128
- );
129
-
130
- if (error || !response.ok) {
131
- return formatToolError(error || response.status);
132
- }
133
-
134
- const comment: components["schemas"]["Comment"] = data;
135
- const transformed = transformComment(comment);
136
-
137
- return formatToolResponse(transformed);
138
- } catch (error) {
139
- return formatToolError(error);
140
- }
141
- }
142
-
143
- export const deleteCommentSchema = createSchemaWithTeamName({
144
- commentId: z.number().describe("The comment ID to delete"),
145
- });
146
-
147
- export async function deleteComment(
148
- client: ReturnType<typeof createEsaClient>,
149
- args: z.infer<typeof deleteCommentSchema>,
150
- ) {
151
- try {
152
- if (!args.teamName) {
153
- throw new MissingTeamNameError();
154
- }
155
- const { error, response } = await client.DELETE(
156
- "/v1/teams/{team_name}/comments/{comment_id}",
157
- {
158
- params: {
159
- path: { team_name: args.teamName, comment_id: args.commentId },
160
- },
161
- },
162
- );
163
-
164
- if (error || !response.ok) {
165
- return formatToolError(error || response.status);
166
- }
167
-
168
- return formatToolResponse({
169
- success: true,
170
- message: "Comment deleted successfully",
171
- });
172
- } catch (error) {
173
- return formatToolError(error);
174
- }
175
- }
176
-
177
- export const getPostCommentsSchema = createSchemaWithTeamName({
178
- postNumber: z.number().describe("The post number to get comments for"),
179
- page: z.number().optional().describe("Page number (starts from 1)"),
180
- perPage: z.number().optional().describe("Number of items per page"),
181
- });
182
-
183
- export async function getPostComments(
184
- client: ReturnType<typeof createEsaClient>,
185
- args: z.infer<typeof getPostCommentsSchema>,
186
- ) {
187
- try {
188
- if (!args.teamName) {
189
- throw new MissingTeamNameError();
190
- }
191
- const { data, error, response } = await client.GET(
192
- "/v1/teams/{team_name}/posts/{post_number}/comments",
193
- {
194
- params: {
195
- path: { team_name: args.teamName, post_number: args.postNumber },
196
- query: {
197
- page: args.page,
198
- per_page: args.perPage,
199
- },
200
- },
201
- },
202
- );
203
-
204
- if (error || !response.ok) {
205
- return formatToolError(error || response.status);
206
- }
207
-
208
- const comments: components["schemas"]["Comment"][] = data.comments;
209
- const transformed = comments.map((comment) =>
210
- transformComment(comment, { truncateBody: 300 }),
211
- );
212
-
213
- return formatToolResponse({ ...data, comments: transformed });
214
- } catch (error) {
215
- return formatToolError(error);
216
- }
217
- }
218
-
219
- export const getTeamCommentsSchema = createSchemaWithTeamName({
220
- page: z.number().optional().describe("Page number (starts from 1)"),
221
- perPage: z.number().optional().describe("Number of items per page"),
222
- });
223
-
224
- export async function getTeamComments(
225
- client: ReturnType<typeof createEsaClient>,
226
- args: z.infer<typeof getTeamCommentsSchema>,
227
- ) {
228
- try {
229
- if (!args.teamName) {
230
- throw new MissingTeamNameError();
231
- }
232
- const { data, error, response } = await client.GET(
233
- "/v1/teams/{team_name}/comments",
234
- {
235
- params: {
236
- path: { team_name: args.teamName },
237
- query: {
238
- page: args.page,
239
- per_page: args.perPage,
240
- },
241
- },
242
- },
243
- );
244
-
245
- if (error || !response.ok) {
246
- return formatToolError(error || response.status);
247
- }
248
-
249
- const comments: components["schemas"]["Comment"][] = data.comments;
250
- const transformed = comments.map((comment) =>
251
- transformComment(comment, { truncateBody: 300 }),
252
- );
253
-
254
- return formatToolResponse({ ...data, comments: transformed });
255
- } catch (error) {
256
- return formatToolError(error);
257
- }
258
- }
@@ -1,50 +0,0 @@
1
- import type { z } from "zod";
2
- import type { createEsaClient } from "../api_client/index.js";
3
- import { getPost } from "./posts.js";
4
- import { searchPosts, searchPostsSchema } from "./search.js";
5
-
6
- // Documentation team and post constants
7
- export const HELP_DOCS = {
8
- TEAM: "docs",
9
- SEARCH_OPTIONS_POST_ID: 104,
10
- MARKDOWN_SYNTAX_POST_ID: 49,
11
- } as const;
12
-
13
- // Schema for searchHelp - omit teamName, order, include, and sort from searchPostsSchema
14
- export const searchHelpSchema = searchPostsSchema.omit({
15
- teamName: true,
16
- order: true,
17
- include: true,
18
- sort: true,
19
- });
20
-
21
- export async function getSearchOptionsHelp(
22
- client: ReturnType<typeof createEsaClient>,
23
- _args: Record<string, never>,
24
- ) {
25
- return getPost(client, {
26
- teamName: HELP_DOCS.TEAM,
27
- postNumber: HELP_DOCS.SEARCH_OPTIONS_POST_ID,
28
- });
29
- }
30
-
31
- export async function getMarkdownSyntaxHelp(
32
- client: ReturnType<typeof createEsaClient>,
33
- _args: Record<string, never>,
34
- ) {
35
- return getPost(client, {
36
- teamName: HELP_DOCS.TEAM,
37
- postNumber: HELP_DOCS.MARKDOWN_SYNTAX_POST_ID,
38
- });
39
- }
40
-
41
- export async function searchHelp(
42
- client: ReturnType<typeof createEsaClient>,
43
- args: z.infer<typeof searchHelpSchema>,
44
- ) {
45
- return searchPosts(client, {
46
- teamName: HELP_DOCS.TEAM,
47
- sort: "best_match",
48
- ...args,
49
- });
50
- }