@opsee/mcp-server 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -0
- package/bin/opsee-mcp.js +9 -3
- package/package.json +9 -3
- package/src/__tests__/tools.test.ts +465 -0
- package/src/auth/oauth-provider.ts +254 -0
- package/src/auth/token-context.ts +11 -0
- package/src/client/api.ts +48 -24
- package/src/index-http.ts +6 -0
- package/src/server-http.ts +120 -0
- package/src/server.ts +11 -9
- package/src/tools/cycles.ts +3 -0
- package/src/tools/docs.ts +4 -0
- package/src/tools/projects.ts +2 -0
- package/src/tools/repositories.ts +1 -0
- package/src/tools/task-metadata.ts +4 -0
- package/src/tools/tasks.ts +4 -0
- package/src/tools/user.ts +1 -0
- package/src/utils/format.ts +8 -8
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
interface TokenContext {
|
|
4
|
+
token: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const tokenContext = new AsyncLocalStorage<TokenContext>();
|
|
8
|
+
|
|
9
|
+
export function getCurrentToken(): string | null {
|
|
10
|
+
return tokenContext.getStore()?.token ?? null;
|
|
11
|
+
}
|
package/src/client/api.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createConnectTransport } from "@connectrpc/connect-node";
|
|
|
3
3
|
import { create } from "@bufbuild/protobuf";
|
|
4
4
|
import type { GenService } from "@bufbuild/protobuf/codegenv1";
|
|
5
5
|
import { authManager } from "../auth/manager.js";
|
|
6
|
+
import { getCurrentToken } from "../auth/token-context.js";
|
|
6
7
|
import {
|
|
7
8
|
PaginationSchema,
|
|
8
9
|
type Pagination,
|
|
@@ -21,7 +22,9 @@ import { UserService } from "../../gen/api/v1/user_pb.js";
|
|
|
21
22
|
import { VCSIntegrationService } from "../../gen/api/v1/vcs_integration_pb.js";
|
|
22
23
|
|
|
23
24
|
const authInterceptor: Interceptor = (next) => async (req) => {
|
|
24
|
-
|
|
25
|
+
// Remote mode: token from AsyncLocalStorage (per-request)
|
|
26
|
+
// Local mode: token from credentials file
|
|
27
|
+
const token = getCurrentToken() || authManager.getToken();
|
|
25
28
|
if (token) {
|
|
26
29
|
req.header.set("Authorization", `Bearer ${token}`);
|
|
27
30
|
}
|
|
@@ -59,33 +62,54 @@ export function defaultPagination(
|
|
|
59
62
|
});
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
let transport: ReturnType<typeof createConnectTransport> | null = null;
|
|
66
|
+
let cachedClients: ApiClients | null = null;
|
|
67
|
+
|
|
68
|
+
function getTransport() {
|
|
69
|
+
if (!transport) {
|
|
70
|
+
transport = createConnectTransport({
|
|
71
|
+
baseUrl: authManager.getServerUrl(),
|
|
72
|
+
httpVersion: "1.1",
|
|
73
|
+
interceptors: [authInterceptor, paginationInterceptor],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return transport;
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
80
|
function makeClient<T extends GenService<any>>(service: T): Client<T> {
|
|
72
|
-
return createClient(service,
|
|
81
|
+
return createClient(service, getTransport());
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
export
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
};
|
|
89
|
-
}
|
|
84
|
+
export type ApiClients = {
|
|
85
|
+
projects: Client<typeof ProjectService>;
|
|
86
|
+
tasks: Client<typeof TaskService>;
|
|
87
|
+
boards: Client<typeof BoardService>;
|
|
88
|
+
boardColumns: Client<typeof BoardColumnService>;
|
|
89
|
+
cycles: Client<typeof CycleService>;
|
|
90
|
+
docSpaces: Client<typeof DocSpaceService>;
|
|
91
|
+
docPages: Client<typeof DocPageService>;
|
|
92
|
+
taskTypes: Client<typeof TaskTypeService>;
|
|
93
|
+
taskPriorities: Client<typeof TaskPriorityService>;
|
|
94
|
+
users: Client<typeof UserService>;
|
|
95
|
+
vcsIntegrations: Client<typeof VCSIntegrationService>;
|
|
96
|
+
};
|
|
90
97
|
|
|
91
|
-
export
|
|
98
|
+
export function getClients(): ApiClients {
|
|
99
|
+
if (!cachedClients) {
|
|
100
|
+
cachedClients = {
|
|
101
|
+
projects: makeClient(ProjectService),
|
|
102
|
+
tasks: makeClient(TaskService),
|
|
103
|
+
boards: makeClient(BoardService),
|
|
104
|
+
boardColumns: makeClient(BoardColumnService),
|
|
105
|
+
cycles: makeClient(CycleService),
|
|
106
|
+
docSpaces: makeClient(DocSpaceService),
|
|
107
|
+
docPages: makeClient(DocPageService),
|
|
108
|
+
taskTypes: makeClient(TaskTypeService),
|
|
109
|
+
taskPriorities: makeClient(TaskPriorityService),
|
|
110
|
+
users: makeClient(UserService),
|
|
111
|
+
vcsIntegrations: makeClient(VCSIntegrationService),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return cachedClients;
|
|
115
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { mcpAuthRouter, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
7
|
+
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
8
|
+
import { OpseeOAuthProvider } from "./auth/oauth-provider.js";
|
|
9
|
+
import { tokenContext } from "./auth/token-context.js";
|
|
10
|
+
import { createServer } from "./server.js";
|
|
11
|
+
|
|
12
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
13
|
+
<html>
|
|
14
|
+
<head><title>Opsee MCP - Connected</title></head>
|
|
15
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f8f9fa;">
|
|
16
|
+
<div style="text-align: center; padding: 2rem;">
|
|
17
|
+
<h1 style="color: #10b981;">Connected!</h1>
|
|
18
|
+
<p>Your Opsee account is now linked. You can close this window.</p>
|
|
19
|
+
</div>
|
|
20
|
+
</body>
|
|
21
|
+
</html>`;
|
|
22
|
+
|
|
23
|
+
export async function startHttpServer(): Promise<void> {
|
|
24
|
+
const port = parseInt(process.env.MCP_PORT || "3100", 10);
|
|
25
|
+
const host = process.env.MCP_HOST || "0.0.0.0";
|
|
26
|
+
const serverUrl =
|
|
27
|
+
process.env.MCP_SERVER_URL || `http://localhost:${port}`;
|
|
28
|
+
const backendUrl =
|
|
29
|
+
process.env.OPSEE_API_URL || "https://grpc.api.opsee.ai";
|
|
30
|
+
|
|
31
|
+
const provider = new OpseeOAuthProvider(serverUrl);
|
|
32
|
+
const issuerUrl = new URL(serverUrl);
|
|
33
|
+
|
|
34
|
+
// Periodically clean up expired auth entries
|
|
35
|
+
setInterval(() => provider.cleanup(), 60_000);
|
|
36
|
+
|
|
37
|
+
const app = express();
|
|
38
|
+
// Trust proxy headers (X-Forwarded-For) from nginx ingress
|
|
39
|
+
app.set("trust proxy", 1);
|
|
40
|
+
app.use(cors());
|
|
41
|
+
app.use(express.json());
|
|
42
|
+
|
|
43
|
+
// --- Health check ---
|
|
44
|
+
app.get("/health", (_req, res) => {
|
|
45
|
+
res.json({ status: "ok" });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// --- OAuth 2.0 endpoints (authorize, token, register, metadata) ---
|
|
49
|
+
app.use(
|
|
50
|
+
mcpAuthRouter({
|
|
51
|
+
provider,
|
|
52
|
+
issuerUrl,
|
|
53
|
+
serviceDocumentationUrl: new URL(
|
|
54
|
+
"https://github.com/ArtisanCloud/opsee",
|
|
55
|
+
),
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// --- Custom OAuth callback (Opsee login redirects here) ---
|
|
60
|
+
app.get("/oauth/callback", (req, res) => {
|
|
61
|
+
const { pending, token, userId, companyId, expiresAt } = req.query as Record<string, string>;
|
|
62
|
+
|
|
63
|
+
if (!pending || !token) {
|
|
64
|
+
res.status(400).send("Missing pending or token parameter");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = provider.handleCallback(
|
|
69
|
+
pending,
|
|
70
|
+
token,
|
|
71
|
+
userId || "",
|
|
72
|
+
companyId || "",
|
|
73
|
+
expiresAt || "",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if ("error" in result) {
|
|
77
|
+
res.status(400).send(result.error);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Redirect to Claude's redirect_uri with the auth code
|
|
82
|
+
res.redirect(result.redirectUri);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// --- Bearer auth middleware for MCP endpoints ---
|
|
86
|
+
const resourceMetadataUrl = getOAuthProtectedResourceMetadataUrl(new URL(serverUrl));
|
|
87
|
+
const authMiddleware = requireBearerAuth({
|
|
88
|
+
verifier: provider,
|
|
89
|
+
resourceMetadataUrl,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// --- MCP Streamable HTTP transport (stateless mode) ---
|
|
93
|
+
// Each request creates a fresh transport+server — no session persistence needed.
|
|
94
|
+
// This works reliably behind proxies/load balancers and with Claude Code's HTTP transport.
|
|
95
|
+
|
|
96
|
+
app.all("/mcp", authMiddleware, async (req, res) => {
|
|
97
|
+
// Extract the verified JWT from the auth middleware
|
|
98
|
+
const accessToken = req.auth?.token;
|
|
99
|
+
|
|
100
|
+
// Wrap the MCP handling in the token context so API calls use this user's JWT
|
|
101
|
+
await tokenContext.run({ token: accessToken || "" }, async () => {
|
|
102
|
+
const transport = new StreamableHTTPServerTransport({
|
|
103
|
+
sessionIdGenerator: undefined, // stateless — no session IDs
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const mcpServer = createServer();
|
|
107
|
+
await mcpServer.connect(transport);
|
|
108
|
+
await transport.handleRequest(req, res, req.body);
|
|
109
|
+
await transport.close();
|
|
110
|
+
await mcpServer.close();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.listen(port, host, () => {
|
|
115
|
+
console.log(`Opsee MCP server (remote) listening on ${host}:${port}`);
|
|
116
|
+
console.log(` MCP endpoint: ${serverUrl}/mcp`);
|
|
117
|
+
console.log(` OAuth authorize: ${serverUrl}/authorize`);
|
|
118
|
+
console.log(` Backend: ${backendUrl}`);
|
|
119
|
+
});
|
|
120
|
+
}
|
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,
|
|
18
|
-
registerProjectTools(server,
|
|
19
|
-
registerTaskTools(server,
|
|
20
|
-
registerTaskMetadataTools(server,
|
|
21
|
-
registerCycleTools(server,
|
|
22
|
-
registerDocTools(server,
|
|
23
|
-
registerRepositoryTools(server,
|
|
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
|
}
|
package/src/tools/cycles.ts
CHANGED
|
@@ -12,6 +12,7 @@ export function registerCycleTools(
|
|
|
12
12
|
"opsee_list_cycles",
|
|
13
13
|
"List cycles/sprints in an Opsee project.",
|
|
14
14
|
{ projectId: z.number().describe("The project ID") },
|
|
15
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
15
16
|
async ({ projectId }) => {
|
|
16
17
|
try {
|
|
17
18
|
const clients = getClients();
|
|
@@ -27,6 +28,7 @@ export function registerCycleTools(
|
|
|
27
28
|
"opsee_get_cycle",
|
|
28
29
|
"Get details of a specific cycle/sprint by ID.",
|
|
29
30
|
{ cycleId: z.number().describe("The cycle ID") },
|
|
31
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
30
32
|
async ({ cycleId }) => {
|
|
31
33
|
try {
|
|
32
34
|
const clients = getClients();
|
|
@@ -49,6 +51,7 @@ export function registerCycleTools(
|
|
|
49
51
|
endDate: z.string().describe("End date (ISO 8601, e.g. 2026-04-08)"),
|
|
50
52
|
description: z.string().optional().describe("Cycle description"),
|
|
51
53
|
},
|
|
54
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
52
55
|
async ({ projectId, name, startDate, endDate, description }) => {
|
|
53
56
|
try {
|
|
54
57
|
const clients = getClients();
|
package/src/tools/docs.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function registerDocTools(
|
|
|
29
29
|
"opsee_list_doc_spaces",
|
|
30
30
|
"List documentation spaces in an Opsee project.",
|
|
31
31
|
{ projectId: z.number().describe("The project ID") },
|
|
32
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
32
33
|
async ({ projectId }) => {
|
|
33
34
|
try {
|
|
34
35
|
const clients = getClients();
|
|
@@ -55,6 +56,7 @@ export function registerDocTools(
|
|
|
55
56
|
"opsee_list_doc_pages",
|
|
56
57
|
"List documentation pages in a doc space.",
|
|
57
58
|
{ docSpaceId: z.number().describe("The doc space ID") },
|
|
59
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
58
60
|
async ({ docSpaceId }) => {
|
|
59
61
|
try {
|
|
60
62
|
const clients = getClients();
|
|
@@ -81,6 +83,7 @@ export function registerDocTools(
|
|
|
81
83
|
"opsee_get_doc_page",
|
|
82
84
|
"Read a documentation page's content by ID.",
|
|
83
85
|
{ pageId: z.number().describe("The doc page ID") },
|
|
86
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
84
87
|
async ({ pageId }) => {
|
|
85
88
|
try {
|
|
86
89
|
const clients = getClients();
|
|
@@ -103,6 +106,7 @@ export function registerDocTools(
|
|
|
103
106
|
content: z.string().describe("Page content (text or JSON)"),
|
|
104
107
|
parentPageId: z.number().optional().describe("Parent page ID for nested pages"),
|
|
105
108
|
},
|
|
109
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
106
110
|
async ({ projectId, title, content, parentPageId }) => {
|
|
107
111
|
try {
|
|
108
112
|
const clients = getClients();
|
package/src/tools/projects.ts
CHANGED
|
@@ -11,6 +11,7 @@ export function registerProjectTools(
|
|
|
11
11
|
"opsee_list_projects",
|
|
12
12
|
"List all Opsee projects the authenticated user has access to.",
|
|
13
13
|
{},
|
|
14
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
14
15
|
async () => {
|
|
15
16
|
try {
|
|
16
17
|
const clients = getClients();
|
|
@@ -26,6 +27,7 @@ export function registerProjectTools(
|
|
|
26
27
|
"opsee_get_project",
|
|
27
28
|
"Get details of a specific Opsee project by ID.",
|
|
28
29
|
{ projectId: z.number().describe("The project ID") },
|
|
30
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
29
31
|
async ({ projectId }) => {
|
|
30
32
|
try {
|
|
31
33
|
const clients = getClients();
|
|
@@ -11,6 +11,7 @@ export function registerRepositoryTools(
|
|
|
11
11
|
"opsee_list_repositories",
|
|
12
12
|
"List connected VCS repositories (GitHub/GitLab) for an Opsee project.",
|
|
13
13
|
{ projectId: z.number().describe("The project ID") },
|
|
14
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
14
15
|
async ({ projectId }) => {
|
|
15
16
|
try {
|
|
16
17
|
const clients = getClients();
|
|
@@ -11,6 +11,7 @@ export function registerTaskMetadataTools(
|
|
|
11
11
|
"opsee_list_task_types",
|
|
12
12
|
"Get available task types (Bug, Feature, etc.) for an Opsee project. Use these IDs when creating or updating tasks.",
|
|
13
13
|
{ projectId: z.number().describe("The project ID") },
|
|
14
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
14
15
|
async ({ projectId }) => {
|
|
15
16
|
try {
|
|
16
17
|
const clients = getClients();
|
|
@@ -32,6 +33,7 @@ export function registerTaskMetadataTools(
|
|
|
32
33
|
"opsee_list_task_priorities",
|
|
33
34
|
"Get priority levels (Critical, High, Medium, Low, etc.) for an Opsee project. Use these IDs when creating or updating tasks.",
|
|
34
35
|
{ projectId: z.number().describe("The project ID") },
|
|
36
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
35
37
|
async ({ projectId }) => {
|
|
36
38
|
try {
|
|
37
39
|
const clients = getClients();
|
|
@@ -53,6 +55,7 @@ export function registerTaskMetadataTools(
|
|
|
53
55
|
"opsee_list_boards",
|
|
54
56
|
"List Kanban boards for an Opsee project. Use the board ID to list board columns.",
|
|
55
57
|
{ projectId: z.number().describe("The project ID") },
|
|
58
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
56
59
|
async ({ projectId }) => {
|
|
57
60
|
try {
|
|
58
61
|
const clients = getClients();
|
|
@@ -74,6 +77,7 @@ export function registerTaskMetadataTools(
|
|
|
74
77
|
"opsee_list_board_columns",
|
|
75
78
|
"Get board columns/statuses (To Do, In Progress, Done, etc.) for a board. Use these IDs when creating or updating tasks.",
|
|
76
79
|
{ boardId: z.number().describe("The board ID") },
|
|
80
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
77
81
|
async ({ boardId }) => {
|
|
78
82
|
try {
|
|
79
83
|
const clients = getClients();
|
package/src/tools/tasks.ts
CHANGED
|
@@ -25,6 +25,7 @@ export function registerTaskTools(
|
|
|
25
25
|
page: z.number().optional().describe("Page number (default: 1)"),
|
|
26
26
|
pageSize: z.number().optional().describe("Items per page (default: 50)"),
|
|
27
27
|
},
|
|
28
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
28
29
|
async ({ projectId, columnId, assigneeId, cycleId, page, pageSize }) => {
|
|
29
30
|
try {
|
|
30
31
|
const clients = getClients();
|
|
@@ -57,6 +58,7 @@ export function registerTaskTools(
|
|
|
57
58
|
"opsee_get_task",
|
|
58
59
|
"Get full details of a specific task by ID.",
|
|
59
60
|
{ taskId: z.number().describe("The task ID") },
|
|
61
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
60
62
|
async ({ taskId }) => {
|
|
61
63
|
try {
|
|
62
64
|
const clients = getClients();
|
|
@@ -82,6 +84,7 @@ export function registerTaskTools(
|
|
|
82
84
|
assigneeId: z.number().optional().describe("Assigned user ID"),
|
|
83
85
|
cycleId: z.number().optional().describe("Cycle/sprint ID"),
|
|
84
86
|
},
|
|
87
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
85
88
|
async ({ projectId, title, description, taskTypeId, priorityId, boardColumnId, assigneeId, cycleId }) => {
|
|
86
89
|
try {
|
|
87
90
|
const clients = getClients();
|
|
@@ -178,6 +181,7 @@ export function registerTaskTools(
|
|
|
178
181
|
assigneeId: z.number().optional().describe("New assigned user ID"),
|
|
179
182
|
cycleId: z.number().optional().describe("New cycle/sprint ID"),
|
|
180
183
|
},
|
|
184
|
+
{ readOnlyHint: false, destructiveHint: false },
|
|
181
185
|
async ({ taskId, title, description, taskTypeId, priorityId, boardColumnId, assigneeId, cycleId }) => {
|
|
182
186
|
try {
|
|
183
187
|
const clients = getClients();
|
package/src/tools/user.ts
CHANGED
package/src/utils/format.ts
CHANGED
|
@@ -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
|
-
|
|
130
|
-
|
|
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 (
|
|
133
|
-
return `
|
|
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
|
-
|
|
136
|
-
|
|
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
|
}
|