@opsee/mcp-server 0.1.5 → 0.1.7

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 ADDED
@@ -0,0 +1,108 @@
1
+ # Opsee MCP Server
2
+
3
+ Manage your Opsee projects, tasks, docs, and cycles directly from AI-powered coding environments like Claude Code and Cursor.
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Authenticate
8
+
9
+ ```bash
10
+ npx @opsee/mcp-server login
11
+ ```
12
+
13
+ This opens your browser to sign in to Opsee. Your credentials are saved to `~/.opsee/credentials.json`.
14
+
15
+ ### 2. Add to Claude Code
16
+
17
+ ```bash
18
+ claude mcp add opsee -- npx @opsee/mcp-server serve
19
+ ```
20
+
21
+ ### 3. Restart Claude Code
22
+
23
+ Exit and reopen Claude Code. Run `/mcp` to verify `opsee` shows as connected.
24
+
25
+ ### 4. Start using it
26
+
27
+ Ask Claude things like:
28
+ - "List my Opsee projects"
29
+ - "Show tasks in project Codlas"
30
+ - "Create a task called 'Fix login bug' in project 1"
31
+ - "What cycles are active in project 1?"
32
+ - "Show me the docs in project 1"
33
+
34
+ ## Available Tools (19)
35
+
36
+ | Tool | Description |
37
+ |------|-------------|
38
+ | `opsee_get_me` | Get your profile info |
39
+ | `opsee_list_projects` | List all projects |
40
+ | `opsee_get_project` | Get project details |
41
+ | `opsee_list_tasks` | List tasks with filters |
42
+ | `opsee_get_task` | Get task details |
43
+ | `opsee_create_task` | Create a new task |
44
+ | `opsee_update_task` | Update task fields |
45
+ | `opsee_list_task_types` | Get task types (Bug, Feature, etc.) |
46
+ | `opsee_list_task_priorities` | Get priority levels |
47
+ | `opsee_list_boards` | List Kanban boards |
48
+ | `opsee_list_board_columns` | Get board columns/statuses |
49
+ | `opsee_list_cycles` | List cycles/sprints |
50
+ | `opsee_get_cycle` | Get cycle details |
51
+ | `opsee_create_cycle` | Create a new cycle |
52
+ | `opsee_list_doc_spaces` | List doc spaces |
53
+ | `opsee_list_doc_pages` | List pages in a doc space |
54
+ | `opsee_get_doc_page` | Read a doc page |
55
+ | `opsee_create_doc_page` | Create a new doc page |
56
+ | `opsee_list_repositories` | List connected repositories |
57
+
58
+ ## Cursor / Other MCP Clients
59
+
60
+ Add this to your MCP client config:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "opsee": {
66
+ "command": "npx",
67
+ "args": ["@opsee/mcp-server", "serve"]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## CI / Headless Environments
74
+
75
+ Skip the browser login by setting a token directly:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "opsee": {
81
+ "command": "npx",
82
+ "args": ["@opsee/mcp-server", "serve"],
83
+ "env": {
84
+ "OPSEE_API_TOKEN": "your-jwt-token"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ ## Environment Variables
92
+
93
+ | Variable | Description | Default |
94
+ |----------|-------------|---------|
95
+ | `OPSEE_API_URL` | Backend gRPC API URL | `https://grpc.api.opsee.ai` |
96
+ | `OPSEE_APP_URL` | Frontend URL (for login) | `https://opsee.ai` |
97
+ | `OPSEE_API_TOKEN` | Direct JWT token (bypasses login) | — |
98
+ | `OPSEE_CREDENTIALS_PATH` | Override credential file path | `~/.opsee/credentials.json` |
99
+
100
+ ## Local Development
101
+
102
+ ```bash
103
+ # With local backend + frontend running:
104
+ OPSEE_APP_URL=http://localhost:5173 OPSEE_API_URL=http://localhost:9990 npx @opsee/mcp-server login
105
+
106
+ # Then add to Claude Code:
107
+ claude mcp add opsee -e OPSEE_API_URL=http://localhost:9990 -- npx @opsee/mcp-server serve
108
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opsee/mcp-server",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Opsee MCP server — manage projects, tasks, docs, and cycles from AI coding environments",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,8 @@
11
11
  "start": "npx tsx src/index.ts",
12
12
  "proto-local": "rm -rf ./gen && cd .. && buf build -o mcp/descriptor.bin && cd mcp && buf generate --include-imports descriptor.bin && rm descriptor.bin",
13
13
  "proto": "rm -rf ./gen && curl -k 'https://general.storage.codilas.link/opsee/proto/descriptor.bin' --output ./descriptor.bin && buf generate --include-imports descriptor.bin && rm ./descriptor.bin",
14
- "lint": "tsc --noEmit"
14
+ "lint": "tsc --noEmit",
15
+ "test": "vitest run"
15
16
  },
16
17
  "dependencies": {
17
18
  "@bufbuild/protobuf": "^2.2.3",
@@ -24,7 +25,8 @@
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^22.13.0",
27
- "typescript": "^5.7.0"
28
+ "typescript": "^5.7.0",
29
+ "vitest": "^4.1.0"
28
30
  },
29
31
  "engines": {
30
32
  "node": ">=18.0.0"
@@ -0,0 +1,465 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
4
+ import { createServer } from "../server.js";
5
+ import type { ApiClients } from "../client/api.js";
6
+
7
+ // --- Mock data ---
8
+
9
+ const mockProject = {
10
+ id: 1,
11
+ name: "Test Project",
12
+ shortName: "TP",
13
+ slug: "test-project",
14
+ description: "A test project",
15
+ isActive: true,
16
+ };
17
+
18
+ const mockTask = {
19
+ id: 10,
20
+ identifier: "TP-1",
21
+ title: "Fix the bug",
22
+ description: "Something is broken",
23
+ displayOrder: 0,
24
+ storyPoints: 3,
25
+ aiModeEnabled: false,
26
+ boardColumn: { id: 1, name: "To Do" },
27
+ taskPriority: { id: 2, name: "High" },
28
+ taskType: { id: 1, name: "Bug" },
29
+ assignedUser: { id: 5, fullName: "Alice" },
30
+ cycle: { id: 1, name: "Sprint 1" },
31
+ reporterUser: { id: 5 },
32
+ board: { id: 1 },
33
+ project: { id: 1 },
34
+ taskRepositories: [],
35
+ };
36
+
37
+ const mockUser = {
38
+ id: 5,
39
+ fullName: "Alice Test",
40
+ email: "alice@test.com",
41
+ companyId: 1,
42
+ company: { name: "TestCo" },
43
+ flattenedRoles: ["User"],
44
+ };
45
+
46
+ const mockBoard = { id: 1, name: "Main Board", isActive: true };
47
+ const mockBoardColumn = {
48
+ id: 1,
49
+ name: "To Do",
50
+ displayOrder: 0,
51
+ lifecycleState: "active",
52
+ };
53
+
54
+ const mockCycle = {
55
+ id: 1,
56
+ name: "Sprint 1",
57
+ description: "First sprint",
58
+ goal: "Ship v1",
59
+ startDate: "2026-03-01",
60
+ endDate: "2026-03-15",
61
+ isActive: true,
62
+ };
63
+
64
+ const mockDocSpace = {
65
+ id: 1,
66
+ title: "Engineering Docs",
67
+ description: "Technical documentation",
68
+ slug: "engineering-docs",
69
+ isDefault: true,
70
+ };
71
+
72
+ const mockDocPage = {
73
+ id: 1,
74
+ title: "Getting Started",
75
+ slug: "getting-started",
76
+ currentContent: "Welcome to the docs",
77
+ currentVersionNumber: 1,
78
+ createdByUser: { fullName: "Alice" },
79
+ };
80
+
81
+ const mockTaskType = { id: 1, name: "Bug" };
82
+ const mockTaskPriority = { id: 2, name: "High", level: 2 };
83
+ const mockVcsIntegration = {
84
+ id: 1,
85
+ provider: 2,
86
+ repoOwner: "acme",
87
+ repoName: "backend",
88
+ defaultBranch: "main",
89
+ isActive: true,
90
+ };
91
+
92
+ // --- Mock client factory ---
93
+
94
+ function createMockClients(): ApiClients {
95
+ return {
96
+ projects: {
97
+ getProjects: async () => ({ projects: [mockProject] }),
98
+ getProject: async () => ({ project: mockProject }),
99
+ },
100
+ tasks: {
101
+ getTasks: async () => ({ tasks: [mockTask] }),
102
+ getTask: async () => ({ task: mockTask }),
103
+ addTask: async () => ({ task: mockTask }),
104
+ editTask: async () => ({ task: { ...mockTask, title: "Updated bug" } }),
105
+ },
106
+ boards: {
107
+ getBoards: async () => ({ boards: [mockBoard] }),
108
+ },
109
+ boardColumns: {
110
+ getBoardColumns: async () => ({ boardColumns: [mockBoardColumn] }),
111
+ },
112
+ cycles: {
113
+ getCycles: async () => ({ cycles: [mockCycle] }),
114
+ getCycle: async () => ({ cycle: mockCycle }),
115
+ addCycle: async () => ({ cycle: mockCycle }),
116
+ },
117
+ docSpaces: {
118
+ getDocSpaces: async () => ({ docSpaces: [mockDocSpace] }),
119
+ },
120
+ docPages: {
121
+ getDocPages: async () => ({ docPages: [mockDocPage] }),
122
+ getDocPage: async () => ({ docPage: mockDocPage }),
123
+ addDocPage: async () => ({ docPage: mockDocPage }),
124
+ },
125
+ taskTypes: {
126
+ getTaskTypes: async () => ({ taskTypes: [mockTaskType] }),
127
+ },
128
+ taskPriorities: {
129
+ getTaskPriorities: async () => ({
130
+ taskPriorities: [mockTaskPriority],
131
+ }),
132
+ },
133
+ users: {
134
+ getMe: async () => ({ user: mockUser }),
135
+ },
136
+ vcsIntegrations: {
137
+ getVCSIntegrations: async () => ({
138
+ vcsIntegrations: [mockVcsIntegration],
139
+ }),
140
+ },
141
+ } as unknown as ApiClients;
142
+ }
143
+
144
+ // --- Test suite ---
145
+
146
+ describe("MCP Tools", () => {
147
+ let client: Client;
148
+ let cleanup: () => Promise<void>;
149
+
150
+ beforeAll(async () => {
151
+ const [clientTransport, serverTransport] =
152
+ InMemoryTransport.createLinkedPair();
153
+ const server = createServer(() => createMockClients());
154
+ await server.connect(serverTransport);
155
+
156
+ client = new Client({ name: "test-client", version: "1.0.0" });
157
+ await client.connect(clientTransport);
158
+
159
+ cleanup = async () => {
160
+ await client.close();
161
+ await server.close();
162
+ };
163
+ });
164
+
165
+ afterAll(async () => {
166
+ await cleanup();
167
+ });
168
+
169
+ // --- Project tools ---
170
+
171
+ test("opsee_list_projects returns projects", async () => {
172
+ const result = await client.callTool({
173
+ name: "opsee_list_projects",
174
+ arguments: {},
175
+ });
176
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
177
+ .text;
178
+ expect(text).toContain("Test Project");
179
+ expect(text).toContain("TP");
180
+ expect(text).toContain("1 project(s)");
181
+ });
182
+
183
+ test("opsee_get_project returns project details", async () => {
184
+ const result = await client.callTool({
185
+ name: "opsee_get_project",
186
+ arguments: { projectId: 1 },
187
+ });
188
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
189
+ .text;
190
+ expect(text).toContain("Test Project");
191
+ expect(text).toContain("test-project");
192
+ expect(text).toContain("ID: 1");
193
+ });
194
+
195
+ // --- Task tools ---
196
+
197
+ test("opsee_list_tasks returns tasks for a project", async () => {
198
+ const result = await client.callTool({
199
+ name: "opsee_list_tasks",
200
+ arguments: { projectId: 1 },
201
+ });
202
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
203
+ .text;
204
+ expect(text).toContain("TP-1");
205
+ expect(text).toContain("Fix the bug");
206
+ expect(text).toContain("1 task(s)");
207
+ });
208
+
209
+ test("opsee_get_task returns task details", async () => {
210
+ const result = await client.callTool({
211
+ name: "opsee_get_task",
212
+ arguments: { taskId: 10 },
213
+ });
214
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
215
+ .text;
216
+ expect(text).toContain("TP-1");
217
+ expect(text).toContain("Fix the bug");
218
+ expect(text).toContain("High");
219
+ expect(text).toContain("Bug");
220
+ });
221
+
222
+ test("opsee_create_task creates a task with defaults", async () => {
223
+ const result = await client.callTool({
224
+ name: "opsee_create_task",
225
+ arguments: { projectId: 1, title: "New task" },
226
+ });
227
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
228
+ .text;
229
+ expect(text).toContain("Task created");
230
+ expect(text).toContain("TP-1");
231
+ });
232
+
233
+ test("opsee_update_task does read-modify-write", async () => {
234
+ const result = await client.callTool({
235
+ name: "opsee_update_task",
236
+ arguments: { taskId: 10, title: "Updated title" },
237
+ });
238
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
239
+ .text;
240
+ expect(text).toContain("Task updated");
241
+ });
242
+
243
+ // --- User tools ---
244
+
245
+ test("opsee_get_me returns current user", async () => {
246
+ const result = await client.callTool({
247
+ name: "opsee_get_me",
248
+ arguments: {},
249
+ });
250
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
251
+ .text;
252
+ expect(text).toContain("Alice Test");
253
+ expect(text).toContain("alice@test.com");
254
+ expect(text).toContain("TestCo");
255
+ });
256
+
257
+ // --- Board/metadata tools ---
258
+
259
+ test("opsee_list_boards returns boards", async () => {
260
+ const result = await client.callTool({
261
+ name: "opsee_list_boards",
262
+ arguments: { projectId: 1 },
263
+ });
264
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
265
+ .text;
266
+ expect(text).toContain("Main Board");
267
+ expect(text).toContain("ID: 1");
268
+ });
269
+
270
+ test("opsee_list_board_columns returns columns", async () => {
271
+ const result = await client.callTool({
272
+ name: "opsee_list_board_columns",
273
+ arguments: { boardId: 1 },
274
+ });
275
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
276
+ .text;
277
+ expect(text).toContain("To Do");
278
+ expect(text).toContain("ID: 1");
279
+ });
280
+
281
+ test("opsee_list_task_types returns types", async () => {
282
+ const result = await client.callTool({
283
+ name: "opsee_list_task_types",
284
+ arguments: { projectId: 1 },
285
+ });
286
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
287
+ .text;
288
+ expect(text).toContain("Bug");
289
+ expect(text).toContain("ID: 1");
290
+ });
291
+
292
+ test("opsee_list_task_priorities returns priorities", async () => {
293
+ const result = await client.callTool({
294
+ name: "opsee_list_task_priorities",
295
+ arguments: { projectId: 1 },
296
+ });
297
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
298
+ .text;
299
+ expect(text).toContain("High");
300
+ expect(text).toContain("level: 2");
301
+ });
302
+
303
+ // --- Cycle tools ---
304
+
305
+ test("opsee_list_cycles returns cycles", async () => {
306
+ const result = await client.callTool({
307
+ name: "opsee_list_cycles",
308
+ arguments: { projectId: 1 },
309
+ });
310
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
311
+ .text;
312
+ expect(text).toContain("Sprint 1");
313
+ expect(text).toContain("1 cycle(s)");
314
+ });
315
+
316
+ test("opsee_get_cycle returns cycle details", async () => {
317
+ const result = await client.callTool({
318
+ name: "opsee_get_cycle",
319
+ arguments: { cycleId: 1 },
320
+ });
321
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
322
+ .text;
323
+ expect(text).toContain("Sprint 1");
324
+ expect(text).toContain("Ship v1");
325
+ expect(text).toContain("ID: 1");
326
+ });
327
+
328
+ test("opsee_create_cycle creates a cycle", async () => {
329
+ const result = await client.callTool({
330
+ name: "opsee_create_cycle",
331
+ arguments: {
332
+ projectId: 1,
333
+ name: "Sprint 2",
334
+ startDate: "2026-03-25",
335
+ endDate: "2026-04-08",
336
+ },
337
+ });
338
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
339
+ .text;
340
+ expect(text).toContain("Cycle created");
341
+ expect(text).toContain("Sprint 1");
342
+ });
343
+
344
+ // --- Doc tools ---
345
+
346
+ test("opsee_list_doc_spaces returns spaces", async () => {
347
+ const result = await client.callTool({
348
+ name: "opsee_list_doc_spaces",
349
+ arguments: { projectId: 1 },
350
+ });
351
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
352
+ .text;
353
+ expect(text).toContain("Engineering Docs");
354
+ expect(text).toContain("1 doc space(s)");
355
+ });
356
+
357
+ test("opsee_list_doc_pages returns pages", async () => {
358
+ const result = await client.callTool({
359
+ name: "opsee_list_doc_pages",
360
+ arguments: { docSpaceId: 1 },
361
+ });
362
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
363
+ .text;
364
+ expect(text).toContain("Getting Started");
365
+ });
366
+
367
+ test("opsee_get_doc_page returns page content", async () => {
368
+ const result = await client.callTool({
369
+ name: "opsee_get_doc_page",
370
+ arguments: { pageId: 1 },
371
+ });
372
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
373
+ .text;
374
+ expect(text).toContain("Getting Started");
375
+ expect(text).toContain("Welcome to the docs");
376
+ });
377
+
378
+ test("opsee_create_doc_page creates a page", async () => {
379
+ const result = await client.callTool({
380
+ name: "opsee_create_doc_page",
381
+ arguments: {
382
+ projectId: 1,
383
+ title: "New Page",
384
+ content: "Hello world",
385
+ },
386
+ });
387
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
388
+ .text;
389
+ expect(text).toContain("Doc page created");
390
+ expect(text).toContain("Getting Started");
391
+ });
392
+
393
+ // --- Repository tools ---
394
+
395
+ test("opsee_list_repositories returns connected repos", async () => {
396
+ const result = await client.callTool({
397
+ name: "opsee_list_repositories",
398
+ arguments: { projectId: 1 },
399
+ });
400
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
401
+ .text;
402
+ expect(text).toContain("acme/backend");
403
+ expect(text).toContain("GitLab");
404
+ });
405
+
406
+ // --- Error handling ---
407
+
408
+ test("tool returns isError on API failure", async () => {
409
+ const errorClients = createMockClients();
410
+ (errorClients.projects as any).getProjects = async () => {
411
+ throw new Error("connection refused ECONNREFUSED");
412
+ };
413
+
414
+ const [ct, st] = InMemoryTransport.createLinkedPair();
415
+ const errorServer = createServer(() => errorClients);
416
+ await errorServer.connect(st);
417
+
418
+ const errorClient = new Client({
419
+ name: "error-test",
420
+ version: "1.0.0",
421
+ });
422
+ await errorClient.connect(ct);
423
+
424
+ const result = await errorClient.callTool({
425
+ name: "opsee_list_projects",
426
+ arguments: {},
427
+ });
428
+
429
+ expect(result.isError).toBe(true);
430
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
431
+ .text;
432
+ expect(text).toContain("ECONNREFUSED");
433
+
434
+ await errorClient.close();
435
+ await errorServer.close();
436
+ });
437
+
438
+ test("auth error returns login instruction", async () => {
439
+ const authClients = createMockClients();
440
+ (authClients.users as any).getMe = async () => {
441
+ throw new Error("Unauthenticated");
442
+ };
443
+
444
+ const [ct, st] = InMemoryTransport.createLinkedPair();
445
+ const authServer = createServer(() => authClients);
446
+ await authServer.connect(st);
447
+
448
+ const authClient = new Client({ name: "auth-test", version: "1.0.0" });
449
+ await authClient.connect(ct);
450
+
451
+ const result = await authClient.callTool({
452
+ name: "opsee_get_me",
453
+ arguments: {},
454
+ });
455
+
456
+ expect(result.isError).toBe(true);
457
+ const text = (result.content as Array<{ type: string; text: string }>)[0]
458
+ .text;
459
+ expect(text).toContain("Not authenticated");
460
+ expect(text).toContain("login");
461
+
462
+ await authClient.close();
463
+ await authServer.close();
464
+ });
465
+ });
@@ -22,7 +22,7 @@ export function readCredentials(): Credentials | null {
22
22
  if (process.env.OPSEE_API_TOKEN) {
23
23
  return {
24
24
  token: process.env.OPSEE_API_TOKEN,
25
- serverUrl: process.env.OPSEE_API_URL || "https://api.opsee.dev",
25
+ serverUrl: process.env.OPSEE_API_URL || "https://grpc.api.opsee.ai",
26
26
  userId: "",
27
27
  companyId: "",
28
28
  expiresAt: "",
package/src/auth/login.ts CHANGED
@@ -27,7 +27,7 @@ const ERROR_HTML = `<!DOCTYPE html>
27
27
  export async function startLoginFlow(): Promise<void> {
28
28
  const port = 9876;
29
29
  const callbackUrl = `http://localhost:${port}/callback`;
30
- const appUrl = process.env.OPSEE_APP_URL || "https://app.opsee.dev";
30
+ const appUrl = process.env.OPSEE_APP_URL || "https://opsee.ai";
31
31
  const loginUrl = `${appUrl}/auth/mcp?callback=${encodeURIComponent(callbackUrl)}`;
32
32
 
33
33
  return new Promise<void>((resolve, reject) => {
@@ -18,7 +18,7 @@ export class AuthManager {
18
18
  return (
19
19
  process.env.OPSEE_API_URL ||
20
20
  this.credentials?.serverUrl ||
21
- "https://api.opsee.dev"
21
+ "https://grpc.api.opsee.ai"
22
22
  );
23
23
  }
24
24
 
package/src/client/api.ts CHANGED
@@ -59,33 +59,54 @@ export function defaultPagination(
59
59
  });
60
60
  }
61
61
 
62
- function createTransport() {
63
- return createConnectTransport({
64
- baseUrl: authManager.getServerUrl(),
65
- httpVersion: "1.1",
66
- interceptors: [authInterceptor, paginationInterceptor],
67
- });
62
+ let transport: ReturnType<typeof createConnectTransport> | null = null;
63
+ let cachedClients: ApiClients | null = null;
64
+
65
+ function getTransport() {
66
+ if (!transport) {
67
+ transport = createConnectTransport({
68
+ baseUrl: authManager.getServerUrl(),
69
+ httpVersion: "1.1",
70
+ interceptors: [authInterceptor, paginationInterceptor],
71
+ });
72
+ }
73
+ return transport;
68
74
  }
69
75
 
70
76
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
77
  function makeClient<T extends GenService<any>>(service: T): Client<T> {
72
- return createClient(service, createTransport());
78
+ return createClient(service, getTransport());
73
79
  }
74
80
 
75
- export function getClients() {
76
- return {
77
- projects: makeClient(ProjectService),
78
- tasks: makeClient(TaskService),
79
- boards: makeClient(BoardService),
80
- boardColumns: makeClient(BoardColumnService),
81
- cycles: makeClient(CycleService),
82
- docSpaces: makeClient(DocSpaceService),
83
- docPages: makeClient(DocPageService),
84
- taskTypes: makeClient(TaskTypeService),
85
- taskPriorities: makeClient(TaskPriorityService),
86
- users: makeClient(UserService),
87
- vcsIntegrations: makeClient(VCSIntegrationService),
88
- };
89
- }
81
+ export type ApiClients = {
82
+ projects: Client<typeof ProjectService>;
83
+ tasks: Client<typeof TaskService>;
84
+ boards: Client<typeof BoardService>;
85
+ boardColumns: Client<typeof BoardColumnService>;
86
+ cycles: Client<typeof CycleService>;
87
+ docSpaces: Client<typeof DocSpaceService>;
88
+ docPages: Client<typeof DocPageService>;
89
+ taskTypes: Client<typeof TaskTypeService>;
90
+ taskPriorities: Client<typeof TaskPriorityService>;
91
+ users: Client<typeof UserService>;
92
+ vcsIntegrations: Client<typeof VCSIntegrationService>;
93
+ };
90
94
 
91
- export type ApiClients = ReturnType<typeof getClients>;
95
+ export function getClients(): ApiClients {
96
+ if (!cachedClients) {
97
+ cachedClients = {
98
+ projects: makeClient(ProjectService),
99
+ tasks: makeClient(TaskService),
100
+ boards: makeClient(BoardService),
101
+ boardColumns: makeClient(BoardColumnService),
102
+ cycles: makeClient(CycleService),
103
+ docSpaces: makeClient(DocSpaceService),
104
+ docPages: makeClient(DocPageService),
105
+ taskTypes: makeClient(TaskTypeService),
106
+ taskPriorities: makeClient(TaskPriorityService),
107
+ users: makeClient(UserService),
108
+ vcsIntegrations: makeClient(VCSIntegrationService),
109
+ };
110
+ }
111
+ return cachedClients;
112
+ }
package/src/server.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { getClients } from "./client/api.js";
2
+ import { getClients, type ApiClients } from "./client/api.js";
3
3
  import { registerUserTools } from "./tools/user.js";
4
4
  import { registerProjectTools } from "./tools/projects.js";
5
5
  import { registerTaskTools } from "./tools/tasks.js";
@@ -8,19 +8,21 @@ import { registerCycleTools } from "./tools/cycles.js";
8
8
  import { registerDocTools } from "./tools/docs.js";
9
9
  import { registerRepositoryTools } from "./tools/repositories.js";
10
10
 
11
- export function createServer(): McpServer {
11
+ export function createServer(clientFactory?: () => ApiClients): McpServer {
12
+ const factory = clientFactory ?? getClients;
13
+
12
14
  const server = new McpServer({
13
15
  name: "opsee",
14
16
  version: "0.1.0",
15
17
  });
16
18
 
17
- registerUserTools(server, getClients);
18
- registerProjectTools(server, getClients);
19
- registerTaskTools(server, getClients);
20
- registerTaskMetadataTools(server, getClients);
21
- registerCycleTools(server, getClients);
22
- registerDocTools(server, getClients);
23
- registerRepositoryTools(server, getClients);
19
+ registerUserTools(server, factory);
20
+ registerProjectTools(server, factory);
21
+ registerTaskTools(server, factory);
22
+ registerTaskMetadataTools(server, factory);
23
+ registerCycleTools(server, factory);
24
+ registerDocTools(server, factory);
25
+ registerRepositoryTools(server, factory);
24
26
 
25
27
  return server;
26
28
  }
@@ -126,16 +126,16 @@ export function formatDocPageList(pages: DocPage[]): string {
126
126
 
127
127
  export function formatError(error: unknown): string {
128
128
  if (error instanceof Error) {
129
- if (error.message.includes("401") || error.message.includes("Unauthenticated")) {
130
- return "Not authenticated. Run `npx opsee-mcp login` to connect your Opsee account.";
129
+ const msg = error.message;
130
+ const code = (error as any).code;
131
+ if (msg.includes("401") || msg.includes("Unauthenticated") || code === "unauthenticated") {
132
+ return "Not authenticated. Run `npx @opsee/mcp-server login` to connect your Opsee account.";
131
133
  }
132
- if (error.message.includes("not found") || error.message.includes("404")) {
133
- return `Not found. ${error.message}`;
134
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND")) {
135
+ return `Could not reach Opsee API. Check your connection and OPSEE_API_URL. (${msg})`;
134
136
  }
135
- if (error.message.includes("ECONNREFUSED") || error.message.includes("ENOTFOUND")) {
136
- return "Could not reach Opsee API. Check your connection and OPSEE_API_URL.";
137
- }
138
- return error.message;
137
+ // Show full error details for debugging
138
+ return code ? `[${code}] ${msg}` : msg;
139
139
  }
140
140
  return String(error);
141
141
  }