@imboard.ai/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +194 -0
- package/dist/index.cjs +1866 -0
- package/package.json +83 -0
- package/src/api-client/errors.ts +42 -0
- package/src/api-client/imboardApiClient.ts +256 -0
- package/src/config.ts +62 -0
- package/src/index.ts +32 -0
- package/src/resources/docs.resources.ts +97 -0
- package/src/server.ts +54 -0
- package/src/tools/action-items.tools.ts +41 -0
- package/src/tools/boards.tools.ts +172 -0
- package/src/tools/dashboards.tools.ts +45 -0
- package/src/tools/documents-write.tools.ts +110 -0
- package/src/tools/documents.tools.ts +46 -0
- package/src/tools/getMe.tool.ts +17 -0
- package/src/tools/invites.tools.ts +90 -0
- package/src/tools/meetings.tools.ts +160 -0
- package/src/tools/members.tools.ts +43 -0
- package/src/tools/notifications.tools.ts +47 -0
- package/src/tools/reports.tools.ts +414 -0
- package/src/tools/shared.ts +82 -0
- package/src/tools/slots.tools.ts +115 -0
- package/src/tools/supporting.tools.ts +92 -0
- package/src/tools/user.tools.ts +31 -0
- package/src/utils/logging.ts +59 -0
- package/src/utils/redact.ts +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@imboard.ai/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "imboard MCP server — adapter over the public REST API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=18.0.0"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"bin": {
|
|
12
|
+
"imboard-mcp-server": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./dist/index.cjs",
|
|
16
|
+
"./api-client": {
|
|
17
|
+
"import": "./src/api-client/imboardApiClient.ts",
|
|
18
|
+
"default": "./src/api-client/imboardApiClient.ts"
|
|
19
|
+
},
|
|
20
|
+
"./config": {
|
|
21
|
+
"import": "./src/config.ts",
|
|
22
|
+
"default": "./src/config.ts"
|
|
23
|
+
},
|
|
24
|
+
"./errors": {
|
|
25
|
+
"import": "./src/api-client/errors.ts",
|
|
26
|
+
"default": "./src/api-client/errors.ts"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist/**/*.cjs",
|
|
31
|
+
"src/**/*.ts",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"mcp",
|
|
39
|
+
"model-context-protocol",
|
|
40
|
+
"imboard",
|
|
41
|
+
"ai",
|
|
42
|
+
"claude",
|
|
43
|
+
"board-management"
|
|
44
|
+
],
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/imboard-ai/imboard-monorepo.git",
|
|
48
|
+
"directory": "main/packages/mcp-server"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/imboard-ai/imboard-monorepo/tree/main/main/packages/mcp-server#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/imboard-ai/imboard-monorepo/issues"
|
|
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
|
+
"dependencies": {
|
|
66
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
67
|
+
"zod": "^4.1.12"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@jest/globals": "^30.3.0",
|
|
71
|
+
"@swc/core": "^1.15.21",
|
|
72
|
+
"@swc/jest": "^0.2.39",
|
|
73
|
+
"@types/jest": "^30.0.0",
|
|
74
|
+
"@types/node": "^25.5.0",
|
|
75
|
+
"cross-env": "^10.1.0",
|
|
76
|
+
"esbuild": "^0.27.2",
|
|
77
|
+
"eslint": "^9.39.2",
|
|
78
|
+
"jest": "^30.3.0",
|
|
79
|
+
"prettier": "^3.8.1",
|
|
80
|
+
"tsx": "^4.11.2",
|
|
81
|
+
"typescript": "^5.8.2"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types for the imboard API client.
|
|
3
|
+
* Maps the /api error envelope into a typed exception.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ApiErrorPayload {
|
|
7
|
+
code: string;
|
|
8
|
+
message: string;
|
|
9
|
+
status: number;
|
|
10
|
+
requestId: string | null;
|
|
11
|
+
details: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ImboardApiError extends Error {
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly status: number;
|
|
17
|
+
readonly requestId: string | null;
|
|
18
|
+
readonly details: unknown;
|
|
19
|
+
|
|
20
|
+
constructor(payload: ApiErrorPayload) {
|
|
21
|
+
super(payload.message);
|
|
22
|
+
this.name = 'ImboardApiError';
|
|
23
|
+
this.code = payload.code;
|
|
24
|
+
this.status = payload.status;
|
|
25
|
+
this.requestId = payload.requestId;
|
|
26
|
+
this.details = payload.details;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ImboardApiTimeoutError extends Error {
|
|
31
|
+
constructor(timeoutMs: number) {
|
|
32
|
+
super(`Request timed out after ${timeoutMs}ms`);
|
|
33
|
+
this.name = 'ImboardApiTimeoutError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ImboardApiNetworkError extends Error {
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ImboardApiNetworkError';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated HTTP client for calling the imboard /api endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Uses Node.js built-in fetch. All MCP tools call through this client —
|
|
5
|
+
* it handles auth, timeout, retry (GET only), and response normalization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { McpConfig } from '../config.js';
|
|
9
|
+
import { logger } from '../utils/logging.js';
|
|
10
|
+
import { ImboardApiError, ImboardApiTimeoutError, ImboardApiNetworkError } from './errors.js';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
13
|
+
const MAX_RETRIES = 2;
|
|
14
|
+
const RETRY_BASE_MS = 500;
|
|
15
|
+
|
|
16
|
+
export interface ApiSuccessResponse<T> {
|
|
17
|
+
data: T;
|
|
18
|
+
requestId: string | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ApiCollectionMeta {
|
|
22
|
+
nextCursor: string | null;
|
|
23
|
+
hasMore: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ApiCollectionResponse<T> {
|
|
27
|
+
data: T[];
|
|
28
|
+
meta: ApiCollectionMeta;
|
|
29
|
+
requestId: string | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
33
|
+
|
|
34
|
+
export class ImboardApiClient {
|
|
35
|
+
private readonly baseUrl: string;
|
|
36
|
+
private readonly token: string;
|
|
37
|
+
private readonly timeoutMs: number;
|
|
38
|
+
|
|
39
|
+
constructor(config: McpConfig, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
40
|
+
this.baseUrl = config.apiBaseUrl.replace(/\/+$/, '');
|
|
41
|
+
this.token = config.apiToken;
|
|
42
|
+
this.timeoutMs = timeoutMs;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async get<T>(path: string, params?: Record<string, string>): Promise<ApiSuccessResponse<T>> {
|
|
46
|
+
return this.request<T>('GET', path, undefined, params);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async getCollection<T>(path: string, params?: Record<string, string>): Promise<ApiCollectionResponse<T>> {
|
|
50
|
+
return this.requestCollection<T>('GET', path, params);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async post<T>(path: string, body?: unknown): Promise<ApiSuccessResponse<T>> {
|
|
54
|
+
return this.request<T>('POST', path, body);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async put<T>(path: string, body?: unknown): Promise<ApiSuccessResponse<T>> {
|
|
58
|
+
return this.request<T>('PUT', path, body);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async patch<T>(path: string, body?: unknown): Promise<ApiSuccessResponse<T>> {
|
|
62
|
+
return this.request<T>('PATCH', path, body);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async delete<T>(path: string): Promise<ApiSuccessResponse<T>> {
|
|
66
|
+
return this.request<T>('DELETE', path);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async request<T>(
|
|
70
|
+
method: HttpMethod,
|
|
71
|
+
path: string,
|
|
72
|
+
body?: unknown,
|
|
73
|
+
params?: Record<string, string>,
|
|
74
|
+
): Promise<ApiSuccessResponse<T>> {
|
|
75
|
+
const response = await this.executeWithRetry(method, path, body, params);
|
|
76
|
+
const requestId = response.headers.get('x-request-id');
|
|
77
|
+
const json = await response.json() as Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
if (!json || typeof json !== 'object' || !('data' in json)) {
|
|
80
|
+
throw new ImboardApiError({
|
|
81
|
+
code: 'INTERNAL_ERROR',
|
|
82
|
+
message: 'Response body missing "data" field',
|
|
83
|
+
status: response.status,
|
|
84
|
+
requestId,
|
|
85
|
+
details: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
data: json.data as T,
|
|
91
|
+
requestId,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async requestCollection<T>(
|
|
96
|
+
method: HttpMethod,
|
|
97
|
+
path: string,
|
|
98
|
+
params?: Record<string, string>,
|
|
99
|
+
): Promise<ApiCollectionResponse<T>> {
|
|
100
|
+
const response = await this.executeWithRetry(method, path, undefined, params);
|
|
101
|
+
const requestId = response.headers.get('x-request-id');
|
|
102
|
+
const json = await response.json() as Record<string, unknown>;
|
|
103
|
+
|
|
104
|
+
if (!json || typeof json !== 'object' || !('data' in json) || !('meta' in json)) {
|
|
105
|
+
throw new ImboardApiError({
|
|
106
|
+
code: 'INTERNAL_ERROR',
|
|
107
|
+
message: 'Response body missing "data" or "meta" field',
|
|
108
|
+
status: response.status,
|
|
109
|
+
requestId,
|
|
110
|
+
details: null,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
data: json.data as T[],
|
|
116
|
+
meta: json.meta as ApiCollectionMeta,
|
|
117
|
+
requestId,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async executeWithRetry(
|
|
122
|
+
method: HttpMethod,
|
|
123
|
+
path: string,
|
|
124
|
+
body?: unknown,
|
|
125
|
+
params?: Record<string, string>,
|
|
126
|
+
): Promise<Response> {
|
|
127
|
+
const url = this.buildUrl(path, params);
|
|
128
|
+
// Conservative: only retry GET. PUT/DELETE are technically idempotent
|
|
129
|
+
// but we avoid retrying writes to prevent confusing side effects.
|
|
130
|
+
const isIdempotent = method === 'GET';
|
|
131
|
+
const maxAttempts = isIdempotent ? MAX_RETRIES + 1 : 1;
|
|
132
|
+
|
|
133
|
+
let lastError: Error | undefined;
|
|
134
|
+
|
|
135
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
136
|
+
if (attempt > 0) {
|
|
137
|
+
const delayMs = RETRY_BASE_MS * Math.pow(2, attempt - 1);
|
|
138
|
+
logger.info(`Retrying ${method} ${path} (attempt ${attempt + 1}/${maxAttempts})`, { delayMs });
|
|
139
|
+
await sleep(delayMs);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const response = await this.executeSingle(method, url, body);
|
|
144
|
+
|
|
145
|
+
if (response.ok) {
|
|
146
|
+
return response;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Parse error envelope
|
|
150
|
+
const errorBody = await this.tryParseErrorBody(response);
|
|
151
|
+
const requestId = response.headers.get('x-request-id');
|
|
152
|
+
|
|
153
|
+
const apiError = new ImboardApiError({
|
|
154
|
+
code: errorBody?.code ?? 'INTERNAL_ERROR',
|
|
155
|
+
message: errorBody?.message ?? `HTTP ${response.status}`,
|
|
156
|
+
status: response.status,
|
|
157
|
+
requestId,
|
|
158
|
+
details: errorBody?.details ?? null,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Retry on 5xx for idempotent requests
|
|
162
|
+
if (isIdempotent && response.status >= 500 && attempt < maxAttempts - 1) {
|
|
163
|
+
logger.warn(`Server error ${response.status} on ${method} ${path}, will retry`, { requestId });
|
|
164
|
+
lastError = apiError;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
throw apiError;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof ImboardApiError) {
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (error instanceof ImboardApiTimeoutError || error instanceof ImboardApiNetworkError) {
|
|
175
|
+
if (isIdempotent && attempt < maxAttempts - 1) {
|
|
176
|
+
lastError = error;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Unexpected error
|
|
183
|
+
throw new ImboardApiNetworkError(
|
|
184
|
+
error instanceof Error ? error.message : 'Unknown network error'
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// All retries exhausted
|
|
190
|
+
throw lastError ?? new ImboardApiNetworkError('All retries exhausted');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private async executeSingle(method: HttpMethod, url: string, body?: unknown): Promise<Response> {
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
196
|
+
|
|
197
|
+
const headers: Record<string, string> = {
|
|
198
|
+
'Authorization': `Bearer ${this.token}`,
|
|
199
|
+
'Accept': 'application/json',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (body !== undefined) {
|
|
203
|
+
headers['Content-Type'] = 'application/json';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const response = await fetch(url, {
|
|
208
|
+
method,
|
|
209
|
+
headers,
|
|
210
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
211
|
+
signal: controller.signal,
|
|
212
|
+
});
|
|
213
|
+
return response;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
216
|
+
throw new ImboardApiTimeoutError(this.timeoutMs);
|
|
217
|
+
}
|
|
218
|
+
throw new ImboardApiNetworkError(
|
|
219
|
+
error instanceof Error ? error.message : 'Network request failed'
|
|
220
|
+
);
|
|
221
|
+
} finally {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private buildUrl(path: string, params?: Record<string, string>): string {
|
|
227
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
228
|
+
const url = new URL(`${this.baseUrl}${normalizedPath}`);
|
|
229
|
+
|
|
230
|
+
if (params) {
|
|
231
|
+
for (const [key, value] of Object.entries(params)) {
|
|
232
|
+
url.searchParams.set(key, value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return url.toString();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async tryParseErrorBody(
|
|
240
|
+
response: Response
|
|
241
|
+
): Promise<{ code: string; message: string; details: unknown } | undefined> {
|
|
242
|
+
try {
|
|
243
|
+
const text = await response.text();
|
|
244
|
+
const json = JSON.parse(text) as {
|
|
245
|
+
error?: { code: string; message: string; details: unknown };
|
|
246
|
+
};
|
|
247
|
+
return json.error;
|
|
248
|
+
} catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function sleep(ms: number): Promise<void> {
|
|
255
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
256
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export interface McpConfig {
|
|
2
|
+
apiBaseUrl: string;
|
|
3
|
+
apiToken: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class ConfigError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'ConfigError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function loadConfig(): McpConfig {
|
|
14
|
+
const errors: string[] = [];
|
|
15
|
+
|
|
16
|
+
const apiBaseUrl = process.env.IMBOARD_API_BASE_URL?.trim();
|
|
17
|
+
const apiToken = process.env.IMBOARD_API_TOKEN?.trim();
|
|
18
|
+
|
|
19
|
+
if (!apiBaseUrl) {
|
|
20
|
+
errors.push('IMBOARD_API_BASE_URL is required');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!apiToken) {
|
|
24
|
+
errors.push('IMBOARD_API_TOKEN is required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (apiBaseUrl && !isValidUrl(apiBaseUrl)) {
|
|
28
|
+
errors.push(`IMBOARD_API_BASE_URL is not a valid URL: ${apiBaseUrl}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (apiBaseUrl && isValidUrl(apiBaseUrl) && isPrivateAddress(apiBaseUrl)) {
|
|
32
|
+
errors.push('IMBOARD_API_BASE_URL must not point to a private/internal address');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
throw new ConfigError(
|
|
37
|
+
`Configuration errors:\n - ${errors.join('\n - ')}`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
apiBaseUrl: apiBaseUrl!,
|
|
43
|
+
apiToken: apiToken!,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isValidUrl(value: string): boolean {
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(value);
|
|
50
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PRIVATE_HOSTNAMES = ['localhost', '127.0.0.1', '0.0.0.0', '169.254.169.254', '[::1]'];
|
|
57
|
+
const PRIVATE_IP_PATTERN = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/;
|
|
58
|
+
|
|
59
|
+
function isPrivateAddress(value: string): boolean {
|
|
60
|
+
const { hostname } = new URL(value);
|
|
61
|
+
return PRIVATE_HOSTNAMES.includes(hostname) || PRIVATE_IP_PATTERN.test(hostname);
|
|
62
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2
|
+
import { loadConfig, ConfigError } from './config.js';
|
|
3
|
+
import { createServer } from './server.js';
|
|
4
|
+
import { logger, setLogLevel } from './utils/logging.js';
|
|
5
|
+
|
|
6
|
+
async function main(): Promise<void> {
|
|
7
|
+
if (process.env.LOG_LEVEL) {
|
|
8
|
+
setLogLevel(process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
logger.info('Starting imboard MCP server...');
|
|
12
|
+
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
logger.info('Configuration loaded', { apiBaseUrl: config.apiBaseUrl });
|
|
15
|
+
|
|
16
|
+
const server = createServer(config);
|
|
17
|
+
const transport = new StdioServerTransport();
|
|
18
|
+
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
logger.info('MCP server connected via stdio transport');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((err: unknown) => {
|
|
24
|
+
if (err instanceof ConfigError) {
|
|
25
|
+
logger.error(err.message);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
logger.error('Fatal error during startup', {
|
|
29
|
+
error: err instanceof Error ? err.message : String(err),
|
|
30
|
+
});
|
|
31
|
+
process.exit(1);
|
|
32
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { logger } from '../utils/logging.js';
|
|
3
|
+
|
|
4
|
+
const DOCS_BASE_URL = 'https://docs.imboard.ai';
|
|
5
|
+
|
|
6
|
+
interface DocResource {
|
|
7
|
+
uri: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const STATIC_RESOURCES: DocResource[] = [
|
|
14
|
+
{
|
|
15
|
+
uri: `${DOCS_BASE_URL}/openapi.json`,
|
|
16
|
+
name: 'OpenAPI Specification',
|
|
17
|
+
description:
|
|
18
|
+
'Complete OpenAPI 3.1 spec for the imboard REST API — all endpoints, schemas, and examples',
|
|
19
|
+
mimeType: 'application/json',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
uri: `${DOCS_BASE_URL}/llms.txt`,
|
|
23
|
+
name: 'LLMs.txt Index',
|
|
24
|
+
description:
|
|
25
|
+
'Structured index of all documentation pages — optimized for LLM consumption',
|
|
26
|
+
mimeType: 'text/plain',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
uri: `${DOCS_BASE_URL}/llms-full.txt`,
|
|
30
|
+
name: 'Full Documentation (LLM)',
|
|
31
|
+
description:
|
|
32
|
+
'Complete documentation content in a single text file — all guides, references, and examples',
|
|
33
|
+
mimeType: 'text/plain',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
uri: `${DOCS_BASE_URL}/api/schemas`,
|
|
37
|
+
name: 'Schema Index',
|
|
38
|
+
description:
|
|
39
|
+
'JSON index of all resource schemas (board, meeting, document, report, dashboard)',
|
|
40
|
+
mimeType: 'application/json',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
uri: `${DOCS_BASE_URL}/api/recipes`,
|
|
44
|
+
name: 'Recipe Index',
|
|
45
|
+
description:
|
|
46
|
+
'JSON index of available dossier recipes (financial-update, sales-pipeline-update)',
|
|
47
|
+
mimeType: 'application/json',
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export function registerDocsResources(server: McpServer): void {
|
|
52
|
+
for (const resource of STATIC_RESOURCES) {
|
|
53
|
+
server.resource(resource.name, resource.uri, {
|
|
54
|
+
description: resource.description,
|
|
55
|
+
mimeType: resource.mimeType,
|
|
56
|
+
}, async () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(resource.uri);
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
return {
|
|
61
|
+
contents: [
|
|
62
|
+
{
|
|
63
|
+
uri: resource.uri,
|
|
64
|
+
mimeType: 'text/plain',
|
|
65
|
+
text: `Failed to fetch ${resource.uri}: ${response.status} ${response.statusText}`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const text = await response.text();
|
|
72
|
+
return {
|
|
73
|
+
contents: [
|
|
74
|
+
{
|
|
75
|
+
uri: resource.uri,
|
|
76
|
+
mimeType: resource.mimeType,
|
|
77
|
+
text,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
return {
|
|
84
|
+
contents: [
|
|
85
|
+
{
|
|
86
|
+
uri: resource.uri,
|
|
87
|
+
mimeType: 'text/plain',
|
|
88
|
+
text: `Error fetching ${resource.uri}: ${message}`,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
logger.info(`Registered ${STATIC_RESOURCES.length} docs resources`);
|
|
97
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { McpConfig } from './config.js';
|
|
3
|
+
import { ImboardApiClient } from './api-client/imboardApiClient.js';
|
|
4
|
+
import { registerGetMeTool } from './tools/getMe.tool.js';
|
|
5
|
+
import { registerBoardsTools } from './tools/boards.tools.js';
|
|
6
|
+
import { registerMemberTools } from './tools/members.tools.js';
|
|
7
|
+
import { registerMeetingTools } from './tools/meetings.tools.js';
|
|
8
|
+
import { logger } from './utils/logging.js';
|
|
9
|
+
import { registerDocumentTools } from './tools/documents.tools.js';
|
|
10
|
+
import { registerDocumentWriteTools } from './tools/documents-write.tools.js';
|
|
11
|
+
import { registerReportTools } from './tools/reports.tools.js';
|
|
12
|
+
import { registerDashboardTools } from './tools/dashboards.tools.js';
|
|
13
|
+
import { registerSlotTools } from './tools/slots.tools.js';
|
|
14
|
+
import { registerInviteTools } from './tools/invites.tools.js';
|
|
15
|
+
import { registerNotificationTools } from './tools/notifications.tools.js';
|
|
16
|
+
import { registerActionItemTools } from './tools/action-items.tools.js';
|
|
17
|
+
import { registerUserTools } from './tools/user.tools.js';
|
|
18
|
+
import { registerSupportingTools } from './tools/supporting.tools.js';
|
|
19
|
+
import { registerDocsResources } from './resources/docs.resources.js';
|
|
20
|
+
|
|
21
|
+
const SERVER_NAME = 'imboard-mcp-server';
|
|
22
|
+
const SERVER_VERSION = '0.1.0';
|
|
23
|
+
|
|
24
|
+
export function createServer(config: McpConfig): McpServer {
|
|
25
|
+
logger.info(`Creating ${SERVER_NAME} v${SERVER_VERSION}`);
|
|
26
|
+
|
|
27
|
+
const server = new McpServer({
|
|
28
|
+
name: SERVER_NAME,
|
|
29
|
+
version: SERVER_VERSION,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const api = new ImboardApiClient(config);
|
|
33
|
+
|
|
34
|
+
registerGetMeTool(server, api);
|
|
35
|
+
registerBoardsTools(server, api);
|
|
36
|
+
registerMemberTools(server, api);
|
|
37
|
+
registerMeetingTools(server, api);
|
|
38
|
+
registerDocumentTools(server, api);
|
|
39
|
+
registerDocumentWriteTools(server, api);
|
|
40
|
+
registerReportTools(server, api);
|
|
41
|
+
registerDashboardTools(server, api);
|
|
42
|
+
registerSlotTools(server, api);
|
|
43
|
+
registerInviteTools(server, api);
|
|
44
|
+
registerNotificationTools(server, api);
|
|
45
|
+
registerActionItemTools(server, api);
|
|
46
|
+
registerUserTools(server, api);
|
|
47
|
+
registerSupportingTools(server, api);
|
|
48
|
+
|
|
49
|
+
registerDocsResources(server);
|
|
50
|
+
|
|
51
|
+
logger.info('All tools and resources registered');
|
|
52
|
+
|
|
53
|
+
return server;
|
|
54
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { resourceIdParam, formatResult, handleToolError, RegisterToolsFn } from './shared.js';
|
|
3
|
+
|
|
4
|
+
export const registerActionItemTools: RegisterToolsFn = (server, client) => {
|
|
5
|
+
server.tool(
|
|
6
|
+
'list_my_action_items',
|
|
7
|
+
'Lists action items assigned to the authenticated user across all boards.',
|
|
8
|
+
{
|
|
9
|
+
includeCompleted: z.enum(['true', 'false']).optional().describe('Include completed items (default: false)'),
|
|
10
|
+
status: z.string().optional().describe('Filter by status (open, in_progress, completed, cancelled)'),
|
|
11
|
+
},
|
|
12
|
+
async ({ includeCompleted, status }) => {
|
|
13
|
+
try {
|
|
14
|
+
const params: Record<string, string> = {};
|
|
15
|
+
if (includeCompleted !== undefined) params.includeCompleted = includeCompleted;
|
|
16
|
+
if (status !== undefined) params.status = status;
|
|
17
|
+
const response = await client.get('/api/action-items/my', params);
|
|
18
|
+
return formatResult({ data: response.data });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return handleToolError(error);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
server.tool(
|
|
26
|
+
'update_action_item_status',
|
|
27
|
+
'Updates an action item status (in_progress, completed, cancelled).',
|
|
28
|
+
{
|
|
29
|
+
actionItemId: resourceIdParam.describe('The action item ID'),
|
|
30
|
+
status: z.string().describe('New status (open, in_progress, completed, cancelled)'),
|
|
31
|
+
},
|
|
32
|
+
async ({ actionItemId, status }) => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await client.patch(`/api/action-items/${actionItemId}/status`, { status });
|
|
35
|
+
return formatResult({ data: response.data });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return handleToolError(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
};
|