@cplace/test-mcp-server 0.1.6 → 0.1.8

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.
package/dist/index.js CHANGED
@@ -7,21 +7,45 @@ import { CplaceApiClient } from './api.js';
7
7
  import SearchFilterSchema from "./searchSchema.js";
8
8
  import { convertSearchFilterToCplaceFormat } from "./searchConversion.js";
9
9
  import { filterSearchResult } from "./utils.js";
10
+ import { debugLogWithTag } from "./logger.js";
11
+ import { DEBUG_LOGGING } from "./logger.js";
12
+ import { convertPagesToCsv } from "./csvUtils.js";
10
13
  dotenv.config();
11
14
  const config = {
12
15
  url: process.env.CPLACE_URL || '',
13
- apiToken: process.env.API_TOKEN || ''
16
+ apiToken: process.env.API_TOKEN || '',
17
+ registerSystemInfoTool: process.env.REGISTER_SYSTEM_INFO_TOOL === 'true' || false,
18
+ registerLayoutTools: process.env.REGISTER_LAYOUT_TOOLS === 'true' || false,
14
19
  };
15
20
  const client = new CplaceApiClient(config);
16
21
  const server = new McpServer({
17
22
  name: "cplace-mcp-server",
18
23
  version: "1.0.0"
19
24
  });
25
+ if (config.registerSystemInfoTool) {
26
+ server.registerTool("cplace_system_info", {
27
+ description: "Get system information about the MCP server - including current working directory, CPLACE_URL setting, and debug state",
28
+ inputSchema: {},
29
+ annotations: { title: "System Information" }
30
+ }, async () => {
31
+ const systemInfo = {
32
+ currentWorkingDirectory: process.cwd(),
33
+ cplaceUrl: config.url || 'Not set',
34
+ debugLogging: DEBUG_LOGGING,
35
+ nodeVersion: process.version,
36
+ platform: process.platform
37
+ };
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(systemInfo, null, 2) }]
40
+ };
41
+ });
42
+ }
20
43
  server.registerTool("cplace_list_workspaces", {
21
44
  description: "Get a list of all workspaces with essential properties (id, name, displayName, totalPages, isFavorite, installed apps)",
22
45
  inputSchema: {},
23
46
  annotations: { title: "List Workspaces" }
24
47
  }, async () => {
48
+ debugLogWithTag('WORKSPACES', 'Starting workspace list request');
25
49
  try {
26
50
  const workspaces = await client.makeApiRequest('json/workspaces');
27
51
  const filteredWorkspaces = workspaces.map((workspace) => ({
@@ -32,6 +56,7 @@ server.registerTool("cplace_list_workspaces", {
32
56
  isFavorite: workspace.isFavorite,
33
57
  installedApps: workspace.apps?.installed?.map(app => app.name) || []
34
58
  }));
59
+ debugLogWithTag('WORKSPACES', `Retrieved ${filteredWorkspaces.length} workspaces`);
35
60
  return {
36
61
  content: [{ type: "text", text: JSON.stringify(filteredWorkspaces, null, 2) }]
37
62
  };
@@ -99,7 +124,7 @@ server.registerTool("cplace_get_page_by_id", {
99
124
  annotations: { title: "Get Page by ID" }
100
125
  }, async ({ id }) => {
101
126
  try {
102
- const result = await client.makeApiRequest('json/page', 'GET', { id });
127
+ const result = await client.makeApiRequest('json/page', 'GET', { pageUID: id });
103
128
  return {
104
129
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
105
130
  };
@@ -151,25 +176,78 @@ server.registerTool("cplace_search_person", {
151
176
  server.registerTool("cplace_search_pages", {
152
177
  description: "Search pages of a type in cplace.",
153
178
  inputSchema: {
154
- spaceId: z.string().optional().describe("The ID of the workspace to search in. If not provided, searches across all accessible workspaces."),
155
- internalTypeName: z.string().describe("The internal name of the type to filter by"),
156
- search_filter: SearchFilterSchema.describe('Search Filters'),
179
+ workspaceId: z.string().optional().describe("The ID of the workspace to search in. If not provided, searches across all accessible workspaces."),
180
+ internalTypeName: z.string().describe(`The internal name of the type to filter by. Examples:
181
+ - "cf.cplace.solution.safe.feature" for Feature pages
182
+ - "cf.cplace.solution.safe.epic" for Epic pages
183
+ - "cf.cplace.solution.safe.story" for Story pages
184
+ - "cf.projectNavigator.project" for Project pages
185
+ - "cf.cplace.solution.safe.portfolio" for Portfolio pages
186
+ Use cplace_get_types_in_workspace to discover available types in a workspace.`),
187
+ search_filter: SearchFilterSchema.describe(`Search Filters using cplace filter format. Examples:
188
+
189
+ Basic string filter:
190
+ [{"attribute": "cf.cplace.workflow", "string": {"equals": "approved"}}]
191
+
192
+ Multiple conditions:
193
+ [
194
+ {"name": {"equals": "Project Alpha"}},
195
+ {"attribute": "cf.cplace.location", "reference": {"equals": "page/abc123"}}
196
+ ]
197
+
198
+ Date range filter:
199
+ [
200
+ {"attribute": "cf.cplace.projectStart", "date": {"on_or_after": "2023-01-01"}},
201
+ {"attribute": "cf.cplace.projectFinish", "date": {"on_or_before": "2024-12-31"}}
202
+ ]
203
+
204
+ Number comparison:
205
+ [{"attribute": "cf.cplace.estimatedCost", "number": {"greater_than": 1000}}]
206
+
207
+ Logical OR operator:
208
+ [{
209
+ "or": [
210
+ {"attribute": "cf.cplace.managementPriority", "string": {"equals": "Focus"}},
211
+ {"attribute": "cf.cplace.managementPriority", "string": {"equals": "Accelerate"}}
212
+ ]
213
+ }]
214
+
215
+ Complex AND/OR combination:
216
+ [{
217
+ "and": [
218
+ {"attribute": "cf.cplace.workflow", "string": {"equals": "inProgress"}},
219
+ {
220
+ "or": [
221
+ {"attribute": "cf.cplace.priority", "string": {"equals": "High"}},
222
+ {"attribute": "cf.cplace.priority", "string": {"equals": "Critical"}}
223
+ ]
224
+ }
225
+ ]
226
+ }]
227
+
228
+ Boolean filter:
229
+ [{"attribute": "cf.cplace.isActive", "boolean": {"equals": true}}]
230
+
231
+ Empty/not empty checks:
232
+ [{"attribute": "cf.cplace.description", "string": {"is_not_empty": true}}]`),
157
233
  limit: z.number().min(1).max(1000).default(50).describe("Maximum number of results to return (1-1000)"),
158
234
  offset: z.number().min(0).max(10000).default(0).describe("Number of results to skip for pagination, cannot be larger than 10000"),
235
+ responseFormat: z.enum(["minimal", "count"]).optional().describe(`Response format options:
236
+ - Default (not specified): Complete page data, respects 'attributes' parameter
237
+ - "minimal": Only core fields (id, name, url, type, workspace)
238
+ - "count": Only return the total count of matching pages`),
239
+ attributes: z.array(z.string()).optional().describe("Specific attribute names to include in response. If not provided, all attributes are included. Examples: ['cf.cplace.workflow', 'cf.cplace.priority']"),
159
240
  },
160
241
  annotations: { title: "Search Pages" }
161
- }, async ({ spaceId, internalTypeName, search_filter, limit = 50, offset = 0, }) => {
162
- if (!search_filter) {
163
- return {
164
- content: [{ type: "text", text: "search_filter is required" }]
165
- };
166
- }
242
+ }, async ({ workspaceId, internalTypeName, search_filter, limit = 50, offset = 0, responseFormat, attributes, }) => {
243
+ debugLogWithTag('SEARCH', `Starting search with params: workspaceId=${workspaceId || 'none'}, type=${internalTypeName}, filter=${JSON.stringify(search_filter)}, limit=${limit}, offset=${offset}`);
167
244
  try {
168
245
  const cplaceFilter = convertSearchFilterToCplaceFormat(search_filter);
169
- if (spaceId) {
246
+ debugLogWithTag('SEARCH', `Converted filter: ${JSON.stringify(cplaceFilter)}`);
247
+ if (workspaceId) {
170
248
  cplaceFilter.filters.unshift({
171
249
  type: "Workspace",
172
- workspaceIds: [spaceId]
250
+ workspaceIds: [workspaceId]
173
251
  });
174
252
  }
175
253
  if (internalTypeName) {
@@ -178,23 +256,43 @@ server.registerTool("cplace_search_pages", {
178
256
  types: [internalTypeName]
179
257
  });
180
258
  }
259
+ debugLogWithTag('SEARCH', `Final cplace filter: ${JSON.stringify(cplaceFilter)}`);
181
260
  const result = await client.makeApiRequest('json/search', 'GET', {
182
261
  filter: JSON.stringify(cplaceFilter),
183
262
  limit,
184
263
  offset
185
264
  });
265
+ if (responseFormat === "count") {
266
+ const countResult = {
267
+ count: result.pagination?.total || 0
268
+ };
269
+ return {
270
+ content: [{ type: "text", text: JSON.stringify(countResult, null, 2) }]
271
+ };
272
+ }
186
273
  const filteredResults = result.results ? result.results.map((item) => filterSearchResult(item, {
187
- minimal: false,
188
- includeAttributes: true,
274
+ minimal: responseFormat === "minimal",
275
+ includeAttributes: responseFormat !== "minimal",
189
276
  includeMetadata: false,
277
+ attributeFields: attributes,
190
278
  truncateLargeValues: true
191
279
  })) : [];
192
- const filteredResult = { ...result, results: filteredResults };
280
+ const filteredResult = {
281
+ results: filteredResults,
282
+ pagination: {
283
+ total: result.pagination?.total || 0,
284
+ limit: result.pagination?.limit || limit,
285
+ offset: result.pagination?.offset || offset,
286
+ hasMore: result.pagination?.hasMore || false
287
+ },
288
+ humanReadableRepresentation: result.summary
289
+ };
193
290
  return {
194
291
  content: [{ type: "text", text: JSON.stringify(filteredResult, null, 2) }]
195
292
  };
196
293
  }
197
294
  catch (error) {
295
+ debugLogWithTag('SEARCH', `Error during search: ${error instanceof Error ? error.message : String(error)}`);
198
296
  return {
199
297
  content: [{ type: "text", text: `Error converting or executing search: ${error instanceof Error ? error.message : String(error)}` }]
200
298
  };
@@ -203,13 +301,13 @@ server.registerTool("cplace_search_pages", {
203
301
  server.registerTool("cplace_fulltext_search", {
204
302
  description: "Make a fulltext search across all pages in cplace. Use this only if the user is not asking about specific types.",
205
303
  inputSchema: {
206
- spaceId: z.string().optional().describe("The ID of the workspace to search in. If not provided, searches across all accessible workspaces."),
304
+ workspaceId: z.string().optional().describe("The ID of the workspace to search in. If not provided, searches across all accessible workspaces."),
207
305
  fulltext: z.string().describe("The fulltext search query to filter pages by. The results will include pages that conatin this string in their name, attributes, or content."),
208
306
  limit: z.number().min(1).max(1000).default(50).describe("Maximum number of results to return (1-1000)"),
209
307
  offset: z.number().min(0).max(10000).default(0).describe("Number of results to skip for pagination, cannot be larger than 10000"),
210
308
  },
211
309
  annotations: { title: "Search Pages" }
212
- }, async ({ spaceId, fulltext, limit = 50, offset = 0, }) => {
310
+ }, async ({ workspaceId, fulltext, limit = 50, offset = 0, }) => {
213
311
  if (!fulltext) {
214
312
  return {
215
313
  content: [{ type: "text", text: "fulltext is required" }]
@@ -217,7 +315,7 @@ server.registerTool("cplace_fulltext_search", {
217
315
  }
218
316
  try {
219
317
  const result = await client.makeApiRequest('json/fulltext-search', 'GET', {
220
- spaceId,
318
+ spaceId: workspaceId,
221
319
  fulltext: `"${fulltext}"`,
222
320
  limit,
223
321
  offset
@@ -228,7 +326,16 @@ server.registerTool("cplace_fulltext_search", {
228
326
  includeMetadata: false,
229
327
  truncateLargeValues: true
230
328
  })) : [];
231
- const filteredResult = { ...result, results: filteredResults };
329
+ const filteredResult = {
330
+ results: filteredResults,
331
+ pagination: {
332
+ total: result.pagination?.total || 0,
333
+ limit: result.pagination?.limit || limit,
334
+ offset: result.pagination?.offset || offset,
335
+ hasMore: result.pagination?.hasMore || false
336
+ },
337
+ summary: result.summary
338
+ };
232
339
  return {
233
340
  content: [{ type: "text", text: JSON.stringify(filteredResult, null, 2) }]
234
341
  };
@@ -242,19 +349,19 @@ server.registerTool("cplace_fulltext_search", {
242
349
  server.registerTool("cplace_get_all_pages_of_type", {
243
350
  description: "Get all pages of a specific type in a workspace in cplace. ONLY use this if you need to retrieve all pages of a type.",
244
351
  inputSchema: {
245
- spaceId: z.string().describe("The ID of the workspace to get pages from"),
352
+ workspaceId: z.string().describe("The ID of the workspace to get pages from"),
246
353
  typeName: z.string().describe("The internal name of the type to retrieve pages for"),
247
354
  limit: z.number().min(1).max(1000).default(50).describe("Maximum number of results to return (1-1000)"),
248
355
  offset: z.number().min(0).max(10000).default(0).describe("Number of results to skip for pagination, cannot be larger than 10000"),
249
356
  },
250
357
  annotations: { title: "Get All Pages of Type" }
251
- }, async ({ spaceId, typeName, limit = 50, offset = 0, }) => {
358
+ }, async ({ workspaceId, typeName, limit = 50, offset = 0, }) => {
252
359
  try {
253
360
  const cplaceFilter = {
254
361
  filters: [
255
362
  {
256
363
  type: "Workspace",
257
- workspaceIds: [spaceId]
364
+ workspaceIds: [workspaceId]
258
365
  },
259
366
  {
260
367
  type: "Type",
@@ -273,7 +380,16 @@ server.registerTool("cplace_get_all_pages_of_type", {
273
380
  includeMetadata: false,
274
381
  truncateLargeValues: true
275
382
  })) : [];
276
- const filteredResult = { ...result, results: filteredResults };
383
+ const filteredResult = {
384
+ results: filteredResults,
385
+ pagination: {
386
+ total: result.pagination?.total || 0,
387
+ limit: result.pagination?.limit || limit,
388
+ offset: result.pagination?.offset || offset,
389
+ hasMore: result.pagination?.hasMore || false
390
+ },
391
+ summary: result.summary
392
+ };
277
393
  return {
278
394
  content: [{ type: "text", text: JSON.stringify(filteredResult, null, 2) }]
279
395
  };
@@ -284,6 +400,569 @@ server.registerTool("cplace_get_all_pages_of_type", {
284
400
  };
285
401
  }
286
402
  });
403
+ server.registerTool("cplace_search_pages_csv", {
404
+ description: "Same as cplace_search_pages but returns results in CSV format instead of JSON. Only use this tool after first calling cplace_search_pages to establish and validate the search parameters. The CSV result should always be stored as an artifact.",
405
+ inputSchema: {
406
+ workspaceId: z.string().optional().describe("Same as cplace_search_pages"),
407
+ internalTypeName: z.string().describe("Same as cplace_search_pages"),
408
+ search_filter: SearchFilterSchema.describe("Same as cplace_search_pages"),
409
+ limit: z.number().min(1).max(1000).default(50).describe("Same as cplace_search_pages"),
410
+ offset: z.number().min(0).max(10000).default(0).describe("Same as cplace_search_pages"),
411
+ attributes: z.array(z.string()).optional().describe("Same as cplace_search_pages"),
412
+ },
413
+ annotations: { title: "Search Pages (CSV Export)" }
414
+ }, async ({ workspaceId, internalTypeName, search_filter, limit = 50, offset = 0, attributes, }) => {
415
+ debugLogWithTag('CSV_SEARCH', `Starting CSV search with params: workspaceId=${workspaceId || 'none'}, type=${internalTypeName}, filter=${JSON.stringify(search_filter)}, limit=${limit}, offset=${offset}`);
416
+ try {
417
+ const cplaceFilter = convertSearchFilterToCplaceFormat(search_filter);
418
+ debugLogWithTag('CSV_SEARCH', `Converted filter: ${JSON.stringify(cplaceFilter)}`);
419
+ if (workspaceId) {
420
+ cplaceFilter.filters.unshift({
421
+ type: "Workspace",
422
+ workspaceIds: [workspaceId]
423
+ });
424
+ }
425
+ if (internalTypeName) {
426
+ cplaceFilter.filters.unshift({
427
+ type: "Type",
428
+ types: [internalTypeName]
429
+ });
430
+ }
431
+ debugLogWithTag('CSV_SEARCH', `Final cplace filter: ${JSON.stringify(cplaceFilter)}`);
432
+ const result = await client.makeApiRequest('json/search', 'GET', {
433
+ filter: JSON.stringify(cplaceFilter),
434
+ limit,
435
+ offset
436
+ });
437
+ const searchResults = result.results || [];
438
+ debugLogWithTag('CSV_SEARCH', `Retrieved ${searchResults.length} results for CSV conversion`);
439
+ const csvContent = await convertPagesToCsv(searchResults, client, attributes);
440
+ return {
441
+ content: [{ type: "text", text: csvContent }]
442
+ };
443
+ }
444
+ catch (error) {
445
+ debugLogWithTag('CSV_SEARCH', `Error during CSV search: ${error instanceof Error ? error.message : String(error)}`);
446
+ return {
447
+ content: [{ type: "text", text: `Error converting or executing CSV search: ${error instanceof Error ? error.message : String(error)}` }]
448
+ };
449
+ }
450
+ });
451
+ if (config.registerLayoutTools) {
452
+ server.registerTool("cplace_list_widget_definitions", {
453
+ description: "Get a list of all available widget definitions in the system with their basic metadata including names, descriptions, and apps",
454
+ inputSchema: {},
455
+ annotations: { title: "List Widget Definitions" }
456
+ }, async () => {
457
+ debugLogWithTag('WIDGETS', 'Starting widget definitions list request');
458
+ try {
459
+ const result = await client.makeApiRequest('json/widget-definitions');
460
+ debugLogWithTag('WIDGETS', `Retrieved widget definitions`);
461
+ return {
462
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
463
+ };
464
+ }
465
+ catch (error) {
466
+ return {
467
+ content: [{ type: "text", text: `Error retrieving widget definitions: ${error instanceof Error ? error.message : String(error)}` }]
468
+ };
469
+ }
470
+ });
471
+ }
472
+ if (config.registerLayoutTools) {
473
+ server.registerTool("cplace_get_widget_definition", {
474
+ description: "Get detailed information about a specific widget definition including its complete configuration schema with all attributes, constraints, and metadata",
475
+ inputSchema: {
476
+ widgetKind: z.string().describe("The widget kind identifier (e.g., 'cf.cplace.platform.table', 'cf.platform.wiki')")
477
+ },
478
+ annotations: { title: "Get Widget Definition Details" }
479
+ }, async ({ widgetKind }) => {
480
+ debugLogWithTag('WIDGETS', `Starting widget definition request for: ${widgetKind}`);
481
+ try {
482
+ const result = await client.makeApiRequest('json/widget-definition', 'GET', { widgetKind });
483
+ debugLogWithTag('WIDGETS', `Retrieved widget definition for: ${widgetKind}`);
484
+ return {
485
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
486
+ };
487
+ }
488
+ catch (error) {
489
+ return {
490
+ content: [{ type: "text", text: `Error retrieving widget definition for ${widgetKind}: ${error instanceof Error ? error.message : String(error)}` }]
491
+ };
492
+ }
493
+ });
494
+ }
495
+ if (config.registerLayoutTools) {
496
+ server.registerTool("cplace_get_page_layout_overview", {
497
+ description: "Get the high-level layout structure of a page including rows, columns, and widget summaries. Provides an overview of the page layout grid without detailed widget configurations.",
498
+ inputSchema: {
499
+ pageUID: z.string().describe("The unique identifier (UID) of the page to get layout for, e.g. 'page/kkt8ol745jqur4581kelm5ply'")
500
+ },
501
+ annotations: { title: "Get Page Layout Overview" }
502
+ }, async ({ pageUID }) => {
503
+ debugLogWithTag('LAYOUT', `Starting page layout overview request for: ${pageUID}`);
504
+ try {
505
+ const result = await client.makeApiRequest('json/pageLayout', 'GET', { pageUID });
506
+ const overview = {
507
+ rows: result.rows?.map((row, rowIndex) => ({
508
+ rowIndex,
509
+ columns: row.columns?.map((column, colIndex) => ({
510
+ columnIndex: colIndex,
511
+ proportion: column.proportion,
512
+ widgets: column.widgets?.map((widget) => ({
513
+ id: widget.id,
514
+ widgetType: widget.widgetType,
515
+ configurationCount: widget.configuration?.length || 0
516
+ })) || []
517
+ })) || []
518
+ })) || []
519
+ };
520
+ debugLogWithTag('LAYOUT', `Retrieved layout overview for: ${pageUID}`);
521
+ return {
522
+ content: [{ type: "text", text: JSON.stringify(overview, null, 2) }]
523
+ };
524
+ }
525
+ catch (error) {
526
+ return {
527
+ content: [{ type: "text", text: `Error retrieving page layout overview for ${pageUID}: ${error instanceof Error ? error.message : String(error)}` }]
528
+ };
529
+ }
530
+ });
531
+ }
532
+ if (config.registerLayoutTools) {
533
+ server.registerTool("cplace_get_widget_details", {
534
+ description: "Get detailed configuration for a specific widget within a page layout. Use this after cplace_get_page_layout_overview to examine specific widget configurations.",
535
+ inputSchema: {
536
+ pageUID: z.string().describe("The unique identifier (UID) of the page containing the widget"),
537
+ widgetId: z.string().describe("The unique widget identifier from the layout (e.g., 'id_123', 'id_456')")
538
+ },
539
+ annotations: { title: "Get Widget Details" }
540
+ }, async ({ pageUID, widgetId }) => {
541
+ debugLogWithTag('LAYOUT', `Starting widget details request for widget ${widgetId} in page ${pageUID}`);
542
+ try {
543
+ const result = await client.makeApiRequest('json/pageLayout', 'GET', { pageUID });
544
+ let foundWidget = null;
545
+ const findWidget = (rows) => {
546
+ for (const row of rows || []) {
547
+ for (const column of row.columns || []) {
548
+ for (const widget of column.widgets || []) {
549
+ if (widget.id === widgetId) {
550
+ return widget;
551
+ }
552
+ if (widget.widgetsLayout?.rows) {
553
+ const nestedWidget = findWidget(widget.widgetsLayout.rows);
554
+ if (nestedWidget)
555
+ return nestedWidget;
556
+ }
557
+ }
558
+ }
559
+ }
560
+ return null;
561
+ };
562
+ foundWidget = findWidget(result.rows);
563
+ if (!foundWidget) {
564
+ return {
565
+ content: [{ type: "text", text: `Widget with ID ${widgetId} not found in page ${pageUID}` }]
566
+ };
567
+ }
568
+ debugLogWithTag('LAYOUT', `Retrieved widget details for: ${widgetId}`);
569
+ return {
570
+ content: [{ type: "text", text: JSON.stringify(foundWidget, null, 2) }]
571
+ };
572
+ }
573
+ catch (error) {
574
+ return {
575
+ content: [{ type: "text", text: `Error retrieving widget details for ${widgetId} in page ${pageUID}: ${error instanceof Error ? error.message : String(error)}` }]
576
+ };
577
+ }
578
+ });
579
+ }
580
+ server.registerTool("cplace_get_current_user", {
581
+ description: "Get information about the currently logged-in user, including their attributes and metadata",
582
+ inputSchema: {},
583
+ annotations: { title: "Get Current User" }
584
+ }, async () => {
585
+ debugLogWithTag('CURRENT_USER', 'Starting current user request');
586
+ try {
587
+ const result = await client.makeApiRequest('json/current-user');
588
+ debugLogWithTag('CURRENT_USER', `Retrieved current user information`);
589
+ return {
590
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
591
+ };
592
+ }
593
+ catch (error) {
594
+ return {
595
+ content: [{ type: "text", text: `Error retrieving current user information: ${error instanceof Error ? error.message : String(error)}` }]
596
+ };
597
+ }
598
+ });
599
+ server.registerTool("cplace_create_page", {
600
+ description: "Create a new page with specified attributes and content using the POST /page/create endpoint",
601
+ inputSchema: {
602
+ workspaceId: z.string().describe("The ID of the workspace/space to create the page in (e.g., 'workspace/abc123')"),
603
+ typeInternalName: z.string().describe("The internal name of the page type (e.g., 'cf.example.myType')"),
604
+ name: z.string().describe("The name of the new page"),
605
+ parentUID: z.string().optional().describe("The UID of the parent page for hierarchy (optional)"),
606
+ content: z.string().optional().describe("Wiki markup content for the page (optional)"),
607
+ attributes: z.record(z.any()).optional().describe(`Custom attribute values as key-value pairs. Supports:
608
+ - String values: "text"
609
+ - Numeric values: 42 or 3.14
610
+ - Boolean values: true/false
611
+ - Date values: "2024-01-15" or "2024-01-15T10:30:00"
612
+ - Array values: ["value1", "value2"] for multi-valued attributes
613
+ - Reference values: "page/ref123" for page references
614
+ - Localized string values: {"en": "English text", "de": "German text", "fr": "French text"}
615
+ - Multi-valued localized strings: [{"en": "Text 1", "de": "Text 1"}, {"en": "Text 2", "de": "Text 2"}]
616
+
617
+ The system automatically detects attribute types from the page type definition and handles:
618
+ - Type-aware processing based on actual attribute definitions
619
+ - Proper multiplicity validation (single vs multi-valued)
620
+ - Automatic type conversion and validation
621
+ - Enhanced error messages with attribute-specific details`)
622
+ },
623
+ annotations: { title: "Create Page" }
624
+ }, async ({ workspaceId, typeInternalName, name, parentUID, content, attributes }) => {
625
+ debugLogWithTag('CREATE_PAGE', `Starting page creation: ${name} of type ${typeInternalName} in workspace ${workspaceId}`);
626
+ try {
627
+ const requestBody = {
628
+ workspaceId,
629
+ typeInternalName,
630
+ name
631
+ };
632
+ if (parentUID) {
633
+ requestBody.parentUID = parentUID;
634
+ debugLogWithTag('CREATE_PAGE', `Including parentUID: ${parentUID}`);
635
+ }
636
+ if (content) {
637
+ requestBody.content = content;
638
+ debugLogWithTag('CREATE_PAGE', `Including content (${content.length} chars)`);
639
+ }
640
+ if (attributes && Object.keys(attributes).length > 0) {
641
+ requestBody.attributes = attributes;
642
+ debugLogWithTag('CREATE_PAGE', `Including attributes: ${JSON.stringify(Object.keys(attributes))}`);
643
+ }
644
+ debugLogWithTag('CREATE_PAGE', `Request body: ${JSON.stringify(requestBody, null, 2)}`);
645
+ const result = await client.makeApiRequest('json/page/create', 'POST', undefined, requestBody);
646
+ debugLogWithTag('CREATE_PAGE', `Successfully created page: ${name}`);
647
+ return {
648
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
649
+ };
650
+ }
651
+ catch (error) {
652
+ debugLogWithTag('CREATE_PAGE', `Error creating page: ${error instanceof Error ? error.message : String(error)}`);
653
+ return {
654
+ content: [{ type: "text", text: `Error creating page: ${error instanceof Error ? error.message : String(error)}` }]
655
+ };
656
+ }
657
+ });
658
+ server.registerTool("cplace_update_page", {
659
+ description: "Update an existing page's attributes, content, or structure using the PATCH /page/{pageId} endpoint",
660
+ inputSchema: {
661
+ pageUID: z.string().describe("The unique identifier (UID) of the page to update"),
662
+ name: z.string().optional().describe("New name for the page (optional)"),
663
+ content: z.string().optional().describe("New wiki markup content (optional). Use empty string to remove content."),
664
+ parentUID: z.string().optional().describe("New parent page UID (optional - for moving pages). Use empty string to remove parent."),
665
+ attributes: z.record(z.any()).optional().describe(`Custom attributes to update as key-value pairs. Supports:
666
+ - String values: "text"
667
+ - Numeric values: 42 or 3.14
668
+ - Boolean values: true/false
669
+ - Date values: "2024-01-15" or "2024-01-15T10:30:00"
670
+ - Array values: ["value1", "value2"] for multi-valued attributes
671
+ - Reference values: "page/ref123" for page references
672
+ - Localized string values: {"en": "English text", "de": "German text", "fr": "French text"}
673
+ - Multi-valued localized strings: [{"en": "Text 1", "de": "Text 1"}, {"en": "Text 2", "de": "Text 2"}]
674
+ - Null values: null (removes the attribute value)
675
+ - Empty arrays: [] (removes all values for multi-valued attributes)
676
+ - New attributes: will be added to the page
677
+
678
+ The system automatically detects attribute types from the page type definition and handles:
679
+ - Type-aware processing based on actual attribute definitions
680
+ - Proper multiplicity validation (single vs multi-valued)
681
+ - Automatic type conversion and validation
682
+ - Enhanced error messages with attribute-specific details`)
683
+ },
684
+ annotations: { title: "Update Page" }
685
+ }, async ({ pageUID, name, content, parentUID, attributes }) => {
686
+ debugLogWithTag('UPDATE_PAGE', `Starting page update for: ${pageUID}`);
687
+ try {
688
+ const requestBody = {
689
+ pageUID
690
+ };
691
+ if (name !== undefined) {
692
+ requestBody.name = name;
693
+ }
694
+ if (content !== undefined) {
695
+ requestBody.content = content;
696
+ }
697
+ if (parentUID !== undefined) {
698
+ requestBody.parentUID = parentUID;
699
+ }
700
+ if (attributes && Object.keys(attributes).length > 0) {
701
+ requestBody.attributes = attributes;
702
+ }
703
+ if (Object.keys(requestBody).length === 1) {
704
+ return {
705
+ content: [{ type: "text", text: "No updates specified. At least one of name, content, parentId, or attributes must be provided." }]
706
+ };
707
+ }
708
+ const result = await client.makeApiRequest('json/page', 'PATCH', undefined, requestBody);
709
+ debugLogWithTag('UPDATE_PAGE', `Successfully updated page: ${pageUID}`);
710
+ return {
711
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
712
+ };
713
+ }
714
+ catch (error) {
715
+ debugLogWithTag('UPDATE_PAGE', `Error updating page: ${error instanceof Error ? error.message : String(error)}`);
716
+ return {
717
+ content: [{ type: "text", text: `Error updating page: ${error instanceof Error ? error.message : String(error)}` }]
718
+ };
719
+ }
720
+ });
721
+ server.registerTool("cplace_update_page_attributes_only", {
722
+ description: "Lightweight tool for updating only page attributes (no content or structural changes) using the same PATCH /page/{pageId} endpoint",
723
+ inputSchema: {
724
+ pageUID: z.string().describe("The unique identifier (UID) of the page to update"),
725
+ attributes: z.record(z.any()).describe(`Attribute updates as key-value pairs. Supports all the same data types as the full update tool:
726
+ - String values: "text"
727
+ - Numeric values: 42 or 3.14
728
+ - Boolean values: true/false
729
+ - Date values: "2024-01-15" or "2024-01-15T10:30:00"
730
+ - Array values: ["value1", "value2"] for multi-valued attributes
731
+ - Reference values: "page/ref123" for page references
732
+ - Localized string values: {"en": "English text", "de": "German text", "fr": "French text"}
733
+ - Multi-valued localized strings: [{"en": "Text 1", "de": "Text 1"}, {"en": "Text 2", "de": "Text 2"}]
734
+ - Null values: null (removes the attribute value)
735
+ - Empty arrays: [] (removes all values for multi-valued attributes)
736
+ - New attributes: will be added to the page
737
+
738
+ The system automatically detects attribute types from the page type definition and handles:
739
+ - Type-aware processing based on actual attribute definitions
740
+ - Proper multiplicity validation (single vs multi-valued)
741
+ - Automatic type conversion and validation
742
+ - Enhanced error messages with attribute-specific details`)
743
+ },
744
+ annotations: { title: "Update Page Attributes Only" }
745
+ }, async ({ pageUID, attributes }) => {
746
+ debugLogWithTag('UPDATE_PAGE_ATTRS', `Starting attribute-only update for: ${pageUID}`);
747
+ try {
748
+ if (!attributes || Object.keys(attributes).length === 0) {
749
+ return {
750
+ content: [{ type: "text", text: "No attributes specified for update. The attributes parameter must contain at least one attribute." }]
751
+ };
752
+ }
753
+ const requestBody = {
754
+ pageUID,
755
+ attributes: attributes
756
+ };
757
+ const result = await client.makeApiRequest('json/page', 'PATCH', undefined, requestBody);
758
+ debugLogWithTag('UPDATE_PAGE_ATTRS', `Successfully updated attributes for page: ${pageUID}`);
759
+ return {
760
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
761
+ };
762
+ }
763
+ catch (error) {
764
+ debugLogWithTag('UPDATE_PAGE_ATTRS', `Error updating page attributes: ${error instanceof Error ? error.message : String(error)}`);
765
+ return {
766
+ content: [{ type: "text", text: `Error updating page attributes: ${error instanceof Error ? error.message : String(error)}` }]
767
+ };
768
+ }
769
+ });
770
+ if (config.registerLayoutTools) {
771
+ server.registerTool("cplace_add_widget_to_layout", {
772
+ description: "Add a new widget to a page layout at a specific position",
773
+ inputSchema: {
774
+ pageUID: z.string().describe("The unique identifier (UID) of the page to add the widget to"),
775
+ widgetType: z.string().describe("The widget type identifier (e.g., 'cf.cplace.platform.table', 'cf.platform.wiki')"),
776
+ configuration: z.record(z.any()).optional().describe(`Widget-specific configuration object as key-value pairs. IMPORTANT: Check the widget definition schema first to understand required data types:
777
+ - localizedString attributes: use {"en": "English text", "de": "German text"} format
778
+ - boolean attributes: use true/false
779
+ - number attributes: use numeric values
780
+ - string attributes: use simple strings
781
+
782
+ RECOMMENDED WORKFLOW:
783
+ 1. Use cplace_get_widget_definition() first to understand the configuration schema
784
+ 2. Match your configuration values to the constraint types defined in the schema
785
+ 3. Use appropriate data formats for each attribute type
786
+
787
+ Examples: {'title': {'en': 'My Widget', 'de': 'Mein Widget'}, 'showHeader': true, 'height': 600}`),
788
+ position: z.object({
789
+ rowIndex: z.number().min(0).describe("Row index in the layout (0-based)"),
790
+ columnIndex: z.number().min(0).describe("Column index within the row (0-based)"),
791
+ widgetIndex: z.number().min(0).describe("Widget index within the column (0-based)")
792
+ }).describe("Grid position where the widget should be added")
793
+ },
794
+ annotations: { title: "Add Widget to Layout" }
795
+ }, async ({ pageUID, widgetType, configuration, position }) => {
796
+ debugLogWithTag('LAYOUT_MODIFY', `Starting add widget operation: ${widgetType} to page ${pageUID} at position ${JSON.stringify(position)}`);
797
+ try {
798
+ const operation = {
799
+ type: "ADD",
800
+ widgetType,
801
+ position: {
802
+ rowIndex: position.rowIndex,
803
+ columnIndex: position.columnIndex,
804
+ widgetIndex: position.widgetIndex
805
+ }
806
+ };
807
+ if (configuration && Object.keys(configuration).length > 0) {
808
+ operation.configuration = configuration;
809
+ debugLogWithTag('LAYOUT_MODIFY', `Including configuration: ${JSON.stringify(Object.keys(configuration))}`);
810
+ }
811
+ const requestBody = {
812
+ pageUID,
813
+ operation
814
+ };
815
+ debugLogWithTag('LAYOUT_MODIFY', `Request body: ${JSON.stringify(requestBody, null, 2)}`);
816
+ const result = await client.makeApiRequest('json/pageLayout', 'PATCH', undefined, requestBody);
817
+ debugLogWithTag('LAYOUT_MODIFY', `Successfully added widget ${widgetType} to page ${pageUID}`);
818
+ return {
819
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
820
+ };
821
+ }
822
+ catch (error) {
823
+ debugLogWithTag('LAYOUT_MODIFY', `Error adding widget: ${error instanceof Error ? error.message : String(error)}`);
824
+ return {
825
+ content: [{ type: "text", text: `Error adding widget to layout: ${error instanceof Error ? error.message : String(error)}` }]
826
+ };
827
+ }
828
+ });
829
+ }
830
+ if (config.registerLayoutTools) {
831
+ server.registerTool("cplace_remove_widget_from_layout", {
832
+ description: "Remove an existing widget from a page layout",
833
+ inputSchema: {
834
+ pageUID: z.string().describe("The unique identifier (UID) of the page containing the widget"),
835
+ widgetId: z.string().describe("The unique identifier of the widget to remove")
836
+ },
837
+ annotations: { title: "Remove Widget from Layout" }
838
+ }, async ({ pageUID, widgetId }) => {
839
+ debugLogWithTag('LAYOUT_MODIFY', `Starting remove widget operation: ${widgetId} from page ${pageUID}`);
840
+ try {
841
+ const requestBody = {
842
+ pageUID,
843
+ operation: {
844
+ type: "REMOVE",
845
+ widgetId
846
+ }
847
+ };
848
+ debugLogWithTag('LAYOUT_MODIFY', `Request body: ${JSON.stringify(requestBody, null, 2)}`);
849
+ const result = await client.makeApiRequest('json/pageLayout', 'PATCH', undefined, requestBody);
850
+ debugLogWithTag('LAYOUT_MODIFY', `Successfully removed widget ${widgetId} from page ${pageUID}`);
851
+ return {
852
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
853
+ };
854
+ }
855
+ catch (error) {
856
+ debugLogWithTag('LAYOUT_MODIFY', `Error removing widget: ${error instanceof Error ? error.message : String(error)}`);
857
+ return {
858
+ content: [{ type: "text", text: `Error removing widget from layout: ${error instanceof Error ? error.message : String(error)}` }]
859
+ };
860
+ }
861
+ });
862
+ }
863
+ if (config.registerLayoutTools) {
864
+ server.registerTool("cplace_update_widget_in_layout", {
865
+ description: "Modify widget configuration or display properties",
866
+ inputSchema: {
867
+ pageUID: z.string().describe("The unique identifier (UID) of the page containing the widget"),
868
+ widgetId: z.string().describe("The unique identifier of the widget to update"),
869
+ newConfiguration: z.record(z.any()).optional().describe(`Updated widget configuration as key-value pairs (optional). IMPORTANT: Check the widget definition schema first to understand required data types:
870
+ - localizedString attributes: use {"en": "English text", "de": "German text"} format
871
+ - boolean attributes: use true/false
872
+ - number attributes: use numeric values
873
+ - string attributes: use simple strings
874
+
875
+ RECOMMENDED WORKFLOW:
876
+ 1. Use cplace_get_widget_definition() first to understand the configuration schema
877
+ 2. Match your configuration values to the constraint types defined in the schema
878
+ 3. Use appropriate data formats for each attribute type
879
+
880
+ Examples: {'title': {'en': 'My Widget', 'de': 'Mein Widget'}, 'showHeader': true, 'height': 600}`),
881
+ collapsed: z.boolean().optional().describe("Widget collapse state (optional)")
882
+ },
883
+ annotations: { title: "Update Widget in Layout" }
884
+ }, async ({ pageUID, widgetId, newConfiguration, collapsed }) => {
885
+ debugLogWithTag('LAYOUT_MODIFY', `Starting update widget operation: ${widgetId} in page ${pageUID}`);
886
+ try {
887
+ const operation = {
888
+ type: "UPDATE",
889
+ widgetId
890
+ };
891
+ if (newConfiguration && Object.keys(newConfiguration).length > 0) {
892
+ operation.newConfiguration = newConfiguration;
893
+ debugLogWithTag('LAYOUT_MODIFY', `Including new configuration: ${JSON.stringify(Object.keys(newConfiguration))}`);
894
+ }
895
+ if (collapsed !== undefined) {
896
+ operation.collapsed = collapsed;
897
+ debugLogWithTag('LAYOUT_MODIFY', `Setting collapsed state to: ${collapsed}`);
898
+ }
899
+ if (!operation.newConfiguration && operation.collapsed === undefined) {
900
+ return {
901
+ content: [{ type: "text", text: "No updates specified. At least one of newConfiguration or collapsed must be provided." }]
902
+ };
903
+ }
904
+ const requestBody = {
905
+ pageUID,
906
+ operation
907
+ };
908
+ debugLogWithTag('LAYOUT_MODIFY', `Request body: ${JSON.stringify(requestBody, null, 2)}`);
909
+ const result = await client.makeApiRequest('json/pageLayout', 'PATCH', undefined, requestBody);
910
+ debugLogWithTag('LAYOUT_MODIFY', `Successfully updated widget ${widgetId} in page ${pageUID}`);
911
+ return {
912
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
913
+ };
914
+ }
915
+ catch (error) {
916
+ debugLogWithTag('LAYOUT_MODIFY', `Error updating widget: ${error instanceof Error ? error.message : String(error)}`);
917
+ return {
918
+ content: [{ type: "text", text: `Error updating widget in layout: ${error instanceof Error ? error.message : String(error)}` }]
919
+ };
920
+ }
921
+ });
922
+ }
923
+ if (config.registerLayoutTools) {
924
+ server.registerTool("cplace_move_widget_in_layout", {
925
+ description: "Relocate a widget to a different position in the layout",
926
+ inputSchema: {
927
+ pageUID: z.string().describe("The unique identifier (UID) of the page containing the widget"),
928
+ widgetId: z.string().describe("The unique identifier of the widget to move"),
929
+ newPosition: z.object({
930
+ rowIndex: z.number().min(0).describe("Target row index in the layout (0-based)"),
931
+ columnIndex: z.number().min(0).describe("Target column index within the row (0-based)"),
932
+ widgetIndex: z.number().min(0).describe("Target widget index within the column (0-based)")
933
+ }).describe("Target grid position for the widget")
934
+ },
935
+ annotations: { title: "Move Widget in Layout" }
936
+ }, async ({ pageUID, widgetId, newPosition }) => {
937
+ debugLogWithTag('LAYOUT_MODIFY', `Starting move widget operation: ${widgetId} in page ${pageUID} to position ${JSON.stringify(newPosition)}`);
938
+ try {
939
+ const requestBody = {
940
+ pageUID,
941
+ operation: {
942
+ type: "MOVE",
943
+ widgetId,
944
+ newPosition: {
945
+ rowIndex: newPosition.rowIndex,
946
+ columnIndex: newPosition.columnIndex,
947
+ widgetIndex: newPosition.widgetIndex
948
+ }
949
+ }
950
+ };
951
+ debugLogWithTag('LAYOUT_MODIFY', `Request body: ${JSON.stringify(requestBody, null, 2)}`);
952
+ const result = await client.makeApiRequest('json/pageLayout', 'PATCH', undefined, requestBody);
953
+ debugLogWithTag('LAYOUT_MODIFY', `Successfully moved widget ${widgetId} in page ${pageUID}`);
954
+ return {
955
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
956
+ };
957
+ }
958
+ catch (error) {
959
+ debugLogWithTag('LAYOUT_MODIFY', `Error moving widget: ${error instanceof Error ? error.message : String(error)}`);
960
+ return {
961
+ content: [{ type: "text", text: `Error moving widget in layout: ${error instanceof Error ? error.message : String(error)}` }]
962
+ };
963
+ }
964
+ });
965
+ }
287
966
  async function runServer() {
288
967
  const transport = new StdioServerTransport();
289
968
  await server.connect(transport);