@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/README.md +112 -88
- package/dist/index.cjs +350 -73
- package/package.json +20 -19
- package/src/api-client/imboardApiClient.ts +61 -14
- package/src/server.ts +9 -1
- package/src/tools/dashboards.tools.ts +21 -9
- package/src/tools/documents.tools.ts +24 -15
- package/src/tools/kg-advisory.tools.ts +110 -0
- package/src/tools/knowledge-graph.tools.ts +82 -0
- package/src/tools/meetings.tools.ts +73 -41
- package/src/tools/persona-dossiers.tools.ts +27 -0
- package/src/tools/reports.tools.ts +84 -43
- package/src/tools/rogue-kpis.tools.ts +60 -0
- package/src/tools/user.tools.ts +9 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imboard.ai/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
60
|
+
"@swc/core": "^1.15.24",
|
|
72
61
|
"@swc/jest": "^0.2.39",
|
|
73
62
|
"@types/jest": "^30.0.0",
|
|
74
|
-
"@types/node": "^25.
|
|
63
|
+
"@types/node": "^25.6.0",
|
|
75
64
|
"cross-env": "^10.1.0",
|
|
76
|
-
"esbuild": "^0.
|
|
77
|
-
"eslint": "^
|
|
65
|
+
"esbuild": "^0.28.0",
|
|
66
|
+
"eslint": "^10.2.0",
|
|
78
67
|
"jest": "^30.3.0",
|
|
79
|
-
"prettier": "^3.8.
|
|
68
|
+
"prettier": "^3.8.2",
|
|
80
69
|
"tsx": "^4.11.2",
|
|
81
|
-
"typescript": "^
|
|
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
|
-
|
|
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>(
|
|
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 (
|
|
112
|
+
if (json === null || typeof json !== 'object') {
|
|
80
113
|
throw new ImboardApiError({
|
|
81
114
|
code: 'INTERNAL_ERROR',
|
|
82
|
-
message: 'Response body
|
|
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})`, {
|
|
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`, {
|
|
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
|
-
|
|
199
|
-
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
+
};
|