@imboard.ai/mcp-server 0.1.0 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imboard.ai/mcp-server",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "imboard MCP server — adapter over the public REST API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,33 +51,34 @@
51
51
  "bugs": {
52
52
  "url": "https://github.com/imboard-ai/imboard-monorepo/issues"
53
53
  },
54
- "scripts": {
55
- "build": "esbuild src/index.ts --bundle --platform=node --outdir=dist --out-extension:.js=.cjs --sourcemap --external:@modelcontextprotocol/sdk --external:zod --banner:js='#!/usr/bin/env node'",
56
- "dev": "tsx watch src/index.ts",
57
- "start": "node dist/index.cjs",
58
- "lint": "eslint src/**/*.ts",
59
- "format:check": "prettier --check \"src/**/*.ts\"",
60
- "format:write": "prettier --write \"src/**/*.ts\"",
61
- "typecheck": "tsc --noEmit",
62
- "test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests --detectOpenHandles --forceExit --verbose --colors",
63
- "prepack": "npm run build"
64
- },
65
54
  "dependencies": {
66
55
  "@modelcontextprotocol/sdk": "^1.29.0",
67
56
  "zod": "^4.1.12"
68
57
  },
69
58
  "devDependencies": {
70
59
  "@jest/globals": "^30.3.0",
71
- "@swc/core": "^1.15.21",
60
+ "@swc/core": "^1.15.24",
72
61
  "@swc/jest": "^0.2.39",
73
62
  "@types/jest": "^30.0.0",
74
- "@types/node": "^25.5.0",
63
+ "@types/node": "^25.6.0",
75
64
  "cross-env": "^10.1.0",
76
- "esbuild": "^0.27.2",
77
- "eslint": "^9.39.2",
65
+ "esbuild": "^0.28.0",
66
+ "eslint": "^10.2.0",
78
67
  "jest": "^30.3.0",
79
- "prettier": "^3.8.1",
68
+ "prettier": "^3.8.2",
80
69
  "tsx": "^4.11.2",
81
- "typescript": "^5.8.2"
70
+ "typescript": "^6.0.2"
71
+ },
72
+ "scripts": {
73
+ "build": "esbuild src/index.ts --bundle --platform=node --outdir=dist --out-extension:.js=.cjs --sourcemap --external:@modelcontextprotocol/sdk --external:zod --banner:js='#!/usr/bin/env node'",
74
+ "build:extension": "node scripts/build-extension.mjs",
75
+ "dev": "tsx watch src/index.ts",
76
+ "start": "node dist/index.cjs",
77
+ "lint": "eslint \"src/**/*.ts\" \"*.config.ts\"",
78
+ "format:check": "prettier --check \"src/**/*.ts\"",
79
+ "format:write": "prettier --write \"src/**/*.ts\"",
80
+ "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
81
+ "hygiene": "bash -c 'pnpm run typecheck && pnpm run lint' --",
82
+ "test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest --passWithNoTests --detectOpenHandles --forceExit --verbose --colors"
82
83
  }
83
- }
84
+ }
@@ -9,12 +9,30 @@ import { McpConfig } from '../config.js';
9
9
  import { logger } from '../utils/logging.js';
10
10
  import { ImboardApiError, ImboardApiTimeoutError, ImboardApiNetworkError } from './errors.js';
11
11
 
12
- const DEFAULT_TIMEOUT_MS = 30_000;
12
+ // These endpoints answer in well under a second; a long timeout only delays
13
+ // failure. 10s + the GET retry below recovers quickly on a fresh connection if a
14
+ // socket stalls, instead of dragging a hung call through the full retry cycle.
15
+ const DEFAULT_TIMEOUT_MS = 10_000;
13
16
  const MAX_RETRIES = 2;
14
17
  const RETRY_BASE_MS = 500;
15
18
 
19
+ /**
20
+ * MCP user-resolution map (#978, propagated by #1247).
21
+ *
22
+ * Backend endpoints that contain `userId` references attach a top-level
23
+ * `users` map when called with `?include=users`, so MCP consumers can render
24
+ * names + positions without a separate user-lookup call.
25
+ */
26
+ export interface UserMapEntry {
27
+ userId: string;
28
+ name: string;
29
+ positions: string[];
30
+ }
31
+ export type UserMap = Record<string, UserMapEntry>;
32
+
16
33
  export interface ApiSuccessResponse<T> {
17
34
  data: T;
35
+ users?: UserMap;
18
36
  requestId: string | null;
19
37
  }
20
38
 
@@ -26,6 +44,7 @@ export interface ApiCollectionMeta {
26
44
  export interface ApiCollectionResponse<T> {
27
45
  data: T[];
28
46
  meta: ApiCollectionMeta;
47
+ users?: UserMap;
29
48
  requestId: string | null;
30
49
  }
31
50
 
@@ -46,10 +65,24 @@ export class ImboardApiClient {
46
65
  return this.request<T>('GET', path, undefined, params);
47
66
  }
48
67
 
49
- async getCollection<T>(path: string, params?: Record<string, string>): Promise<ApiCollectionResponse<T>> {
68
+ async getCollection<T>(
69
+ path: string,
70
+ params?: Record<string, string>
71
+ ): Promise<ApiCollectionResponse<T>> {
50
72
  return this.requestCollection<T>('GET', path, params);
51
73
  }
52
74
 
75
+ /**
76
+ * GET a legacy non-enveloped endpoint and return the parsed body as-is,
77
+ * with no `{ data }` unwrapping. `get()` now also tolerates bare bodies, so
78
+ * this is mostly for callers that want the raw shape without the
79
+ * `{ data, users, requestId }` wrapper. Still goes through timeout + GET-retry.
80
+ */
81
+ async getRaw<T>(path: string, params?: Record<string, string>): Promise<T> {
82
+ const response = await this.executeWithRetry('GET', path, undefined, params);
83
+ return (await response.json()) as T;
84
+ }
85
+
53
86
  async post<T>(path: string, body?: unknown): Promise<ApiSuccessResponse<T>> {
54
87
  return this.request<T>('POST', path, body);
55
88
  }
@@ -70,24 +103,33 @@ export class ImboardApiClient {
70
103
  method: HttpMethod,
71
104
  path: string,
72
105
  body?: unknown,
73
- params?: Record<string, string>,
106
+ params?: Record<string, string>
74
107
  ): Promise<ApiSuccessResponse<T>> {
75
108
  const response = await this.executeWithRetry(method, path, body, params);
76
109
  const requestId = response.headers.get('x-request-id');
77
- const json = await response.json() as Record<string, unknown>;
110
+ const json = (await response.json()) as Record<string, unknown>;
78
111
 
79
- if (!json || typeof json !== 'object' || !('data' in json)) {
112
+ if (json === null || typeof json !== 'object') {
80
113
  throw new ImboardApiError({
81
114
  code: 'INTERNAL_ERROR',
82
- message: 'Response body missing "data" field',
115
+ message: 'Response body is not a JSON object',
83
116
  status: response.status,
84
117
  requestId,
85
118
  details: null,
86
119
  });
87
120
  }
88
121
 
122
+ // Most v1 routes wrap payloads as `{ data, meta, users }`, but several legacy
123
+ // routes return a bare object (e.g. historical-metrics `{ boardId, metrics }`,
124
+ // board-feedback `{ messages }`, document search `{ documentIds, meta }`).
125
+ // Tolerate both: use `data` when enveloped, otherwise pass the whole body
126
+ // through. Without this, every non-enveloped tool failed with a confusing
127
+ // "Response body missing data field" error.
128
+ const hasEnvelope = 'data' in json;
129
+
89
130
  return {
90
- data: json.data as T,
131
+ data: (hasEnvelope ? json.data : json) as T,
132
+ users: 'users' in json ? (json.users as UserMap) : undefined,
91
133
  requestId,
92
134
  };
93
135
  }
@@ -95,11 +137,11 @@ export class ImboardApiClient {
95
137
  private async requestCollection<T>(
96
138
  method: HttpMethod,
97
139
  path: string,
98
- params?: Record<string, string>,
140
+ params?: Record<string, string>
99
141
  ): Promise<ApiCollectionResponse<T>> {
100
142
  const response = await this.executeWithRetry(method, path, undefined, params);
101
143
  const requestId = response.headers.get('x-request-id');
102
- const json = await response.json() as Record<string, unknown>;
144
+ const json = (await response.json()) as Record<string, unknown>;
103
145
 
104
146
  if (!json || typeof json !== 'object' || !('data' in json) || !('meta' in json)) {
105
147
  throw new ImboardApiError({
@@ -114,6 +156,7 @@ export class ImboardApiClient {
114
156
  return {
115
157
  data: json.data as T[],
116
158
  meta: json.meta as ApiCollectionMeta,
159
+ users: 'users' in json ? (json.users as UserMap) : undefined,
117
160
  requestId,
118
161
  };
119
162
  }
@@ -122,7 +165,7 @@ export class ImboardApiClient {
122
165
  method: HttpMethod,
123
166
  path: string,
124
167
  body?: unknown,
125
- params?: Record<string, string>,
168
+ params?: Record<string, string>
126
169
  ): Promise<Response> {
127
170
  const url = this.buildUrl(path, params);
128
171
  // Conservative: only retry GET. PUT/DELETE are technically idempotent
@@ -135,7 +178,9 @@ export class ImboardApiClient {
135
178
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
136
179
  if (attempt > 0) {
137
180
  const delayMs = RETRY_BASE_MS * Math.pow(2, attempt - 1);
138
- logger.info(`Retrying ${method} ${path} (attempt ${attempt + 1}/${maxAttempts})`, { delayMs });
181
+ logger.info(`Retrying ${method} ${path} (attempt ${attempt + 1}/${maxAttempts})`, {
182
+ delayMs,
183
+ });
139
184
  await sleep(delayMs);
140
185
  }
141
186
 
@@ -160,7 +205,9 @@ export class ImboardApiClient {
160
205
 
161
206
  // Retry on 5xx for idempotent requests
162
207
  if (isIdempotent && response.status >= 500 && attempt < maxAttempts - 1) {
163
- logger.warn(`Server error ${response.status} on ${method} ${path}, will retry`, { requestId });
208
+ logger.warn(`Server error ${response.status} on ${method} ${path}, will retry`, {
209
+ requestId,
210
+ });
164
211
  lastError = apiError;
165
212
  continue;
166
213
  }
@@ -195,8 +242,8 @@ export class ImboardApiClient {
195
242
  const timer = setTimeout(() => controller.abort(), this.timeoutMs);
196
243
 
197
244
  const headers: Record<string, string> = {
198
- 'Authorization': `Bearer ${this.token}`,
199
- 'Accept': 'application/json',
245
+ Authorization: `Bearer ${this.token}`,
246
+ Accept: 'application/json',
200
247
  };
201
248
 
202
249
  if (body !== undefined) {
package/src/server.ts CHANGED
@@ -16,10 +16,14 @@ import { registerNotificationTools } from './tools/notifications.tools.js';
16
16
  import { registerActionItemTools } from './tools/action-items.tools.js';
17
17
  import { registerUserTools } from './tools/user.tools.js';
18
18
  import { registerSupportingTools } from './tools/supporting.tools.js';
19
+ import { registerRogueKpiTools } from './tools/rogue-kpis.tools.js';
20
+ import { registerPersonaDossierTools } from './tools/persona-dossiers.tools.js';
21
+ import { registerKnowledgeGraphTools } from './tools/knowledge-graph.tools.js';
22
+ import { registerKgAdvisoryTools } from './tools/kg-advisory.tools.js';
19
23
  import { registerDocsResources } from './resources/docs.resources.js';
20
24
 
21
25
  const SERVER_NAME = 'imboard-mcp-server';
22
- const SERVER_VERSION = '0.1.0';
26
+ const SERVER_VERSION = '0.1.3';
23
27
 
24
28
  export function createServer(config: McpConfig): McpServer {
25
29
  logger.info(`Creating ${SERVER_NAME} v${SERVER_VERSION}`);
@@ -45,6 +49,10 @@ export function createServer(config: McpConfig): McpServer {
45
49
  registerActionItemTools(server, api);
46
50
  registerUserTools(server, api);
47
51
  registerSupportingTools(server, api);
52
+ registerRogueKpiTools(server, api);
53
+ registerPersonaDossierTools(server, api);
54
+ registerKnowledgeGraphTools(server, api);
55
+ registerKgAdvisoryTools(server, api);
48
56
 
49
57
  registerDocsResources(server);
50
58
 
@@ -1,31 +1,42 @@
1
- import { boardIdParam, resourceIdParam, paginationParams, buildQueryParams, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
1
+ import {
2
+ boardIdParam,
3
+ resourceIdParam,
4
+ paginationParams,
5
+ buildQueryParams,
6
+ formatResult,
7
+ handleToolError,
8
+ RegisterToolsFn,
9
+ } from './shared.js';
2
10
 
3
11
  export const registerDashboardTools: RegisterToolsFn = (server, client) => {
4
12
  server.tool(
5
13
  'list_report_dashboards',
6
- 'Lists dashboards under a specific report. Dashboards are nested under reports in imboard\'s domain model.',
14
+ "Lists dashboards under a specific report. Dashboards are nested under reports in imboard's domain model. Includes a top-level `users` map resolving any userId references in the response (e.g. createdByUserId).",
7
15
  {
8
16
  boardId: boardIdParam,
9
- reportId: resourceIdParam.describe('The report ID (required — dashboards are nested under reports)'),
17
+ reportId: resourceIdParam.describe(
18
+ 'The report ID (required — dashboards are nested under reports)'
19
+ ),
10
20
  ...paginationParams,
11
21
  },
12
22
  async ({ boardId, reportId, ...rest }) => {
13
23
  try {
14
24
  const params = buildQueryParams(rest, []);
25
+ params.include = 'users';
15
26
  const result = await client.getCollection(
16
27
  `/api/boards/${boardId}/reports/${reportId}/dashboards`,
17
- params,
28
+ params
18
29
  );
19
- return formatResult({ data: result.data, meta: result.meta });
30
+ return formatResult({ data: result.data, meta: result.meta, users: result.users });
20
31
  } catch (error) {
21
32
  return handleToolError(error);
22
33
  }
23
- },
34
+ }
24
35
  );
25
36
 
26
37
  server.tool(
27
38
  'get_dashboard',
28
- 'Returns details for a specific dashboard including type, title, and associated report.',
39
+ 'Returns details for a specific dashboard including type, title, and associated report. Includes a top-level `users` map resolving any userId references in the response.',
29
40
  {
30
41
  boardId: boardIdParam,
31
42
  reportId: resourceIdParam.describe('The report ID'),
@@ -35,11 +46,12 @@ export const registerDashboardTools: RegisterToolsFn = (server, client) => {
35
46
  try {
36
47
  const result = await client.get(
37
48
  `/api/boards/${boardId}/reports/${reportId}/dashboards/${dashboardId}`,
49
+ { include: 'users' }
38
50
  );
39
- return formatResult({ data: result.data });
51
+ return formatResult({ data: result.data, users: result.users });
40
52
  } catch (error) {
41
53
  return handleToolError(error);
42
54
  }
43
- },
55
+ }
44
56
  );
45
57
  };
@@ -1,46 +1,55 @@
1
1
  import { z } from 'zod';
2
- import { boardIdParam, resourceIdParam, paginationParams, buildQueryParams, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
2
+ import {
3
+ boardIdParam,
4
+ resourceIdParam,
5
+ paginationParams,
6
+ buildQueryParams,
7
+ formatResult,
8
+ handleToolError,
9
+ RegisterToolsFn,
10
+ } from './shared.js';
3
11
 
4
12
  export const registerDocumentTools: RegisterToolsFn = (server, client) => {
5
13
  server.tool(
6
14
  'list_board_documents',
7
- 'Lists document metadata for a board. Supports filtering by meeting or document type. Does not provide file download in V1.',
15
+ 'Lists document metadata for a board. Supports filtering by meeting or document type. Does not provide file download in V1. Includes a top-level `users` map resolving any userId references in the response (e.g. createdByUserId).',
8
16
  {
9
17
  boardId: boardIdParam,
10
18
  ...paginationParams,
11
19
  meetingId: z.string().optional().describe('Filter by meeting ID'),
12
- documentType: z.string().optional().describe('Filter by document type (e.g. boardResolutions, minutesOfMeetings, capTable)'),
20
+ documentType: z
21
+ .string()
22
+ .optional()
23
+ .describe('Filter by document type (e.g. boardResolutions, minutesOfMeetings, capTable)'),
13
24
  },
14
25
  async ({ boardId, ...rest }) => {
15
26
  try {
16
27
  const params = buildQueryParams(rest, ['meetingId', 'documentType']);
17
- const result = await client.getCollection(
18
- `/api/boards/${boardId}/documents`,
19
- params,
20
- );
21
- return formatResult({ data: result.data, meta: result.meta });
28
+ params.include = 'users';
29
+ const result = await client.getCollection(`/api/boards/${boardId}/documents`, params);
30
+ return formatResult({ data: result.data, meta: result.meta, users: result.users });
22
31
  } catch (error) {
23
32
  return handleToolError(error);
24
33
  }
25
- },
34
+ }
26
35
  );
27
36
 
28
37
  server.tool(
29
38
  'get_document',
30
- 'Returns metadata for a specific document including title, type, and associated meeting. Does not provide file download in V1.',
39
+ 'Returns metadata for a specific document including title, type, and associated meeting. Does not provide file download in V1. Includes a top-level `users` map resolving any userId references in the response.',
31
40
  {
32
41
  boardId: boardIdParam,
33
42
  documentId: resourceIdParam.describe('The document ID'),
34
43
  },
35
44
  async ({ boardId, documentId }) => {
36
45
  try {
37
- const result = await client.get(
38
- `/api/boards/${boardId}/documents/${documentId}`,
39
- );
40
- return formatResult({ data: result.data });
46
+ const result = await client.get(`/api/boards/${boardId}/documents/${documentId}`, {
47
+ include: 'users',
48
+ });
49
+ return formatResult({ data: result.data, users: result.users });
41
50
  } catch (error) {
42
51
  return handleToolError(error);
43
52
  }
44
- },
53
+ }
45
54
  );
46
55
  };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * KG governance advisory MCP tools (#978).
3
+ *
4
+ * Read-only surface (3 tools): list advisories, list runs, run a review pass.
5
+ * Mutation tools (dismiss/snooze/reopen/config-edit) are NOT exposed in v1 —
6
+ * those should remain explicit user actions through the UI for proper
7
+ * attribution.
8
+ *
9
+ * `get_governance_advisories` enriches the response with a `users` map (name
10
+ * + positions) so agent consumers don't need to re-resolve user IDs.
11
+ */
12
+
13
+ import { z } from 'zod';
14
+ import { boardIdParam, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
15
+
16
+ const ruleTypeEnum = z.enum([
17
+ 'stale_commitment',
18
+ 'dropped_topic',
19
+ 'governance_coverage_gap',
20
+ 'narrative_drift',
21
+ 'unanswered_director_question',
22
+ 'orphan_decision',
23
+ 'textual_contradiction',
24
+ ]);
25
+
26
+ const statusEnum = z.enum(['open', 'dismissed', 'resolved', 'snoozed']);
27
+
28
+ export const registerKgAdvisoryTools: RegisterToolsFn = (server, client) => {
29
+ server.tool(
30
+ 'get_governance_advisories',
31
+ 'List KG governance advisories for a board. Returns all statuses by default (consumer groups by status). Filter via status, ruleType, or limit. Includes a top-level `users` map resolving any userId references in the response (owner, raisedBy, dismissedBy).',
32
+ {
33
+ boardId: boardIdParam,
34
+ status: statusEnum
35
+ .optional()
36
+ .describe('Filter to a single status. Default: all statuses returned.'),
37
+ ruleType: ruleTypeEnum.optional().describe('Filter to a single rule type.'),
38
+ limit: z
39
+ .number()
40
+ .int()
41
+ .min(1)
42
+ .max(200)
43
+ .optional()
44
+ .describe('Max advisories to return (default 100).'),
45
+ },
46
+ async ({ boardId, status, ruleType, limit }) => {
47
+ try {
48
+ const params: Record<string, string> = { include: 'users' };
49
+ if (status) params.status = status;
50
+ if (ruleType) params.ruleType = ruleType;
51
+ if (limit !== undefined) params.limit = String(limit);
52
+ const response = await client.get(
53
+ `/api/boards/${boardId}/knowledge-graph/governance-advisory`,
54
+ params
55
+ );
56
+ return formatResult(response.data);
57
+ } catch (error) {
58
+ return handleToolError(error);
59
+ }
60
+ }
61
+ );
62
+
63
+ server.tool(
64
+ 'get_governance_advisory_runs',
65
+ 'List recent KG governance advisory review runs for a board (paginated, newest first). Audit/retro surface — see when runs fired, who triggered them, and finding counts per rule.',
66
+ {
67
+ boardId: boardIdParam,
68
+ limit: z
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .max(100)
73
+ .optional()
74
+ .describe('Max runs to return (default 10).'),
75
+ },
76
+ async ({ boardId, limit }) => {
77
+ try {
78
+ const params: Record<string, string> = {};
79
+ if (limit !== undefined) params.limit = String(limit);
80
+ const response = await client.get(
81
+ `/api/boards/${boardId}/knowledge-graph/governance-advisory/runs`,
82
+ params
83
+ );
84
+ return formatResult(response.data);
85
+ } catch (error) {
86
+ return handleToolError(error);
87
+ }
88
+ }
89
+ );
90
+
91
+ server.tool(
92
+ 'run_governance_advisory_review',
93
+ 'Trigger a KG governance advisory review pass for a board. Returns the review ID. A 60s soft check returns the in-flight review if one already started in the last minute.',
94
+ {
95
+ boardId: boardIdParam,
96
+ reason: z.string().optional().describe('Free-text reason for triggering this run.'),
97
+ },
98
+ async ({ boardId, reason }) => {
99
+ try {
100
+ const response = await client.post(
101
+ `/api/boards/${boardId}/knowledge-graph/governance-advisory/run`,
102
+ { reason, source: 'mcp_tool' }
103
+ );
104
+ return formatResult(response.data);
105
+ } catch (error) {
106
+ return handleToolError(error);
107
+ }
108
+ }
109
+ );
110
+ };
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod';
2
+ import { boardIdParam, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
3
+
4
+ export const registerKnowledgeGraphTools: RegisterToolsFn = (server, client) => {
5
+ server.tool(
6
+ 'get_board_prep_brief',
7
+ 'Get a full board preparation brief from the knowledge graph: open contradictions, unanswered director questions, approved decisions pending follow-up, governance coverage gaps, dropped topics, director question patterns, and narrative drift. Requires board admin or internal management role.',
8
+ {
9
+ boardId: boardIdParam,
10
+ },
11
+ async ({ boardId }) => {
12
+ try {
13
+ const response = await client.get(`/api/boards/${boardId}/kg/prep-brief`);
14
+ return formatResult({ data: response.data });
15
+ } catch (error) {
16
+ return handleToolError(error);
17
+ }
18
+ }
19
+ );
20
+
21
+ server.tool(
22
+ 'get_knowledge_graph_summary',
23
+ 'Get knowledge graph index stats for a board: node counts by type, edge count, health metrics (open contradictions, open questions, stale commitments), governance coverage gaps, dropped topics, compiled summary, and recent ingestion history. Requires board admin or internal management role.',
24
+ {
25
+ boardId: boardIdParam,
26
+ },
27
+ async ({ boardId }) => {
28
+ try {
29
+ const response = await client.get(`/api/boards/${boardId}/kg/summary`);
30
+ return formatResult({ data: response.data });
31
+ } catch (error) {
32
+ return handleToolError(error);
33
+ }
34
+ }
35
+ );
36
+
37
+ server.tool(
38
+ 'get_open_items',
39
+ 'Get open items from the knowledge graph. Filter by type: "contradictions" (conflicting data across sources), "questions" (unanswered or deferred director questions), or "commitments" (approved/proposed decisions pending follow-up). Omit type to get all. Requires board admin or internal management role.',
40
+ {
41
+ boardId: boardIdParam,
42
+ type: z
43
+ .enum(['contradictions', 'questions', 'commitments'])
44
+ .optional()
45
+ .describe(
46
+ 'Filter by item type: contradictions, questions, or commitments. Omit to get all types.'
47
+ ),
48
+ },
49
+ async ({ boardId, type }) => {
50
+ try {
51
+ const params: Record<string, string> = {};
52
+ if (type) params.type = type;
53
+ const response = await client.get(`/api/boards/${boardId}/kg/open-items`, params);
54
+ return formatResult({ data: response.data });
55
+ } catch (error) {
56
+ return handleToolError(error);
57
+ }
58
+ }
59
+ );
60
+
61
+ server.tool(
62
+ 'query_knowledge_graph',
63
+ 'Ask a natural language question against the board knowledge graph. The system retrieves relevant graph data (contradictions, decisions, questions, topics, themes, KPI mentions) and synthesizes an answer using an LLM. Requires board admin or internal management role.',
64
+ {
65
+ boardId: boardIdParam,
66
+ question: z
67
+ .string()
68
+ .max(2000)
69
+ .describe(
70
+ 'The natural language question to ask about the board knowledge graph (max 2000 chars)'
71
+ ),
72
+ },
73
+ async ({ boardId, question }) => {
74
+ try {
75
+ const response = await client.post(`/api/boards/${boardId}/kg/query`, { question });
76
+ return formatResult({ data: response.data });
77
+ } catch (error) {
78
+ return handleToolError(error);
79
+ }
80
+ }
81
+ );
82
+ };