cucumberstudio-mcp 1.1.0

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 (130) hide show
  1. package/.env.example +36 -0
  2. package/.github/workflows/pr-checks.yml +41 -0
  3. package/.github/workflows/release.yml +194 -0
  4. package/.prettierignore +26 -0
  5. package/.prettierrc +14 -0
  6. package/CLAUDE.md +140 -0
  7. package/Dockerfile +50 -0
  8. package/Dockerfile.dev +31 -0
  9. package/LICENSE +21 -0
  10. package/README.md +395 -0
  11. package/build/api/client.d.ts +49 -0
  12. package/build/api/client.d.ts.map +1 -0
  13. package/build/api/client.js +204 -0
  14. package/build/api/client.js.map +1 -0
  15. package/build/api/types.d.ts +113 -0
  16. package/build/api/types.d.ts.map +1 -0
  17. package/build/api/types.js +2 -0
  18. package/build/api/types.js.map +1 -0
  19. package/build/config/settings.d.ts +123 -0
  20. package/build/config/settings.d.ts.map +1 -0
  21. package/build/config/settings.js +97 -0
  22. package/build/config/settings.js.map +1 -0
  23. package/build/constants.d.ts +16 -0
  24. package/build/constants.d.ts.map +1 -0
  25. package/build/constants.js +24 -0
  26. package/build/constants.js.map +1 -0
  27. package/build/generated/version.d.ts +3 -0
  28. package/build/generated/version.d.ts.map +1 -0
  29. package/build/generated/version.js +5 -0
  30. package/build/generated/version.js.map +1 -0
  31. package/build/index.d.ts +3 -0
  32. package/build/index.d.ts.map +1 -0
  33. package/build/index.js +81 -0
  34. package/build/index.js.map +1 -0
  35. package/build/mcp-server.d.ts +6 -0
  36. package/build/mcp-server.d.ts.map +1 -0
  37. package/build/mcp-server.js +263 -0
  38. package/build/mcp-server.js.map +1 -0
  39. package/build/tools/action-words.d.ts +18 -0
  40. package/build/tools/action-words.d.ts.map +1 -0
  41. package/build/tools/action-words.js +191 -0
  42. package/build/tools/action-words.js.map +1 -0
  43. package/build/tools/projects.d.ts +19 -0
  44. package/build/tools/projects.d.ts.map +1 -0
  45. package/build/tools/projects.js +123 -0
  46. package/build/tools/projects.js.map +1 -0
  47. package/build/tools/scenarios.d.ts +18 -0
  48. package/build/tools/scenarios.d.ts.map +1 -0
  49. package/build/tools/scenarios.js +194 -0
  50. package/build/tools/scenarios.js.map +1 -0
  51. package/build/tools/test-runs.d.ts +21 -0
  52. package/build/tools/test-runs.d.ts.map +1 -0
  53. package/build/tools/test-runs.js +324 -0
  54. package/build/tools/test-runs.js.map +1 -0
  55. package/build/transports/http.d.ts +38 -0
  56. package/build/transports/http.d.ts.map +1 -0
  57. package/build/transports/http.js +381 -0
  58. package/build/transports/http.js.map +1 -0
  59. package/build/transports/index.d.ts +22 -0
  60. package/build/transports/index.d.ts.map +1 -0
  61. package/build/transports/index.js +10 -0
  62. package/build/transports/index.js.map +1 -0
  63. package/build/transports/stdio.d.ts +13 -0
  64. package/build/transports/stdio.d.ts.map +1 -0
  65. package/build/transports/stdio.js +24 -0
  66. package/build/transports/stdio.js.map +1 -0
  67. package/build/utils/errors.d.ts +10 -0
  68. package/build/utils/errors.d.ts.map +1 -0
  69. package/build/utils/errors.js +35 -0
  70. package/build/utils/errors.js.map +1 -0
  71. package/build/utils/logger-constants.d.ts +15 -0
  72. package/build/utils/logger-constants.d.ts.map +1 -0
  73. package/build/utils/logger-constants.js +16 -0
  74. package/build/utils/logger-constants.js.map +1 -0
  75. package/build/utils/logger.d.ts +55 -0
  76. package/build/utils/logger.d.ts.map +1 -0
  77. package/build/utils/logger.js +113 -0
  78. package/build/utils/logger.js.map +1 -0
  79. package/build/utils/validation.d.ts +89 -0
  80. package/build/utils/validation.d.ts.map +1 -0
  81. package/build/utils/validation.js +78 -0
  82. package/build/utils/validation.js.map +1 -0
  83. package/docker-compose.yml +20 -0
  84. package/eslint.config.js +97 -0
  85. package/package.json +92 -0
  86. package/scripts/generate-version.js +31 -0
  87. package/src/api/client.ts +286 -0
  88. package/src/api/types.ts +137 -0
  89. package/src/config/settings.ts +113 -0
  90. package/src/constants.ts +29 -0
  91. package/src/index.ts +99 -0
  92. package/src/mcp-server.ts +342 -0
  93. package/src/tools/action-words.ts +240 -0
  94. package/src/tools/projects.ts +144 -0
  95. package/src/tools/scenarios.ts +231 -0
  96. package/src/tools/test-runs.ts +400 -0
  97. package/src/transports/http.ts +467 -0
  98. package/src/transports/index.ts +26 -0
  99. package/src/transports/stdio.ts +28 -0
  100. package/src/utils/errors.ts +45 -0
  101. package/src/utils/logger-constants.ts +18 -0
  102. package/src/utils/logger.ts +150 -0
  103. package/src/utils/validation.ts +94 -0
  104. package/test/api/client-with-msw.test.ts +122 -0
  105. package/test/api/client.test.ts +326 -0
  106. package/test/api/types.test.ts +88 -0
  107. package/test/config/settings.test.ts +204 -0
  108. package/test/mocks/data/action-words.ts +40 -0
  109. package/test/mocks/data/index.ts +13 -0
  110. package/test/mocks/data/projects.ts +38 -0
  111. package/test/mocks/data/scenarios.ts +53 -0
  112. package/test/mocks/data/test-runs.ts +101 -0
  113. package/test/mocks/handlers/action-words.ts +52 -0
  114. package/test/mocks/handlers/index.ts +10 -0
  115. package/test/mocks/handlers/projects.ts +45 -0
  116. package/test/mocks/handlers/scenarios.ts +72 -0
  117. package/test/mocks/handlers/test-runs.ts +106 -0
  118. package/test/mocks/server.ts +26 -0
  119. package/test/setup/vitest.setup.ts +18 -0
  120. package/test/tools/coverage-boost.test.ts +252 -0
  121. package/test/tools/projects.test.ts +290 -0
  122. package/test/tools/tools-basic.test.ts +146 -0
  123. package/test/transports/http-basic.test.ts +87 -0
  124. package/test/transports/http-simple.test.ts +33 -0
  125. package/test/transports/stdio.test.ts +73 -0
  126. package/test/utils/errors.test.ts +117 -0
  127. package/test/utils/validation.test.ts +261 -0
  128. package/tsconfig.build.json +8 -0
  129. package/tsconfig.json +27 -0
  130. package/vitest.config.ts +43 -0
@@ -0,0 +1,342 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { z } from 'zod'
3
+
4
+ import { CucumberStudioApiClient } from './api/client.js'
5
+ import { configManager } from './config/settings.js'
6
+ import { SERVER_NAME, SERVER_VERSION, LOG_PREFIX } from './constants.js'
7
+ import { ActionWordTools } from './tools/action-words.js'
8
+ import { ProjectTools } from './tools/projects.js'
9
+ import { ScenarioTools } from './tools/scenarios.js'
10
+ import { TestRunTools } from './tools/test-runs.js'
11
+ import { StderrLogger, getLogLevel } from './utils/logger.js'
12
+ import { validateEnvironment } from './utils/validation.js'
13
+
14
+ /**
15
+ * Create and configure a new CucumberStudio MCP Server
16
+ */
17
+ export function createCucumberStudioMcpServer(): McpServer {
18
+ const server = new McpServer({
19
+ name: SERVER_NAME,
20
+ version: SERVER_VERSION,
21
+ })
22
+
23
+ // Initialize server tools asynchronously
24
+ initializeServerTools(server).catch((error) => {
25
+ console.error('❌ Failed to initialize server tools:', error)
26
+ })
27
+
28
+ return server
29
+ }
30
+
31
+ /**
32
+ * Initialize all tools and register them with the server
33
+ */
34
+ async function initializeServerTools(server: McpServer): Promise<void> {
35
+ // Validate environment variables
36
+ validateEnvironment()
37
+
38
+ // Load configuration
39
+ const config = configManager.loadFromEnvironment()
40
+
41
+ // Initialize API client
42
+ const apiLogger = new StderrLogger({ level: getLogLevel(), prefix: LOG_PREFIX })
43
+ const apiClient = new CucumberStudioApiClient(config, apiLogger)
44
+
45
+ // Initialize tool classes
46
+ const projectTools = new ProjectTools(apiClient)
47
+ const scenarioTools = new ScenarioTools(apiClient)
48
+ const actionWordTools = new ActionWordTools(apiClient)
49
+ const testRunTools = new TestRunTools(apiClient)
50
+
51
+ // Register all tools
52
+ registerProjectTools(server, projectTools)
53
+ registerScenarioTools(server, scenarioTools)
54
+ registerActionWordTools(server, actionWordTools)
55
+ registerTestRunTools(server, testRunTools)
56
+
57
+ console.error('✅ CucumberStudio MCP Server tools initialized')
58
+ }
59
+
60
+ /**
61
+ * Register project-related tools
62
+ */
63
+ function registerProjectTools(server: McpServer, projectTools: ProjectTools): void {
64
+ server.registerTool(
65
+ 'cucumberstudio_list_projects',
66
+ {
67
+ description: 'List all projects accessible to the authenticated user',
68
+ inputSchema: {
69
+ pagination: z
70
+ .object({
71
+ page: z.number().min(1).optional(),
72
+ pageSize: z.number().min(1).max(100).optional(),
73
+ })
74
+ .optional(),
75
+ filter: z
76
+ .object({
77
+ name: z.string().optional(),
78
+ })
79
+ .optional(),
80
+ },
81
+ },
82
+ async (args) => {
83
+ return await projectTools.listProjects(args || {})
84
+ },
85
+ )
86
+
87
+ server.registerTool(
88
+ 'cucumberstudio_get_project',
89
+ {
90
+ description: 'Get detailed information about a specific project',
91
+ inputSchema: {
92
+ projectId: z.string(),
93
+ },
94
+ },
95
+ async (args) => {
96
+ return await projectTools.getProject(args)
97
+ },
98
+ )
99
+ }
100
+
101
+ /**
102
+ * Register scenario-related tools
103
+ */
104
+ function registerScenarioTools(server: McpServer, scenarioTools: ScenarioTools): void {
105
+ server.registerTool(
106
+ 'cucumberstudio_list_scenarios',
107
+ {
108
+ description: 'List all scenarios in a project',
109
+ inputSchema: {
110
+ projectId: z.string(),
111
+ pagination: z
112
+ .object({
113
+ page: z.number().min(1).optional(),
114
+ pageSize: z.number().min(1).max(100).optional(),
115
+ })
116
+ .optional(),
117
+ filter: z
118
+ .object({
119
+ name: z.string().optional(),
120
+ tags: z.string().optional(),
121
+ })
122
+ .optional(),
123
+ },
124
+ },
125
+ async (args) => {
126
+ return await scenarioTools.listScenarios(args)
127
+ },
128
+ )
129
+
130
+ server.registerTool(
131
+ 'cucumberstudio_get_scenario',
132
+ {
133
+ description: 'Get detailed information about a specific scenario',
134
+ inputSchema: {
135
+ projectId: z.string(),
136
+ scenarioId: z.string(),
137
+ },
138
+ },
139
+ async (args) => {
140
+ return await scenarioTools.getScenario(args)
141
+ },
142
+ )
143
+
144
+ server.registerTool(
145
+ 'cucumberstudio_find_scenarios_by_tags',
146
+ {
147
+ description: 'Find scenarios by tags in a project',
148
+ inputSchema: {
149
+ projectId: z.string(),
150
+ tags: z.string().min(1),
151
+ pagination: z
152
+ .object({
153
+ page: z.number().min(1).optional(),
154
+ pageSize: z.number().min(1).max(100).optional(),
155
+ })
156
+ .optional(),
157
+ },
158
+ },
159
+ async (args) => {
160
+ return await scenarioTools.findScenariosByTags(args)
161
+ },
162
+ )
163
+ }
164
+
165
+ /**
166
+ * Register action word-related tools
167
+ */
168
+ function registerActionWordTools(server: McpServer, actionWordTools: ActionWordTools): void {
169
+ server.registerTool(
170
+ 'cucumberstudio_list_action_words',
171
+ {
172
+ description: 'List all action words (reusable test steps) in a project',
173
+ inputSchema: {
174
+ projectId: z.string(),
175
+ pagination: z
176
+ .object({
177
+ page: z.number().min(1).optional(),
178
+ pageSize: z.number().min(1).max(100).optional(),
179
+ })
180
+ .optional(),
181
+ filter: z
182
+ .object({
183
+ name: z.string().optional(),
184
+ tags: z.string().optional(),
185
+ })
186
+ .optional(),
187
+ },
188
+ },
189
+ async (args) => {
190
+ return await actionWordTools.listActionWords(args)
191
+ },
192
+ )
193
+
194
+ server.registerTool(
195
+ 'cucumberstudio_get_action_word',
196
+ {
197
+ description: 'Get detailed information about a specific action word',
198
+ inputSchema: {
199
+ projectId: z.string(),
200
+ actionWordId: z.string(),
201
+ },
202
+ },
203
+ async (args) => {
204
+ return await actionWordTools.getActionWord(args)
205
+ },
206
+ )
207
+
208
+ server.registerTool(
209
+ 'cucumberstudio_find_action_words_by_tags',
210
+ {
211
+ description: 'Find action words by tags in a project',
212
+ inputSchema: {
213
+ projectId: z.string(),
214
+ tags: z.string().min(1),
215
+ pagination: z
216
+ .object({
217
+ page: z.number().min(1).optional(),
218
+ pageSize: z.number().min(1).max(100).optional(),
219
+ })
220
+ .optional(),
221
+ },
222
+ },
223
+ async (args) => {
224
+ return await actionWordTools.findActionWordsByTags(args)
225
+ },
226
+ )
227
+ }
228
+
229
+ /**
230
+ * Register test run-related tools
231
+ */
232
+ function registerTestRunTools(server: McpServer, testRunTools: TestRunTools): void {
233
+ server.registerTool(
234
+ 'cucumberstudio_list_test_runs',
235
+ {
236
+ description: 'List all test runs in a project',
237
+ inputSchema: {
238
+ projectId: z.string(),
239
+ pagination: z
240
+ .object({
241
+ page: z.number().min(1).optional(),
242
+ pageSize: z.number().min(1).max(100).optional(),
243
+ })
244
+ .optional(),
245
+ filter: z
246
+ .object({
247
+ name: z.string().optional(),
248
+ })
249
+ .optional(),
250
+ },
251
+ },
252
+ async (args) => {
253
+ return await testRunTools.listTestRuns(args)
254
+ },
255
+ )
256
+
257
+ server.registerTool(
258
+ 'cucumberstudio_get_test_run',
259
+ {
260
+ description: 'Get detailed information about a specific test run',
261
+ inputSchema: {
262
+ projectId: z.string(),
263
+ testRunId: z.string(),
264
+ },
265
+ },
266
+ async (args) => {
267
+ return await testRunTools.getTestRun(args)
268
+ },
269
+ )
270
+
271
+ server.registerTool(
272
+ 'cucumberstudio_get_test_executions',
273
+ {
274
+ description: 'Get test executions (individual test results) for a test run',
275
+ inputSchema: {
276
+ projectId: z.string(),
277
+ testRunId: z.string(),
278
+ pagination: z
279
+ .object({
280
+ page: z.number().min(1).optional(),
281
+ pageSize: z.number().min(1).max(100).optional(),
282
+ })
283
+ .optional(),
284
+ },
285
+ },
286
+ async (args) => {
287
+ return await testRunTools.getTestExecutions(args)
288
+ },
289
+ )
290
+
291
+ server.registerTool(
292
+ 'cucumberstudio_list_builds',
293
+ {
294
+ description: 'List all builds in a project',
295
+ inputSchema: {
296
+ projectId: z.string(),
297
+ pagination: z
298
+ .object({
299
+ page: z.number().min(1).optional(),
300
+ pageSize: z.number().min(1).max(100).optional(),
301
+ })
302
+ .optional(),
303
+ },
304
+ },
305
+ async (args) => {
306
+ return await testRunTools.listBuilds(args)
307
+ },
308
+ )
309
+
310
+ server.registerTool(
311
+ 'cucumberstudio_get_build',
312
+ {
313
+ description: 'Get detailed information about a specific build',
314
+ inputSchema: {
315
+ projectId: z.string(),
316
+ buildId: z.string(),
317
+ },
318
+ },
319
+ async (args) => {
320
+ return await testRunTools.getBuild(args)
321
+ },
322
+ )
323
+
324
+ server.registerTool(
325
+ 'cucumberstudio_list_execution_environments',
326
+ {
327
+ description: 'List all execution environments in a project',
328
+ inputSchema: {
329
+ projectId: z.string(),
330
+ pagination: z
331
+ .object({
332
+ page: z.number().min(1).optional(),
333
+ pageSize: z.number().min(1).max(100).optional(),
334
+ })
335
+ .optional(),
336
+ },
337
+ },
338
+ async (args) => {
339
+ return await testRunTools.listExecutionEnvironments(args)
340
+ },
341
+ )
342
+ }
@@ -0,0 +1,240 @@
1
+ import { Tool, CallToolRequest, CallToolResult, TextContent } from '@modelcontextprotocol/sdk/types.js'
2
+ import { z } from 'zod'
3
+
4
+ import { CucumberStudioApiClient } from '../api/client.js'
5
+ import { ActionWord } from '../api/types.js'
6
+ import { safeExecute } from '../utils/errors.js'
7
+ import {
8
+ validateInput,
9
+ ProjectIdSchema,
10
+ ActionWordIdSchema,
11
+ ListParamsSchema,
12
+ convertToApiParams,
13
+ } from '../utils/validation.js'
14
+
15
+ const FindActionWordsByTagsSchema = z.object({
16
+ projectId: ProjectIdSchema,
17
+ tags: z.string().min(1, 'Tags parameter is required'),
18
+ pagination: z
19
+ .object({
20
+ page: z.number().int().min(1).optional(),
21
+ pageSize: z.number().int().min(1).max(100).optional(),
22
+ })
23
+ .optional(),
24
+ })
25
+
26
+ export class ActionWordTools {
27
+ constructor(private apiClient: CucumberStudioApiClient) {}
28
+
29
+ /**
30
+ * Get all available MCP tools for action words
31
+ */
32
+ getTools(): Tool[] {
33
+ return [
34
+ {
35
+ name: 'cucumberstudio_list_action_words',
36
+ description: 'List all action words (reusable test steps) in a project',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: {
40
+ projectId: { type: 'string', description: 'The ID of the project' },
41
+ pagination: {
42
+ type: 'object',
43
+ properties: {
44
+ page: { type: 'number', minimum: 1, description: 'Page number' },
45
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
46
+ },
47
+ additionalProperties: false,
48
+ },
49
+ filter: {
50
+ type: 'object',
51
+ properties: {
52
+ name: { type: 'string', description: 'Filter action words by name' },
53
+ tags: { type: 'string', description: 'Filter action words by tags' },
54
+ },
55
+ additionalProperties: false,
56
+ },
57
+ },
58
+ required: ['projectId'],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ {
63
+ name: 'cucumberstudio_get_action_word',
64
+ description: 'Get detailed information about a specific action word',
65
+ inputSchema: {
66
+ type: 'object',
67
+ properties: {
68
+ projectId: { type: 'string', description: 'The ID of the project' },
69
+ actionWordId: { type: 'string', description: 'The ID of the action word to retrieve' },
70
+ },
71
+ required: ['projectId', 'actionWordId'],
72
+ additionalProperties: false,
73
+ },
74
+ },
75
+ {
76
+ name: 'cucumberstudio_find_action_words_by_tags',
77
+ description: 'Find action words by tags in a project',
78
+ inputSchema: {
79
+ type: 'object',
80
+ properties: {
81
+ projectId: { type: 'string', description: 'The ID of the project' },
82
+ tags: { type: 'string', description: 'Tags to search for (comma-separated)' },
83
+ pagination: {
84
+ type: 'object',
85
+ properties: {
86
+ page: { type: 'number', minimum: 1, description: 'Page number' },
87
+ pageSize: { type: 'number', minimum: 1, maximum: 100, description: 'Number of items per page' },
88
+ },
89
+ additionalProperties: false,
90
+ },
91
+ },
92
+ required: ['projectId', 'tags'],
93
+ additionalProperties: false,
94
+ },
95
+ },
96
+ ]
97
+ }
98
+
99
+ /**
100
+ * Handle tool calls for action word-related operations
101
+ */
102
+ async handleToolCall(request: CallToolRequest): Promise<CallToolResult> {
103
+ switch (request.params.name) {
104
+ case 'cucumberstudio_list_action_words':
105
+ return this.listActionWords(request.params.arguments)
106
+
107
+ case 'cucumberstudio_get_action_word':
108
+ return this.getActionWord(request.params.arguments)
109
+
110
+ case 'cucumberstudio_find_action_words_by_tags':
111
+ return this.findActionWordsByTags(request.params.arguments)
112
+
113
+ default:
114
+ throw new Error(`Unknown tool: ${request.params.name}`)
115
+ }
116
+ }
117
+
118
+ async listActionWords(args: unknown): Promise<CallToolResult> {
119
+ return safeExecute(async () => {
120
+ const projectId = validateInput(
121
+ ProjectIdSchema,
122
+ (args as Record<string, unknown>)?.projectId,
123
+ 'list_action_words',
124
+ )
125
+ const listParams = validateInput(ListParamsSchema, args, 'list_action_words')
126
+ const apiParams = convertToApiParams(listParams)
127
+
128
+ const response = await this.apiClient.getActionWords(projectId, apiParams)
129
+
130
+ const actionWords = Array.isArray(response.data) ? response.data : [response.data]
131
+ const actionWordList = actionWords.map((actionWord: ActionWord) => ({
132
+ id: actionWord.id,
133
+ name: actionWord.attributes.name,
134
+ description: actionWord.attributes.description || '',
135
+ definition: actionWord.attributes.definition || '',
136
+ created_at: actionWord.attributes.created_at,
137
+ updated_at: actionWord.attributes.updated_at,
138
+ }))
139
+
140
+ return {
141
+ content: [
142
+ {
143
+ type: 'text',
144
+ text: JSON.stringify(
145
+ {
146
+ action_words: actionWordList,
147
+ meta: response.meta || {},
148
+ total_count: actionWordList.length,
149
+ },
150
+ null,
151
+ 2,
152
+ ),
153
+ } as TextContent,
154
+ ],
155
+ }
156
+ }, 'listing action words')
157
+ }
158
+
159
+ async getActionWord(args: unknown): Promise<CallToolResult> {
160
+ return safeExecute(async () => {
161
+ const projectId = validateInput(ProjectIdSchema, (args as Record<string, unknown>)?.projectId, 'get_action_word')
162
+ const actionWordId = validateInput(
163
+ ActionWordIdSchema,
164
+ (args as Record<string, unknown>)?.actionWordId,
165
+ 'get_action_word',
166
+ )
167
+
168
+ const response = await this.apiClient.getActionWord(projectId, actionWordId)
169
+
170
+ const actionWord = response.data
171
+ const actionWordDetails = {
172
+ id: actionWord.id,
173
+ type: actionWord.type,
174
+ name: actionWord.attributes.name,
175
+ description: actionWord.attributes.description || '',
176
+ definition: actionWord.attributes.definition || '',
177
+ created_at: actionWord.attributes.created_at,
178
+ updated_at: actionWord.attributes.updated_at,
179
+ relationships: actionWord.relationships || {},
180
+ }
181
+
182
+ return {
183
+ content: [
184
+ {
185
+ type: 'text',
186
+ text: JSON.stringify(
187
+ {
188
+ action_word: actionWordDetails,
189
+ included: response.included || [],
190
+ },
191
+ null,
192
+ 2,
193
+ ),
194
+ } as TextContent,
195
+ ],
196
+ }
197
+ }, 'getting action word details')
198
+ }
199
+
200
+ async findActionWordsByTags(args: unknown): Promise<CallToolResult> {
201
+ return safeExecute(async () => {
202
+ const { projectId, tags, pagination } = validateInput(
203
+ FindActionWordsByTagsSchema,
204
+ args,
205
+ 'find_action_words_by_tags',
206
+ )
207
+ const apiParams = convertToApiParams({ pagination })
208
+
209
+ const response = await this.apiClient.findActionWordsByTag(projectId, tags, apiParams)
210
+
211
+ const actionWords = Array.isArray(response.data) ? response.data : [response.data]
212
+ const actionWordList = actionWords.map((actionWord: ActionWord) => ({
213
+ id: actionWord.id,
214
+ name: actionWord.attributes.name,
215
+ description: actionWord.attributes.description || '',
216
+ definition: actionWord.attributes.definition || '',
217
+ created_at: actionWord.attributes.created_at,
218
+ updated_at: actionWord.attributes.updated_at,
219
+ }))
220
+
221
+ return {
222
+ content: [
223
+ {
224
+ type: 'text',
225
+ text: JSON.stringify(
226
+ {
227
+ action_words: actionWordList,
228
+ search_tags: tags,
229
+ meta: response.meta || {},
230
+ total_count: actionWordList.length,
231
+ },
232
+ null,
233
+ 2,
234
+ ),
235
+ } as TextContent,
236
+ ],
237
+ }
238
+ }, 'finding action words by tags')
239
+ }
240
+ }