@opsee/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/bin/opsee-mcp.js +21 -0
- package/gen/api/v1/account_pb.d.ts +393 -0
- package/gen/api/v1/account_pb.js +111 -0
- package/gen/api/v1/activity_log_pb.d.ts +297 -0
- package/gen/api/v1/activity_log_pb.js +89 -0
- package/gen/api/v1/ai_run_status_pb.d.ts +237 -0
- package/gen/api/v1/ai_run_status_pb.js +89 -0
- package/gen/api/v1/ai_unit_category_pb.d.ts +237 -0
- package/gen/api/v1/ai_unit_category_pb.js +89 -0
- package/gen/api/v1/ai_unit_stage_pb.d.ts +347 -0
- package/gen/api/v1/ai_unit_stage_pb.js +117 -0
- package/gen/api/v1/ai_unit_status_pb.d.ts +237 -0
- package/gen/api/v1/ai_unit_status_pb.js +89 -0
- package/gen/api/v1/ai_workflow_run_pb.d.ts +542 -0
- package/gen/api/v1/ai_workflow_run_pb.js +159 -0
- package/gen/api/v1/ai_workflow_unit_pb.d.ts +431 -0
- package/gen/api/v1/ai_workflow_unit_pb.js +103 -0
- package/gen/api/v1/attachment_pb.d.ts +297 -0
- package/gen/api/v1/attachment_pb.js +89 -0
- package/gen/api/v1/billing_pb.d.ts +1206 -0
- package/gen/api/v1/billing_pb.js +288 -0
- package/gen/api/v1/board_column_pb.d.ts +282 -0
- package/gen/api/v1/board_column_pb.js +89 -0
- package/gen/api/v1/board_pb.d.ts +296 -0
- package/gen/api/v1/board_pb.js +96 -0
- package/gen/api/v1/comment_pb.d.ts +277 -0
- package/gen/api/v1/comment_pb.js +89 -0
- package/gen/api/v1/company_pb.d.ts +452 -0
- package/gen/api/v1/company_pb.js +110 -0
- package/gen/api/v1/component_pb.d.ts +267 -0
- package/gen/api/v1/component_pb.js +89 -0
- package/gen/api/v1/contact_pb.d.ts +58 -0
- package/gen/api/v1/contact_pb.js +28 -0
- package/gen/api/v1/country_pb.d.ts +257 -0
- package/gen/api/v1/country_pb.js +89 -0
- package/gen/api/v1/credit_transaction_pb.d.ts +277 -0
- package/gen/api/v1/credit_transaction_pb.js +89 -0
- package/gen/api/v1/cycle_pb.d.ts +281 -0
- package/gen/api/v1/cycle_pb.js +87 -0
- package/gen/api/v1/deployment_pb.d.ts +277 -0
- package/gen/api/v1/deployment_pb.js +89 -0
- package/gen/api/v1/doc_page_pb.d.ts +505 -0
- package/gen/api/v1/doc_page_pb.js +138 -0
- package/gen/api/v1/doc_page_version_pb.d.ts +287 -0
- package/gen/api/v1/doc_page_version_pb.js +89 -0
- package/gen/api/v1/doc_space_pb.d.ts +297 -0
- package/gen/api/v1/doc_space_pb.js +89 -0
- package/gen/api/v1/document_link_pb.d.ts +174 -0
- package/gen/api/v1/document_link_pb.js +61 -0
- package/gen/api/v1/document_pb.d.ts +548 -0
- package/gen/api/v1/document_pb.js +152 -0
- package/gen/api/v1/environment_pb.d.ts +277 -0
- package/gen/api/v1/environment_pb.js +89 -0
- package/gen/api/v1/filter_pb.d.ts +66 -0
- package/gen/api/v1/filter_pb.js +26 -0
- package/gen/api/v1/label_pb.d.ts +267 -0
- package/gen/api/v1/label_pb.js +89 -0
- package/gen/api/v1/models_pb.d.ts +3137 -0
- package/gen/api/v1/models_pb.js +357 -0
- package/gen/api/v1/notification_pb.d.ts +317 -0
- package/gen/api/v1/notification_pb.js +89 -0
- package/gen/api/v1/orchestrator_pb.d.ts +1813 -0
- package/gen/api/v1/orchestrator_pb.js +353 -0
- package/gen/api/v1/pagination_pb.d.ts +92 -0
- package/gen/api/v1/pagination_pb.js +33 -0
- package/gen/api/v1/permission_pb.d.ts +222 -0
- package/gen/api/v1/permission_pb.js +89 -0
- package/gen/api/v1/preference_pb.d.ts +109 -0
- package/gen/api/v1/preference_pb.js +42 -0
- package/gen/api/v1/project_membership_pb.d.ts +321 -0
- package/gen/api/v1/project_membership_pb.js +103 -0
- package/gen/api/v1/project_pb.d.ts +473 -0
- package/gen/api/v1/project_pb.js +145 -0
- package/gen/api/v1/reporting_pb.d.ts +1481 -0
- package/gen/api/v1/reporting_pb.js +373 -0
- package/gen/api/v1/role_pb.d.ts +252 -0
- package/gen/api/v1/role_pb.js +101 -0
- package/gen/api/v1/subscription_pb.d.ts +307 -0
- package/gen/api/v1/subscription_pb.js +89 -0
- package/gen/api/v1/swagger_pb.d.ts +11 -0
- package/gen/api/v1/swagger_pb.js +13 -0
- package/gen/api/v1/task_component_pb.d.ts +242 -0
- package/gen/api/v1/task_component_pb.js +89 -0
- package/gen/api/v1/task_contributor_pb.d.ts +252 -0
- package/gen/api/v1/task_contributor_pb.js +89 -0
- package/gen/api/v1/task_doc_page_pb.d.ts +186 -0
- package/gen/api/v1/task_doc_page_pb.js +73 -0
- package/gen/api/v1/task_document_pb.d.ts +186 -0
- package/gen/api/v1/task_document_pb.js +73 -0
- package/gen/api/v1/task_label_pb.d.ts +242 -0
- package/gen/api/v1/task_label_pb.js +89 -0
- package/gen/api/v1/task_pb.d.ts +780 -0
- package/gen/api/v1/task_pb.js +173 -0
- package/gen/api/v1/task_priority_pb.d.ts +267 -0
- package/gen/api/v1/task_priority_pb.js +89 -0
- package/gen/api/v1/task_repository_pb.d.ts +242 -0
- package/gen/api/v1/task_repository_pb.js +89 -0
- package/gen/api/v1/task_type_pb.d.ts +277 -0
- package/gen/api/v1/task_type_pb.js +89 -0
- package/gen/api/v1/test_run_pb.d.ts +277 -0
- package/gen/api/v1/test_run_pb.js +89 -0
- package/gen/api/v1/user_pb.d.ts +366 -0
- package/gen/api/v1/user_pb.js +117 -0
- package/gen/api/v1/vcs_connection_pb.d.ts +964 -0
- package/gen/api/v1/vcs_connection_pb.js +228 -0
- package/gen/api/v1/vcs_integration_pb.d.ts +479 -0
- package/gen/api/v1/vcs_integration_pb.js +125 -0
- package/gen/api/v1/vcs_pb.d.ts +36 -0
- package/gen/api/v1/vcs_pb.js +24 -0
- package/gen/api/v1/view_pb.d.ts +337 -0
- package/gen/api/v1/view_pb.js +102 -0
- package/gen/google/api/annotations_pb.d.ts +34 -0
- package/gen/google/api/annotations_pb.js +36 -0
- package/gen/google/api/http_pb.d.ts +477 -0
- package/gen/google/api/http_pb.js +47 -0
- package/gen/protoc-gen-openapiv2/options/annotations_pb.d.ts +63 -0
- package/gen/protoc-gen-openapiv2/options/annotations_pb.js +69 -0
- package/gen/protoc-gen-openapiv2/options/openapiv2_pb.d.ts +1497 -0
- package/gen/protoc-gen-openapiv2/options/openapiv2_pb.js +232 -0
- package/gen/validate/validate_pb.d.ts +1953 -0
- package/gen/validate/validate_pb.js +223 -0
- package/package.json +37 -0
- package/src/auth/credentials.ts +65 -0
- package/src/auth/login.ts +96 -0
- package/src/auth/manager.ts +49 -0
- package/src/client/api.ts +91 -0
- package/src/index.ts +13 -0
- package/src/server.ts +26 -0
- package/src/tools/cycles.ts +77 -0
- package/src/tools/docs.ts +148 -0
- package/src/tools/projects.ts +40 -0
- package/src/tools/repositories.ts +41 -0
- package/src/tools/task-metadata.ts +96 -0
- package/src/tools/tasks.ts +228 -0
- package/src/tools/user.ts +33 -0
- package/src/utils/format.ts +141 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { timestampFromDate } from "@bufbuild/protobuf/wkt";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { ApiClients } from "../client/api.js";
|
|
5
|
+
import { formatCycleList, formatCycle, formatError } from "../utils/format.js";
|
|
6
|
+
|
|
7
|
+
export function registerCycleTools(
|
|
8
|
+
server: McpServer,
|
|
9
|
+
getClients: () => ApiClients,
|
|
10
|
+
): void {
|
|
11
|
+
server.tool(
|
|
12
|
+
"opsee_list_cycles",
|
|
13
|
+
"List cycles/sprints in an Opsee project.",
|
|
14
|
+
{ projectId: z.number().describe("The project ID") },
|
|
15
|
+
async ({ projectId }) => {
|
|
16
|
+
try {
|
|
17
|
+
const clients = getClients();
|
|
18
|
+
const res = await clients.cycles.getCycles({ projectId });
|
|
19
|
+
return { content: [{ type: "text", text: formatCycleList(res.cycles) }] };
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
server.tool(
|
|
27
|
+
"opsee_get_cycle",
|
|
28
|
+
"Get details of a specific cycle/sprint by ID.",
|
|
29
|
+
{ cycleId: z.number().describe("The cycle ID") },
|
|
30
|
+
async ({ cycleId }) => {
|
|
31
|
+
try {
|
|
32
|
+
const clients = getClients();
|
|
33
|
+
const res = await clients.cycles.getCycle({ id: cycleId });
|
|
34
|
+
if (!res.cycle) return { content: [{ type: "text", text: "Cycle not found." }] };
|
|
35
|
+
return { content: [{ type: "text", text: formatCycle(res.cycle) }] };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
server.tool(
|
|
43
|
+
"opsee_create_cycle",
|
|
44
|
+
"Create a new cycle/sprint in an Opsee project.",
|
|
45
|
+
{
|
|
46
|
+
projectId: z.number().describe("The project ID"),
|
|
47
|
+
name: z.string().describe("Cycle name"),
|
|
48
|
+
startDate: z.string().describe("Start date (ISO 8601, e.g. 2026-03-25)"),
|
|
49
|
+
endDate: z.string().describe("End date (ISO 8601, e.g. 2026-04-08)"),
|
|
50
|
+
description: z.string().optional().describe("Cycle description"),
|
|
51
|
+
},
|
|
52
|
+
async ({ projectId, name, startDate, endDate, description }) => {
|
|
53
|
+
try {
|
|
54
|
+
const clients = getClients();
|
|
55
|
+
const res = await clients.cycles.addCycle({
|
|
56
|
+
projectId,
|
|
57
|
+
name,
|
|
58
|
+
goal: "",
|
|
59
|
+
startDate: timestampFromDate(new Date(startDate)),
|
|
60
|
+
endDate: timestampFromDate(new Date(endDate)),
|
|
61
|
+
isActive: true,
|
|
62
|
+
description,
|
|
63
|
+
});
|
|
64
|
+
if (!res.cycle)
|
|
65
|
+
return { content: [{ type: "text", text: "Failed to create cycle." }] };
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{ type: "text", text: `Cycle created:\n${formatCycle(res.cycle)}` },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { create } from "@bufbuild/protobuf";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { ApiClients } from "../client/api.js";
|
|
5
|
+
import { authManager } from "../auth/manager.js";
|
|
6
|
+
import {
|
|
7
|
+
formatDocSpaceList,
|
|
8
|
+
formatDocPageList,
|
|
9
|
+
formatDocPage,
|
|
10
|
+
formatError,
|
|
11
|
+
} from "../utils/format.js";
|
|
12
|
+
import {
|
|
13
|
+
FilterOptionsSchema,
|
|
14
|
+
FilterSchema,
|
|
15
|
+
} from "../../gen/api/v1/filter_pb.js";
|
|
16
|
+
|
|
17
|
+
function toKebabCase(str: string): string {
|
|
18
|
+
return str
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
21
|
+
.replace(/^-|-$/g, "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerDocTools(
|
|
25
|
+
server: McpServer,
|
|
26
|
+
getClients: () => ApiClients,
|
|
27
|
+
): void {
|
|
28
|
+
server.tool(
|
|
29
|
+
"opsee_list_doc_spaces",
|
|
30
|
+
"List documentation spaces in an Opsee project.",
|
|
31
|
+
{ projectId: z.number().describe("The project ID") },
|
|
32
|
+
async ({ projectId }) => {
|
|
33
|
+
try {
|
|
34
|
+
const clients = getClients();
|
|
35
|
+
const res = await clients.docSpaces.getDocSpaces({
|
|
36
|
+
filterOptions: create(FilterOptionsSchema, {
|
|
37
|
+
filters: [
|
|
38
|
+
create(FilterSchema, {
|
|
39
|
+
key: "ProjectId",
|
|
40
|
+
value: String(projectId),
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: formatDocSpaceList(res.docSpaces) }],
|
|
47
|
+
};
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
server.tool(
|
|
55
|
+
"opsee_list_doc_pages",
|
|
56
|
+
"List documentation pages in a doc space.",
|
|
57
|
+
{ docSpaceId: z.number().describe("The doc space ID") },
|
|
58
|
+
async ({ docSpaceId }) => {
|
|
59
|
+
try {
|
|
60
|
+
const clients = getClients();
|
|
61
|
+
const res = await clients.docPages.getDocPages({
|
|
62
|
+
filterOptions: create(FilterOptionsSchema, {
|
|
63
|
+
filters: [
|
|
64
|
+
create(FilterSchema, {
|
|
65
|
+
key: "DocSpaceId",
|
|
66
|
+
value: String(docSpaceId),
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: formatDocPageList(res.docPages) }],
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.tool(
|
|
81
|
+
"opsee_get_doc_page",
|
|
82
|
+
"Read a documentation page's content by ID.",
|
|
83
|
+
{ pageId: z.number().describe("The doc page ID") },
|
|
84
|
+
async ({ pageId }) => {
|
|
85
|
+
try {
|
|
86
|
+
const clients = getClients();
|
|
87
|
+
const res = await clients.docPages.getDocPage({ id: pageId });
|
|
88
|
+
if (!res.docPage)
|
|
89
|
+
return { content: [{ type: "text", text: "Doc page not found." }] };
|
|
90
|
+
return { content: [{ type: "text", text: formatDocPage(res.docPage) }] };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
server.tool(
|
|
98
|
+
"opsee_create_doc_page",
|
|
99
|
+
"Create a new documentation page in an Opsee project.",
|
|
100
|
+
{
|
|
101
|
+
projectId: z.number().describe("The project ID"),
|
|
102
|
+
title: z.string().describe("Page title"),
|
|
103
|
+
content: z.string().describe("Page content (text or JSON)"),
|
|
104
|
+
parentPageId: z.number().optional().describe("Parent page ID for nested pages"),
|
|
105
|
+
},
|
|
106
|
+
async ({ projectId, title, content, parentPageId }) => {
|
|
107
|
+
try {
|
|
108
|
+
const clients = getClients();
|
|
109
|
+
|
|
110
|
+
// Resolve current user for createdByUserId
|
|
111
|
+
let userId = Number(authManager.getUserId()) || 0;
|
|
112
|
+
if (!userId) {
|
|
113
|
+
const meRes = await clients.users.getMe({});
|
|
114
|
+
if (meRes.user) userId = meRes.user.id;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const res = await clients.docPages.addDocPage({
|
|
118
|
+
projectId,
|
|
119
|
+
title,
|
|
120
|
+
slug: toKebabCase(title),
|
|
121
|
+
currentContent: content,
|
|
122
|
+
contentType: "json",
|
|
123
|
+
currentVersionNumber: 1,
|
|
124
|
+
isRoot: !parentPageId,
|
|
125
|
+
isArchived: false,
|
|
126
|
+
displayOrder: 0,
|
|
127
|
+
parentPageId,
|
|
128
|
+
createdByUserId: userId,
|
|
129
|
+
updatedByUserId: userId,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!res.docPage)
|
|
133
|
+
return { content: [{ type: "text", text: "Failed to create doc page." }] };
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
content: [
|
|
137
|
+
{
|
|
138
|
+
type: "text",
|
|
139
|
+
text: `Doc page created:\n${formatDocPage(res.docPage)}`,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ApiClients } from "../client/api.js";
|
|
4
|
+
import { formatProjectList, formatProject, formatError } from "../utils/format.js";
|
|
5
|
+
|
|
6
|
+
export function registerProjectTools(
|
|
7
|
+
server: McpServer,
|
|
8
|
+
getClients: () => ApiClients,
|
|
9
|
+
): void {
|
|
10
|
+
server.tool(
|
|
11
|
+
"opsee_list_projects",
|
|
12
|
+
"List all Opsee projects the authenticated user has access to.",
|
|
13
|
+
{},
|
|
14
|
+
async () => {
|
|
15
|
+
try {
|
|
16
|
+
const clients = getClients();
|
|
17
|
+
const res = await clients.projects.getProjects({});
|
|
18
|
+
return { content: [{ type: "text", text: formatProjectList(res.projects) }] };
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
server.tool(
|
|
26
|
+
"opsee_get_project",
|
|
27
|
+
"Get details of a specific Opsee project by ID.",
|
|
28
|
+
{ projectId: z.number().describe("The project ID") },
|
|
29
|
+
async ({ projectId }) => {
|
|
30
|
+
try {
|
|
31
|
+
const clients = getClients();
|
|
32
|
+
const res = await clients.projects.getProject({ id: projectId });
|
|
33
|
+
if (!res.project) return { content: [{ type: "text", text: "Project not found." }] };
|
|
34
|
+
return { content: [{ type: "text", text: formatProject(res.project) }] };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ApiClients } from "../client/api.js";
|
|
4
|
+
import { formatError } from "../utils/format.js";
|
|
5
|
+
|
|
6
|
+
export function registerRepositoryTools(
|
|
7
|
+
server: McpServer,
|
|
8
|
+
getClients: () => ApiClients,
|
|
9
|
+
): void {
|
|
10
|
+
server.tool(
|
|
11
|
+
"opsee_list_repositories",
|
|
12
|
+
"List connected VCS repositories (GitHub/GitLab) for an Opsee project.",
|
|
13
|
+
{ projectId: z.number().describe("The project ID") },
|
|
14
|
+
async ({ projectId }) => {
|
|
15
|
+
try {
|
|
16
|
+
const clients = getClients();
|
|
17
|
+
const res = await clients.vcsIntegrations.getVCSIntegrations({ projectId });
|
|
18
|
+
if (!res.vcsIntegrations?.length)
|
|
19
|
+
return { content: [{ type: "text", text: "No repositories connected." }] };
|
|
20
|
+
|
|
21
|
+
const text = res.vcsIntegrations
|
|
22
|
+
.map((v) => {
|
|
23
|
+
const provider = v.provider === 1 ? "GitHub" : v.provider === 2 ? "GitLab" : "Unknown";
|
|
24
|
+
return `- ${v.repoOwner}/${v.repoName} (${provider}, branch: ${v.defaultBranch}, active: ${v.isActive}, ID: ${v.id})`;
|
|
25
|
+
})
|
|
26
|
+
.join("\n");
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `Connected repositories:\n${text}`,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import type { ApiClients } from "../client/api.js";
|
|
4
|
+
import { formatError } from "../utils/format.js";
|
|
5
|
+
|
|
6
|
+
export function registerTaskMetadataTools(
|
|
7
|
+
server: McpServer,
|
|
8
|
+
getClients: () => ApiClients,
|
|
9
|
+
): void {
|
|
10
|
+
server.tool(
|
|
11
|
+
"opsee_list_task_types",
|
|
12
|
+
"Get available task types (Bug, Feature, etc.) for an Opsee project. Use these IDs when creating or updating tasks.",
|
|
13
|
+
{ projectId: z.number().describe("The project ID") },
|
|
14
|
+
async ({ projectId }) => {
|
|
15
|
+
try {
|
|
16
|
+
const clients = getClients();
|
|
17
|
+
const res = await clients.taskTypes.getTaskTypes({ projectId });
|
|
18
|
+
if (!res.taskTypes?.length)
|
|
19
|
+
return { content: [{ type: "text", text: "No task types found." }] };
|
|
20
|
+
|
|
21
|
+
const text = res.taskTypes
|
|
22
|
+
.map((t) => `- ${t.name} (ID: ${t.id})`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
return { content: [{ type: "text", text: `Task types:\n${text}` }] };
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
server.tool(
|
|
32
|
+
"opsee_list_task_priorities",
|
|
33
|
+
"Get priority levels (Critical, High, Medium, Low, etc.) for an Opsee project. Use these IDs when creating or updating tasks.",
|
|
34
|
+
{ projectId: z.number().describe("The project ID") },
|
|
35
|
+
async ({ projectId }) => {
|
|
36
|
+
try {
|
|
37
|
+
const clients = getClients();
|
|
38
|
+
const res = await clients.taskPriorities.getTaskPriorities({ projectId });
|
|
39
|
+
if (!res.taskPriorities?.length)
|
|
40
|
+
return { content: [{ type: "text", text: "No task priorities found." }] };
|
|
41
|
+
|
|
42
|
+
const text = res.taskPriorities
|
|
43
|
+
.map((p) => `- ${p.name} (level: ${p.level}, ID: ${p.id})`)
|
|
44
|
+
.join("\n");
|
|
45
|
+
return { content: [{ type: "text", text: `Task priorities:\n${text}` }] };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
server.tool(
|
|
53
|
+
"opsee_list_boards",
|
|
54
|
+
"List Kanban boards for an Opsee project. Use the board ID to list board columns.",
|
|
55
|
+
{ projectId: z.number().describe("The project ID") },
|
|
56
|
+
async ({ projectId }) => {
|
|
57
|
+
try {
|
|
58
|
+
const clients = getClients();
|
|
59
|
+
const res = await clients.boards.getBoards({ projectId });
|
|
60
|
+
if (!res.boards?.length)
|
|
61
|
+
return { content: [{ type: "text", text: "No boards found." }] };
|
|
62
|
+
|
|
63
|
+
const text = res.boards
|
|
64
|
+
.map((b) => `- ${b.name} (ID: ${b.id}, active: ${b.isActive})`)
|
|
65
|
+
.join("\n");
|
|
66
|
+
return { content: [{ type: "text", text: `Boards:\n${text}` }] };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
server.tool(
|
|
74
|
+
"opsee_list_board_columns",
|
|
75
|
+
"Get board columns/statuses (To Do, In Progress, Done, etc.) for a board. Use these IDs when creating or updating tasks.",
|
|
76
|
+
{ boardId: z.number().describe("The board ID") },
|
|
77
|
+
async ({ boardId }) => {
|
|
78
|
+
try {
|
|
79
|
+
const clients = getClients();
|
|
80
|
+
const res = await clients.boardColumns.getBoardColumns({ boardId });
|
|
81
|
+
if (!res.boardColumns?.length)
|
|
82
|
+
return { content: [{ type: "text", text: "No board columns found." }] };
|
|
83
|
+
|
|
84
|
+
const text = res.boardColumns
|
|
85
|
+
.map(
|
|
86
|
+
(c) =>
|
|
87
|
+
`- ${c.name} (ID: ${c.id}, order: ${c.displayOrder}, lifecycle: ${c.lifecycleState})`,
|
|
88
|
+
)
|
|
89
|
+
.join("\n");
|
|
90
|
+
return { content: [{ type: "text", text: `Board columns:\n${text}` }] };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { create } from "@bufbuild/protobuf";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { ApiClients } from "../client/api.js";
|
|
5
|
+
import { defaultPagination } from "../client/api.js";
|
|
6
|
+
import { authManager } from "../auth/manager.js";
|
|
7
|
+
import { formatTaskList, formatTask, formatError } from "../utils/format.js";
|
|
8
|
+
import {
|
|
9
|
+
FilterOptionsSchema,
|
|
10
|
+
FilterSchema,
|
|
11
|
+
} from "../../gen/api/v1/filter_pb.js";
|
|
12
|
+
|
|
13
|
+
export function registerTaskTools(
|
|
14
|
+
server: McpServer,
|
|
15
|
+
getClients: () => ApiClients,
|
|
16
|
+
): void {
|
|
17
|
+
server.tool(
|
|
18
|
+
"opsee_list_tasks",
|
|
19
|
+
"List tasks in an Opsee project. Supports filtering by status, assignee, board column, and cycle.",
|
|
20
|
+
{
|
|
21
|
+
projectId: z.number().describe("The project ID"),
|
|
22
|
+
columnId: z.number().optional().describe("Filter by board column ID (status)"),
|
|
23
|
+
assigneeId: z.number().optional().describe("Filter by assigned user ID"),
|
|
24
|
+
cycleId: z.number().optional().describe("Filter by cycle/sprint ID"),
|
|
25
|
+
page: z.number().optional().describe("Page number (default: 1)"),
|
|
26
|
+
pageSize: z.number().optional().describe("Items per page (default: 50)"),
|
|
27
|
+
},
|
|
28
|
+
async ({ projectId, columnId, assigneeId, cycleId, page, pageSize }) => {
|
|
29
|
+
try {
|
|
30
|
+
const clients = getClients();
|
|
31
|
+
|
|
32
|
+
const filters: Array<{ key: string; value: string }> = [];
|
|
33
|
+
if (columnId) filters.push({ key: "BoardColumnId", value: String(columnId) });
|
|
34
|
+
if (assigneeId) filters.push({ key: "AssignedUserId", value: String(assigneeId) });
|
|
35
|
+
if (cycleId) filters.push({ key: "CycleId", value: String(cycleId) });
|
|
36
|
+
|
|
37
|
+
const filterOptions = filters.length > 0
|
|
38
|
+
? create(FilterOptionsSchema, {
|
|
39
|
+
filters: filters.map((f) => create(FilterSchema, f)),
|
|
40
|
+
})
|
|
41
|
+
: undefined;
|
|
42
|
+
|
|
43
|
+
const res = await clients.tasks.getTasks({
|
|
44
|
+
projectId,
|
|
45
|
+
pagination: defaultPagination(page || 1, pageSize || 50),
|
|
46
|
+
filterOptions,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return { content: [{ type: "text", text: formatTaskList(res.tasks) }] };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
server.tool(
|
|
57
|
+
"opsee_get_task",
|
|
58
|
+
"Get full details of a specific task by ID.",
|
|
59
|
+
{ taskId: z.number().describe("The task ID") },
|
|
60
|
+
async ({ taskId }) => {
|
|
61
|
+
try {
|
|
62
|
+
const clients = getClients();
|
|
63
|
+
const res = await clients.tasks.getTask({ id: taskId });
|
|
64
|
+
if (!res.task) return { content: [{ type: "text", text: "Task not found. Use opsee_list_tasks to see available tasks." }] };
|
|
65
|
+
return { content: [{ type: "text", text: formatTask(res.task) }] };
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.tool(
|
|
73
|
+
"opsee_create_task",
|
|
74
|
+
"Create a new task in an Opsee project. Use opsee_list_task_types, opsee_list_task_priorities, and opsee_list_board_columns first to get valid values for optional fields.",
|
|
75
|
+
{
|
|
76
|
+
projectId: z.number().describe("The project ID"),
|
|
77
|
+
title: z.string().describe("Task title"),
|
|
78
|
+
description: z.string().optional().describe("Task description"),
|
|
79
|
+
taskTypeId: z.number().optional().describe("Task type ID (from opsee_list_task_types)"),
|
|
80
|
+
priorityId: z.number().optional().describe("Priority ID (from opsee_list_task_priorities)"),
|
|
81
|
+
boardColumnId: z.number().optional().describe("Board column ID (from opsee_list_board_columns)"),
|
|
82
|
+
assigneeId: z.number().optional().describe("Assigned user ID"),
|
|
83
|
+
cycleId: z.number().optional().describe("Cycle/sprint ID"),
|
|
84
|
+
},
|
|
85
|
+
async ({ projectId, title, description, taskTypeId, priorityId, boardColumnId, assigneeId, cycleId }) => {
|
|
86
|
+
try {
|
|
87
|
+
const clients = getClients();
|
|
88
|
+
|
|
89
|
+
// Auto-resolve defaults
|
|
90
|
+
let resolvedBoardId = 0;
|
|
91
|
+
let resolvedColumnId = boardColumnId || 0;
|
|
92
|
+
let resolvedTaskTypeId = taskTypeId || 0;
|
|
93
|
+
let resolvedPriorityId = priorityId || 0;
|
|
94
|
+
|
|
95
|
+
// Get default board
|
|
96
|
+
const boardsRes = await clients.boards.getBoards({ projectId });
|
|
97
|
+
if (boardsRes.boards?.length) {
|
|
98
|
+
resolvedBoardId = boardsRes.boards[0].id;
|
|
99
|
+
|
|
100
|
+
// Get first column if not specified
|
|
101
|
+
if (!boardColumnId) {
|
|
102
|
+
const colsRes = await clients.boardColumns.getBoardColumns({
|
|
103
|
+
boardId: resolvedBoardId,
|
|
104
|
+
});
|
|
105
|
+
if (colsRes.boardColumns?.length) {
|
|
106
|
+
resolvedColumnId = colsRes.boardColumns[0].id;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get first task type if not specified
|
|
112
|
+
if (!taskTypeId) {
|
|
113
|
+
const typesRes = await clients.taskTypes.getTaskTypes({ projectId });
|
|
114
|
+
if (typesRes.taskTypes?.length) {
|
|
115
|
+
resolvedTaskTypeId = typesRes.taskTypes[0].id;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Get first priority if not specified
|
|
120
|
+
if (!priorityId) {
|
|
121
|
+
const priRes = await clients.taskPriorities.getTaskPriorities({ projectId });
|
|
122
|
+
if (priRes.taskPriorities?.length) {
|
|
123
|
+
resolvedPriorityId = priRes.taskPriorities[0].id;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Get reporter (current user)
|
|
128
|
+
let reporterUserId = 0;
|
|
129
|
+
const userId = authManager.getUserId();
|
|
130
|
+
if (userId) {
|
|
131
|
+
reporterUserId = Number(userId);
|
|
132
|
+
}
|
|
133
|
+
if (!reporterUserId) {
|
|
134
|
+
const meRes = await clients.users.getMe({});
|
|
135
|
+
if (meRes.user) reporterUserId = meRes.user.id;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const res = await clients.tasks.addTask({
|
|
139
|
+
projectId,
|
|
140
|
+
title,
|
|
141
|
+
description,
|
|
142
|
+
boardId: resolvedBoardId,
|
|
143
|
+
boardColumnId: resolvedColumnId,
|
|
144
|
+
taskTypeId: resolvedTaskTypeId,
|
|
145
|
+
taskPriorityId: resolvedPriorityId,
|
|
146
|
+
reporterUserId,
|
|
147
|
+
assignedUserId: assigneeId,
|
|
148
|
+
displayOrder: 0,
|
|
149
|
+
aiModeEnabled: false,
|
|
150
|
+
cycleId,
|
|
151
|
+
repositoryIds: [],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!res.task)
|
|
155
|
+
return { content: [{ type: "text", text: "Failed to create task." }] };
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{ type: "text", text: `Task created:\n${formatTask(res.task)}` },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
server.tool(
|
|
169
|
+
"opsee_update_task",
|
|
170
|
+
"Update a task's fields (patch semantics — only provided fields are changed). Fetches the current task first, merges your changes, and sends the full update.",
|
|
171
|
+
{
|
|
172
|
+
taskId: z.number().describe("The task ID to update"),
|
|
173
|
+
title: z.string().optional().describe("New title"),
|
|
174
|
+
description: z.string().optional().describe("New description"),
|
|
175
|
+
taskTypeId: z.number().optional().describe("New task type ID"),
|
|
176
|
+
priorityId: z.number().optional().describe("New priority ID"),
|
|
177
|
+
boardColumnId: z.number().optional().describe("New board column ID (status)"),
|
|
178
|
+
assigneeId: z.number().optional().describe("New assigned user ID"),
|
|
179
|
+
cycleId: z.number().optional().describe("New cycle/sprint ID"),
|
|
180
|
+
},
|
|
181
|
+
async ({ taskId, title, description, taskTypeId, priorityId, boardColumnId, assigneeId, cycleId }) => {
|
|
182
|
+
try {
|
|
183
|
+
const clients = getClients();
|
|
184
|
+
|
|
185
|
+
// Read current task
|
|
186
|
+
const getRes = await clients.tasks.getTask({ id: taskId });
|
|
187
|
+
const task = getRes.task;
|
|
188
|
+
if (!task)
|
|
189
|
+
return { content: [{ type: "text", text: "Task not found. Use opsee_list_tasks to see available tasks." }] };
|
|
190
|
+
|
|
191
|
+
// Merge changes into full EditTaskRequest
|
|
192
|
+
const res = await clients.tasks.editTask({
|
|
193
|
+
id: task.id,
|
|
194
|
+
identifier: task.identifier,
|
|
195
|
+
title: title ?? task.title,
|
|
196
|
+
description: description ?? task.description,
|
|
197
|
+
displayOrder: task.displayOrder,
|
|
198
|
+
storyPoints: task.storyPoints,
|
|
199
|
+
estimatedHours: task.estimatedHours,
|
|
200
|
+
actualHours: task.actualHours,
|
|
201
|
+
dueDate: task.dueDate,
|
|
202
|
+
aiModeEnabled: task.aiModeEnabled,
|
|
203
|
+
metadataJson: task.metadataJson,
|
|
204
|
+
boardId: task.board?.id ?? 0,
|
|
205
|
+
boardColumnId: boardColumnId ?? task.boardColumn?.id ?? 0,
|
|
206
|
+
projectId: task.project?.id ?? 0,
|
|
207
|
+
taskTypeId: taskTypeId ?? task.taskType?.id ?? 0,
|
|
208
|
+
taskPriorityId: priorityId ?? task.taskPriority?.id ?? 0,
|
|
209
|
+
reporterUserId: task.reporterUser?.id ?? 0,
|
|
210
|
+
assignedUserId: assigneeId ?? task.assignedUser?.id,
|
|
211
|
+
cycleId: cycleId ?? task.cycle?.id,
|
|
212
|
+
repositoryIds: task.taskRepositories?.map((r: { id: number }) => r.id) ?? [],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!res.task)
|
|
216
|
+
return { content: [{ type: "text", text: "Failed to update task." }] };
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
content: [
|
|
220
|
+
{ type: "text", text: `Task updated:\n${formatTask(res.task)}` },
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { ApiClients } from "../client/api.js";
|
|
3
|
+
import { formatError } from "../utils/format.js";
|
|
4
|
+
|
|
5
|
+
export function registerUserTools(
|
|
6
|
+
server: McpServer,
|
|
7
|
+
getClients: () => ApiClients,
|
|
8
|
+
): void {
|
|
9
|
+
server.tool(
|
|
10
|
+
"opsee_get_me",
|
|
11
|
+
"Get the currently authenticated Opsee user's profile (name, email, role, company).",
|
|
12
|
+
{},
|
|
13
|
+
async () => {
|
|
14
|
+
try {
|
|
15
|
+
const clients = getClients();
|
|
16
|
+
const res = await clients.users.getMe({});
|
|
17
|
+
const u = res.user;
|
|
18
|
+
if (!u) return { content: [{ type: "text", text: "No user found." }] };
|
|
19
|
+
|
|
20
|
+
const text = [
|
|
21
|
+
`${u.fullName} (${u.email})`,
|
|
22
|
+
` ID: ${u.id}`,
|
|
23
|
+
` Company: ${u.company?.name || "N/A"} (ID: ${u.companyId})`,
|
|
24
|
+
` Roles: ${u.flattenedRoles?.join(", ") || "N/A"}`,
|
|
25
|
+
].join("\n");
|
|
26
|
+
|
|
27
|
+
return { content: [{ type: "text", text }] };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return { content: [{ type: "text", text: formatError(error) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
}
|