@opsee/mcp-server 0.9.60 → 0.10.2
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/gen/api/v1/billing_pb.d.ts +0 -40
- package/gen/api/v1/billing_pb.js +22 -36
- package/gen/api/v1/developer_metrics_pb.d.ts +9 -3
- package/gen/api/v1/developer_metrics_pb.js +2 -1
- package/gen/api/v1/models_pb.d.ts +116 -5
- package/gen/api/v1/models_pb.js +33 -26
- package/gen/api/v1/project_pb.d.ts +18 -13
- package/gen/api/v1/project_pb.js +1 -1
- package/gen/api/v1/task_subscription_pb.d.ts +146 -0
- package/gen/api/v1/task_subscription_pb.js +66 -0
- package/gen/api/v1/vcs_connection_pb.d.ts +34 -3
- package/gen/api/v1/vcs_connection_pb.js +28 -20
- package/gen/api/v1/vcs_integration_pb.d.ts +11 -1
- package/gen/api/v1/vcs_integration_pb.js +2 -1
- package/gen/fk/foreign_key_pb.d.ts +22 -0
- package/gen/fk/foreign_key_pb.js +24 -0
- package/package.json +11 -11
- package/src/auth/kv-store.ts +12 -33
- package/src/auth/manager.ts +1 -6
- package/src/auth/oauth-provider.ts +32 -104
- package/src/auth/renew.ts +5 -15
- package/src/client/api.ts +3 -5
- package/src/server-http.ts +14 -30
- package/src/tools/acceptance-criteria.ts +4 -14
- package/src/tools/descriptions.ts +2 -7
- package/src/tools/doc-links.ts +4 -9
- package/src/tools/doc-spaces.ts +1 -1
- package/src/tools/docs.ts +8 -19
- package/src/tools/notifications.ts +2 -6
- package/src/tools/tasks-bulk.ts +3 -8
- package/src/tools/tasks-read-write.ts +1 -8
- package/src/utils/format/blocknote.ts +8 -23
- package/src/utils/tool-schema.ts +7 -29
- package/README.md +0 -304
- package/gen/api/v1/deployment_pb.d.ts +0 -277
- package/gen/api/v1/deployment_pb.js +0 -89
- package/gen/api/v1/environment_pb.d.ts +0 -277
- package/gen/api/v1/environment_pb.js +0 -89
- package/gen/api/v1/test_run_pb.d.ts +0 -277
- package/gen/api/v1/test_run_pb.js +0 -89
package/src/tools/docs.ts
CHANGED
|
@@ -39,11 +39,8 @@ export function registerDocTools(
|
|
|
39
39
|
async ({ projectId }) => {
|
|
40
40
|
try {
|
|
41
41
|
const clients = getClients();
|
|
42
|
-
// project_id
|
|
43
|
-
//
|
|
44
|
-
// ProjectId filter cannot stand in for it -- scopes.ParseFilterOptions
|
|
45
|
-
// drops that key as security-sensitive, which returned every doc space
|
|
46
|
-
// in the company.
|
|
42
|
+
// project_id must ride on the request: ParseFilterOptions drops it from generic filters as
|
|
43
|
+
// security-sensitive, which returned every doc space in the company.
|
|
47
44
|
const res = await clients.docSpaces.getDocSpaces({
|
|
48
45
|
projectId,
|
|
49
46
|
pagination: overridePagination(),
|
|
@@ -65,11 +62,8 @@ export function registerDocTools(
|
|
|
65
62
|
async ({ projectId }) => {
|
|
66
63
|
try {
|
|
67
64
|
const clients = getClients();
|
|
68
|
-
// Doc pages hang off the project,
|
|
69
|
-
//
|
|
70
|
-
// scopes the list with the request's own project_id, which the validator
|
|
71
|
-
// requires to be >= 1. A generic ProjectId filter cannot substitute for
|
|
72
|
-
// it -- scopes.ParseFilterOptions drops that key as security-sensitive.
|
|
65
|
+
// Doc pages hang off the project, and the request's own project_id (validated >= 1) is what
|
|
66
|
+
// scopes the list; a generic filter is dropped as security-sensitive.
|
|
73
67
|
const res = await clients.docPages.getDocPages({
|
|
74
68
|
projectId,
|
|
75
69
|
pagination: overridePagination(),
|
|
@@ -121,7 +115,6 @@ export function registerDocTools(
|
|
|
121
115
|
try {
|
|
122
116
|
const clients = getClients();
|
|
123
117
|
|
|
124
|
-
// Resolve current user for createdByUserId
|
|
125
118
|
let userId = Number(authManager.getUserId()) || 0;
|
|
126
119
|
if (!userId) {
|
|
127
120
|
const meRes = await clients.users.getMe({});
|
|
@@ -235,10 +228,8 @@ export function registerDocTools(
|
|
|
235
228
|
isRoot: existing.isRoot,
|
|
236
229
|
isArchived: existing.isArchived,
|
|
237
230
|
displayOrder: existing.displayOrder,
|
|
238
|
-
// EditDocPageRequest carries project_id
|
|
239
|
-
//
|
|
240
|
-
// field -- only the unused legacy doc_space_id -- so there is no way
|
|
241
|
-
// to read the existing value back to resend it here. Left unset.
|
|
231
|
+
// Left unset: EditDocPageRequest carries project_id but the DocPage message has no such
|
|
232
|
+
// field, so the existing value cannot be read back.
|
|
242
233
|
parentPageId: existing.parentPageId,
|
|
243
234
|
createdByUserId: existing.createdByUserId,
|
|
244
235
|
updatedByUserId: userId,
|
|
@@ -260,10 +251,8 @@ export function registerDocTools(
|
|
|
260
251
|
createdByUserId: userId,
|
|
261
252
|
});
|
|
262
253
|
} catch (snapshotError) {
|
|
263
|
-
// The
|
|
264
|
-
//
|
|
265
|
-
// so the gap between the page counter and version history can be
|
|
266
|
-
// reconciled by hand.
|
|
254
|
+
// The edit already committed, so report rather than raise, naming the version so the gap
|
|
255
|
+
// against version history can be reconciled by hand.
|
|
267
256
|
warning = `\n\nWarning: the page was saved, but recording version ${nextVersionNumber} in page history failed: ${formatError(snapshotError)}\nThe page now reports version ${nextVersionNumber} with no matching history entry. Re-running this tool creates version ${nextVersionNumber + 1} rather than backfilling ${nextVersionNumber}.`;
|
|
268
257
|
}
|
|
269
258
|
}
|
|
@@ -9,12 +9,8 @@ import {
|
|
|
9
9
|
FilterSchema,
|
|
10
10
|
} from "../../gen/api/v1/filter_pb.js";
|
|
11
11
|
|
|
12
|
-
// Notifications are user-scoped server-side (the
|
|
13
|
-
//
|
|
14
|
-
// notification_type values are free-form strings produced by the backend
|
|
15
|
-
// (e.g. "task_assigned", "comment_added", "task_blocked_unblocked",
|
|
16
|
-
// "mention", "ai_workflow_status_change"). Treat them as opaque tags;
|
|
17
|
-
// callers can filter via the unreadOnly param or post-process by type.
|
|
12
|
+
// Notifications are user-scoped server-side (GetNotifications reads user_id off the auth context), so
|
|
13
|
+
// there is no user parameter. notification_type values are backend-defined opaque tags.
|
|
18
14
|
|
|
19
15
|
function formatNotification(n: {
|
|
20
16
|
id: number;
|
package/src/tools/tasks-bulk.ts
CHANGED
|
@@ -4,14 +4,9 @@ import type { ApiClients } from "../client/api.js";
|
|
|
4
4
|
import { formatError } from "../utils/format.js";
|
|
5
5
|
import { BULK_PROJECT_ID } from "./descriptions.js";
|
|
6
6
|
|
|
7
|
-
// Multi-task edits
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// OPS-203: the per-field convenience wrappers (bulk_assign, bulk_set_priority,
|
|
12
|
-
// bulk_move_to_column, bulk_move_to_cycle) were removed. Each was a strict
|
|
13
|
-
// subset of opsee_bulk_update_tasks with a single field set, and four extra
|
|
14
|
-
// tool definitions cost more context on every request than they saved.
|
|
7
|
+
// Multi-task edits via BulkEditTasks, which applies everything in one transaction and silently skips
|
|
8
|
+
// invisible task ids, hence the "n of m" response wording. The per-field wrappers were removed:
|
|
9
|
+
// each was opsee_bulk_update_tasks with one field set, and cost context on every request.
|
|
15
10
|
export function registerTaskBulkTools(
|
|
16
11
|
server: McpServer,
|
|
17
12
|
getClients: () => ApiClients,
|
|
@@ -204,18 +204,15 @@ export function registerTaskReadWriteTools(
|
|
|
204
204
|
try {
|
|
205
205
|
const clients = getClients();
|
|
206
206
|
|
|
207
|
-
// Auto-resolve defaults
|
|
208
207
|
let resolvedBoardId = 0;
|
|
209
208
|
let resolvedColumnId = boardColumnId || 0;
|
|
210
209
|
let resolvedTaskTypeId = taskTypeId || 0;
|
|
211
210
|
let resolvedPriorityId = priorityId || 0;
|
|
212
211
|
|
|
213
|
-
// Get default board
|
|
214
212
|
const boardsRes = await clients.boards.getBoards({ projectId });
|
|
215
213
|
if (boardsRes.boards?.length) {
|
|
216
214
|
resolvedBoardId = boardsRes.boards[0].id;
|
|
217
215
|
|
|
218
|
-
// Get first column if not specified
|
|
219
216
|
if (!boardColumnId) {
|
|
220
217
|
const colsRes = await clients.boardColumns.getBoardColumns({
|
|
221
218
|
boardId: resolvedBoardId,
|
|
@@ -226,7 +223,6 @@ export function registerTaskReadWriteTools(
|
|
|
226
223
|
}
|
|
227
224
|
}
|
|
228
225
|
|
|
229
|
-
// Get first task type if not specified
|
|
230
226
|
if (!taskTypeId) {
|
|
231
227
|
const typesRes = await clients.taskTypes.getTaskTypes({ projectId });
|
|
232
228
|
if (typesRes.taskTypes?.length) {
|
|
@@ -234,7 +230,6 @@ export function registerTaskReadWriteTools(
|
|
|
234
230
|
}
|
|
235
231
|
}
|
|
236
232
|
|
|
237
|
-
// Get first priority if not specified
|
|
238
233
|
if (!priorityId) {
|
|
239
234
|
const priRes = await clients.taskPriorities.getTaskPriorities({ projectId });
|
|
240
235
|
if (priRes.taskPriorities?.length) {
|
|
@@ -242,7 +237,6 @@ export function registerTaskReadWriteTools(
|
|
|
242
237
|
}
|
|
243
238
|
}
|
|
244
239
|
|
|
245
|
-
// Get reporter (current user)
|
|
246
240
|
let reporterUserId = 0;
|
|
247
241
|
const userId = authManager.getUserId();
|
|
248
242
|
if (userId) {
|
|
@@ -312,13 +306,12 @@ export function registerTaskReadWriteTools(
|
|
|
312
306
|
try {
|
|
313
307
|
const clients = getClients();
|
|
314
308
|
|
|
315
|
-
// Read current task
|
|
316
309
|
const getRes = await clients.tasks.getTask({ id: taskId });
|
|
317
310
|
const task = getRes.task;
|
|
318
311
|
if (!task)
|
|
319
312
|
return { content: [{ type: "text", text: "Task not found. Use opsee_list_tasks to see available tasks." }] };
|
|
320
313
|
|
|
321
|
-
//
|
|
314
|
+
// EditTaskRequest is a full replacement, so merge onto the current task.
|
|
322
315
|
const res = await clients.tasks.editTask({
|
|
323
316
|
id: task.id,
|
|
324
317
|
identifier: task.identifier,
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
// `strike` is the key the BlockNote default schema uses, and it is what both Go
|
|
6
|
-
// writers persist (backend/service/task.go parseInlineMarkdown and
|
|
7
|
-
// orchestrator/internal/util/blocknote.go). `strikethrough` is accepted too so a
|
|
8
|
-
// payload using the long spelling is not silently dropped.
|
|
1
|
+
// `strike` is the BlockNote default schema's key and what both Go writers persist;
|
|
2
|
+
// `strikethrough` is accepted too so the long spelling is not silently dropped.
|
|
9
3
|
type BNInlineStyle = {
|
|
10
4
|
bold?: boolean;
|
|
11
5
|
italic?: boolean;
|
|
@@ -31,14 +25,12 @@ function renderInline(content: BNInlineContent[]): string {
|
|
|
31
25
|
const inner = renderInline(node.content ?? []);
|
|
32
26
|
return `[${inner}](${node.href})`;
|
|
33
27
|
}
|
|
34
|
-
// text node
|
|
35
28
|
const t = node as BNTextContent;
|
|
36
29
|
let s = t.text ?? "";
|
|
37
30
|
if (t.styles?.code) s = `\`${s}\``;
|
|
38
31
|
if (t.styles?.bold) s = `**${s}**`;
|
|
39
32
|
if (t.styles?.italic) s = `*${s}*`;
|
|
40
|
-
//
|
|
41
|
-
// markers would present an abandoned plan item as a live one.
|
|
33
|
+
// Keep the markers: struck text means "decided against", not a live plan item.
|
|
42
34
|
if (t.styles?.strike || t.styles?.strikethrough) s = `~~${s}~~`;
|
|
43
35
|
if (t.styles?.underline) s = `<u>${s}</u>`;
|
|
44
36
|
return s;
|
|
@@ -70,8 +62,7 @@ function renderBlock(block: BNBlock, indent: string): string[] {
|
|
|
70
62
|
case "codeBlock": {
|
|
71
63
|
const lang = (block.props?.language as string) || "";
|
|
72
64
|
lines.push(`${indent}\`\`\`${lang}`);
|
|
73
|
-
// A code block
|
|
74
|
-
// not just the first, or the rest escapes the list item it is nested in.
|
|
65
|
+
// A code block is one node with newlines; indent every line or it escapes the enclosing list item.
|
|
75
66
|
for (const body of inline.split("\n")) lines.push(`${indent}${body}`);
|
|
76
67
|
lines.push(`${indent}\`\`\``);
|
|
77
68
|
break;
|
|
@@ -94,12 +85,9 @@ function renderBlock(block: BNBlock, indent: string): string[] {
|
|
|
94
85
|
return lines;
|
|
95
86
|
}
|
|
96
87
|
|
|
97
|
-
// Mirrors the
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
// and backend/service/mentions.go IsBlockNoteJSON (unmarshal into []interface{}).
|
|
101
|
-
// A JSON array is the only shape BlockNote persists, so an object or a bare
|
|
102
|
-
// scalar is content, not a document body.
|
|
88
|
+
// Mirrors the product-side detectors (useDocTreeMutations.js, mentions.go IsBlockNoteJSON) so MCP and
|
|
89
|
+
// the web UI label identical bytes alike. BlockNote persists only arrays, so an object or scalar is
|
|
90
|
+
// content, not a document body.
|
|
103
91
|
export function isBlockNoteJSON(content: string): boolean {
|
|
104
92
|
try {
|
|
105
93
|
return Array.isArray(JSON.parse(content));
|
|
@@ -108,9 +96,7 @@ export function isBlockNoteJSON(content: string): boolean {
|
|
|
108
96
|
}
|
|
109
97
|
}
|
|
110
98
|
|
|
111
|
-
//
|
|
112
|
-
// contentType is "json" and otherwise wraps the raw string in a paragraph, so
|
|
113
|
-
// mislabelling markdown as "json" renders an empty page.
|
|
99
|
+
// DocPage.jsx only JSON-parses when contentType is "json", so mislabelling markdown renders an empty page.
|
|
114
100
|
export function detectDocContentType(content: string): "text" | "json" {
|
|
115
101
|
return isBlockNoteJSON(content) ? "json" : "text";
|
|
116
102
|
}
|
|
@@ -126,7 +112,6 @@ export function blockNoteToMarkdown(content: string): string {
|
|
|
126
112
|
if (typeof block !== "object" || block === null || !block.type) return content;
|
|
127
113
|
const blockLines = renderBlock(block, "");
|
|
128
114
|
mdLines.push(...blockLines);
|
|
129
|
-
// Separate top-level blocks with a blank line (except last)
|
|
130
115
|
if (i < parsed.length - 1) mdLines.push("");
|
|
131
116
|
}
|
|
132
117
|
return mdLines.join("\n");
|
package/src/utils/tool-schema.ts
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
3
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// "$schema": "http://json-schema.org/draft-07/schema#" (once per tool)
|
|
9
|
-
// "maximum": 9007199254740991 (per unbounded int)
|
|
10
|
-
// "minimum": -9007199254740991 (per unbounded int)
|
|
11
|
-
// "execution": {"taskSupport": "forbidden"} (once per tool)
|
|
12
|
-
//
|
|
13
|
-
// The bounds are Number.MAX/MIN_SAFE_INTEGER, which `z.number().int()` records
|
|
14
|
-
// on any int field that doesn't declare its own; no caller is going to pass an
|
|
15
|
-
// id near 2^53, and the server-side Zod schema still rejects one if they do.
|
|
16
|
-
// Stripping them leaves the advertised schema a valid, strictly weaker JSON
|
|
17
|
-
// Schema — an explicit bound like `.min(1)` is left alone.
|
|
18
|
-
//
|
|
19
|
-
// `execution.taskSupport` is optional in the protocol and clients only act on
|
|
20
|
-
// "required"/"optional" (see the SDK client's callTool path) — an absent field
|
|
21
|
-
// and an explicit "forbidden" are the same thing to them.
|
|
4
|
+
// tools/list is re-sent on every AI request, so these no-information SDK constructs are stripped:
|
|
5
|
+
// "$schema", the MAX/MIN_SAFE_INTEGER bounds z.number().int() adds to undeclared int fields, and
|
|
6
|
+
// "execution": {"taskSupport": "forbidden"}. Explicit bounds like .min(1) are left alone, the server-side
|
|
7
|
+
// Zod schema still enforces the removed ones, and clients treat absent taskSupport as "forbidden".
|
|
22
8
|
function stripSafeIntBounds(node: unknown): void {
|
|
23
9
|
if (Array.isArray(node)) {
|
|
24
10
|
for (const child of node) stripSafeIntBounds(child);
|
|
@@ -54,17 +40,9 @@ function compactTool(tool: ToolDefinition): void {
|
|
|
54
40
|
}
|
|
55
41
|
}
|
|
56
42
|
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* are registered as Zod shapes and converted inside its list handler), so
|
|
61
|
-
* post-processing the response is the only place this can happen.
|
|
62
|
-
*
|
|
63
|
-
* Degrades to a no-op if the SDK ever stops storing handlers where we look for
|
|
64
|
-
* them — the definitions go back to being verbose, nothing breaks. The test in
|
|
65
|
-
* tool-schema.test.ts asserts the compaction is live so that degradation is
|
|
66
|
-
* caught in CI rather than in production token bills.
|
|
67
|
-
*/
|
|
43
|
+
/** Wraps the SDK's tools/list handler to compact each definition on the way out; the SDK converts Zod
|
|
44
|
+
* to JSON Schema internally, so the response is the only interception point. Degrades to a no-op if the
|
|
45
|
+
* SDK moves its handlers, which tool-schema.test.ts asserts against so CI catches it, not token bills. */
|
|
68
46
|
export function compactToolDefinitions(server: McpServer): void {
|
|
69
47
|
const handlers = (
|
|
70
48
|
server.server as unknown as {
|
package/README.md
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
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 (47)
|
|
35
|
-
|
|
36
|
-
| Tool | Description |
|
|
37
|
-
|------|-------------|
|
|
38
|
-
| `opsee_get_me` | Get your profile info |
|
|
39
|
-
| `opsee_list_users` | List company users (resolve assignee IDs) |
|
|
40
|
-
| `opsee_get_user` | Get user details |
|
|
41
|
-
| `opsee_list_projects` | List all projects |
|
|
42
|
-
| `opsee_get_project` | Get project details |
|
|
43
|
-
| `opsee_list_tasks` | List tasks with filters |
|
|
44
|
-
| `opsee_get_task` | Get task details; `includeContext: true` adds labels, comments, dependencies, parent, subtasks in the same call |
|
|
45
|
-
| `opsee_create_task` | Create a new task (accepts inline labelIds[] to skip follow-up attach calls) |
|
|
46
|
-
| `opsee_update_task` | Update task fields (accepts labelIds[] with replace-set semantics) |
|
|
47
|
-
| `opsee_bulk_update_tasks` | Update many tasks in one transaction — assignee, priority, column, cycle, storyPoints, estimatedHours, parentTaskId, labelIds |
|
|
48
|
-
| `opsee_replace_task_labels` | Replace a single task's full set of labels |
|
|
49
|
-
| `opsee_delete_task` | Delete a task by ID |
|
|
50
|
-
| `opsee_bulk_delete_tasks` | Delete many tasks in one call |
|
|
51
|
-
| `opsee_list_comments` | List comments on a task |
|
|
52
|
-
| `opsee_add_comment` | Add a comment to a task |
|
|
53
|
-
| `opsee_edit_comment` | Edit an existing comment |
|
|
54
|
-
| `opsee_delete_comment` | Delete a comment |
|
|
55
|
-
| `opsee_list_labels` | List labels in a project |
|
|
56
|
-
| `opsee_get_label` | Get label details |
|
|
57
|
-
| `opsee_create_label` | Create a label |
|
|
58
|
-
| `opsee_update_label` | Update a label |
|
|
59
|
-
| `opsee_delete_label` | Delete a label |
|
|
60
|
-
| `opsee_attach_label_to_task` | Attach a label to a task |
|
|
61
|
-
| `opsee_detach_label_from_task` | Detach a label from a task (pass TaskLabel join-row id) |
|
|
62
|
-
| `opsee_list_task_labels` | List labels attached to a task |
|
|
63
|
-
| `opsee_create_task_dependency` | Add a typed task-to-task relation (BLOCKS / BLOCKED_BY / DUPLICATES / RELATES_TO) |
|
|
64
|
-
| `opsee_list_task_dependencies` | List a task's dependencies (both directions) |
|
|
65
|
-
| `opsee_delete_task_dependency` | Delete a task dependency by ID |
|
|
66
|
-
| `opsee_list_task_types` | Get task types (Bug, Feature, etc.) |
|
|
67
|
-
| `opsee_list_task_priorities` | Get priority levels |
|
|
68
|
-
| `opsee_list_boards` | List Kanban boards |
|
|
69
|
-
| `opsee_list_board_columns` | Get board columns/statuses |
|
|
70
|
-
| `opsee_list_cycles` | List cycles/sprints |
|
|
71
|
-
| `opsee_get_cycle` | Get cycle details |
|
|
72
|
-
| `opsee_create_cycle` | Create a new cycle |
|
|
73
|
-
| `opsee_update_cycle` | Update a cycle's fields |
|
|
74
|
-
| `opsee_delete_cycle` | Delete a cycle |
|
|
75
|
-
| `opsee_list_doc_spaces` | List doc spaces in a project |
|
|
76
|
-
| `opsee_get_doc_space` | Get doc space details |
|
|
77
|
-
| `opsee_create_doc_space` | Create a doc space |
|
|
78
|
-
| `opsee_update_doc_space` | Update a doc space |
|
|
79
|
-
| `opsee_delete_doc_space` | Delete a doc space |
|
|
80
|
-
| `opsee_list_doc_pages` | List pages in a doc space |
|
|
81
|
-
| `opsee_get_doc_page` | Read a doc page |
|
|
82
|
-
| `opsee_create_doc_page` | Create a new doc page |
|
|
83
|
-
| `opsee_update_doc_page` | Update a doc page (read-merge-write, with version snapshotting) |
|
|
84
|
-
| `opsee_delete_doc_page` | Delete a doc page |
|
|
85
|
-
| `opsee_set_doc_page_archived` | Archive or unarchive a doc page |
|
|
86
|
-
| `opsee_list_doc_templates` | List doc templates in a project |
|
|
87
|
-
| `opsee_create_doc_template` | Create a doc template |
|
|
88
|
-
| `opsee_update_doc_template` | Update a doc template |
|
|
89
|
-
| `opsee_delete_doc_template` | Delete a doc template |
|
|
90
|
-
| `opsee_list_task_doc_pages` | List doc pages attached to a task |
|
|
91
|
-
| `opsee_list_milestone_doc_pages` | List doc pages attached to a milestone |
|
|
92
|
-
| `opsee_attach_doc_page` | Attach a doc page to a task or a milestone |
|
|
93
|
-
| `opsee_detach_doc_page` | Detach a doc page from a task or a milestone |
|
|
94
|
-
| `opsee_list_repositories` | List connected repositories |
|
|
95
|
-
|
|
96
|
-
## Task description format
|
|
97
|
-
|
|
98
|
-
The `description` field on tasks accepts and returns user-supplied text, but the Opsee backend may normalize the stored representation. The exact round-trip behavior depends on the backend version and is not yet fully documented here.
|
|
99
|
-
|
|
100
|
-
### Acceptance criteria → description checklist
|
|
101
|
-
|
|
102
|
-
Write acceptance criteria **inline in the task description** using a `## Acceptance criteria` heading followed by markdown checkbox lines. The frontend (BlockNote) renders those as clickable checkboxes; users can toggle them in the UI.
|
|
103
|
-
|
|
104
|
-
```
|
|
105
|
-
## Acceptance criteria
|
|
106
|
-
|
|
107
|
-
- [ ] WaiveLateFee RPC compiles and builds
|
|
108
|
-
- [ ] Admin can waive full or partial late fee with reason
|
|
109
|
-
- [ ] Apply-as-credit reduces next payment amount
|
|
110
|
-
- [x] Audit log entry with before/after snapshots
|
|
111
|
-
- [ ] Modal UI matches mockup
|
|
112
|
-
- [ ] Non-admin users cannot access waive action
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
This is the path `/tpm` and any other AI agent should use when creating or updating tasks via `opsee_create_task` / `opsee_update_task`. The heading casing doesn't matter to the parser (`## Acceptance Criteria` works too).
|
|
116
|
-
|
|
117
|
-
**Do not use the structured `opsee_add_acceptance_criterion` tool for typical AC.** That tool writes to a separate `acceptance_criteria` DB table that does NOT surface in the task detail UI; it's reserved for automation paths that need machine-trackable criteria outside the description (PR-to-criterion mapping, AI workflow outputs, reporting).
|
|
118
|
-
|
|
119
|
-
### What's confirmed
|
|
120
|
-
|
|
121
|
-
- The MCP wrapper passes `description` to the backend unmodified. Whatever transformation happens is server-side.
|
|
122
|
-
- Read tools (`opsee_get_task`, `opsee_list_tasks`) return whatever the backend has stored, byte-for-byte.
|
|
123
|
-
|
|
124
|
-
### What needs empirical verification
|
|
125
|
-
|
|
126
|
-
The following test plan, run against a live Opsee instance, captures round-trip behavior for the three common input forms. **Results have not yet been recorded here — run the test and fill in the observed strings below.**
|
|
127
|
-
|
|
128
|
-
```text
|
|
129
|
-
# Plain text with separator and trailer
|
|
130
|
-
opsee_create_task(projectId=<P>, title="rt plain",
|
|
131
|
-
description="line one\nline two\n---\nLabels: x")
|
|
132
|
-
opsee_get_task(taskId=<id>) → record exact returned description
|
|
133
|
-
|
|
134
|
-
# Markdown with headers, list, fenced code
|
|
135
|
-
opsee_create_task(projectId=<P>, title="rt md",
|
|
136
|
-
description="# H1\n- bullet\n\n```ts\ncode\n```")
|
|
137
|
-
opsee_get_task(taskId=<id>) → record exact returned description
|
|
138
|
-
|
|
139
|
-
# BlockNote JSON
|
|
140
|
-
opsee_create_task(projectId=<P>, title="rt blocknote",
|
|
141
|
-
description='[{"type":"paragraph","content":[{"type":"text","text":"x"}]}]')
|
|
142
|
-
opsee_get_task(taskId=<id>) → record exact returned description
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Observed round-trip outcomes
|
|
146
|
-
|
|
147
|
-
| Input form | Stored form (from `opsee_get_task`) | Notes |
|
|
148
|
-
|---|---|---|
|
|
149
|
-
| Plain text | _Pending empirical verification_ | |
|
|
150
|
-
| Markdown | _Pending empirical verification_ | |
|
|
151
|
-
| BlockNote JSON | _Pending empirical verification_ | |
|
|
152
|
-
|
|
153
|
-
> If the round trip is lossy (e.g. plain text becomes BlockNote JSON on read), callers relying on byte-identical round-trip should treat `description` as backend-defined and avoid parsing trailers; use first-class fields (`opsee_attach_label_to_task`, `opsee_add_comment`, etc.) for structured metadata instead.
|
|
154
|
-
|
|
155
|
-
## Remote MCP Server (Recommended)
|
|
156
|
-
|
|
157
|
-
The remote server runs in the cloud with OAuth 2.0 authentication - no local setup or tokens needed.
|
|
158
|
-
|
|
159
|
-
### Environments
|
|
160
|
-
|
|
161
|
-
| Environment | URL | Opsee Instance |
|
|
162
|
-
|-------------|-----|----------------|
|
|
163
|
-
| **Production** | `https://mcp.api.opsee.ai/mcp` | `https://opsee.ai` |
|
|
164
|
-
| **Development** | `https://opsee-development.mcp.cls.codilas.link/mcp` | `https://opsee-development.cdn.codilas.link` |
|
|
165
|
-
|
|
166
|
-
### Claude Code
|
|
167
|
-
|
|
168
|
-
```bash
|
|
169
|
-
# Production
|
|
170
|
-
claude mcp add opsee --transport http https://mcp.api.opsee.ai/mcp
|
|
171
|
-
|
|
172
|
-
# Development
|
|
173
|
-
claude mcp add opsee-dev --transport http https://opsee-development.mcp.cls.codilas.link/mcp
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Restart Claude Code. On first use, your browser opens for Opsee login. After authenticating, all tools are available immediately.
|
|
177
|
-
|
|
178
|
-
You can have both environments connected at the same time - use tools prefixed with the server name (e.g. `opsee` for production, `opsee-dev` for development).
|
|
179
|
-
|
|
180
|
-
### Cursor / Other MCP Clients
|
|
181
|
-
|
|
182
|
-
```json
|
|
183
|
-
{
|
|
184
|
-
"mcpServers": {
|
|
185
|
-
"opsee": {
|
|
186
|
-
"type": "http",
|
|
187
|
-
"url": "https://mcp.api.opsee.ai/mcp"
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Replace the URL with the development endpoint if targeting that environment.
|
|
194
|
-
|
|
195
|
-
Your MCP client handles the OAuth flow automatically - just sign in when prompted.
|
|
196
|
-
|
|
197
|
-
### How it works
|
|
198
|
-
|
|
199
|
-
1. Client connects to the MCP endpoint (e.g. `https://mcp.api.opsee.ai/mcp`)
|
|
200
|
-
2. Server returns 401 with OAuth discovery metadata
|
|
201
|
-
3. Client registers itself and opens the Opsee login page in your browser
|
|
202
|
-
4. After login, the client exchanges the auth code for an access token
|
|
203
|
-
5. All subsequent tool calls use the token automatically
|
|
204
|
-
|
|
205
|
-
## Local MCP Server (Alternative)
|
|
206
|
-
|
|
207
|
-
Run the MCP server locally via npx. Requires a one-time login to get a token.
|
|
208
|
-
|
|
209
|
-
### 1. Authenticate
|
|
210
|
-
|
|
211
|
-
```bash
|
|
212
|
-
npx @opsee/mcp-server login
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### 2. Add to Claude Code
|
|
216
|
-
|
|
217
|
-
```bash
|
|
218
|
-
claude mcp add opsee -- npx @opsee/mcp-server serve
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
### Cursor / Other MCP Clients (Local)
|
|
222
|
-
|
|
223
|
-
```json
|
|
224
|
-
{
|
|
225
|
-
"mcpServers": {
|
|
226
|
-
"opsee": {
|
|
227
|
-
"command": "npx",
|
|
228
|
-
"args": ["@opsee/mcp-server", "serve"]
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
### CI / Headless Environments
|
|
235
|
-
|
|
236
|
-
Skip the browser login by setting a token directly:
|
|
237
|
-
|
|
238
|
-
```json
|
|
239
|
-
{
|
|
240
|
-
"mcpServers": {
|
|
241
|
-
"opsee": {
|
|
242
|
-
"command": "npx",
|
|
243
|
-
"args": ["@opsee/mcp-server", "serve"],
|
|
244
|
-
"env": {
|
|
245
|
-
"OPSEE_API_TOKEN": "your-jwt-token"
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
## Self-Hosting the Remote Server
|
|
253
|
-
|
|
254
|
-
Deploy the MCP server in your own infrastructure using Docker.
|
|
255
|
-
|
|
256
|
-
### Docker
|
|
257
|
-
|
|
258
|
-
```bash
|
|
259
|
-
docker build -f mcp/Dockerfile . -t opsee-mcp-server
|
|
260
|
-
|
|
261
|
-
docker run -p 3100:3100 \
|
|
262
|
-
-e MCP_SERVER_URL=https://mcp.yourdomain.com \
|
|
263
|
-
-e OPSEE_API_URL=http://your-backend:8080 \
|
|
264
|
-
-e OPSEE_APP_URL=https://your-frontend.com \
|
|
265
|
-
opsee-mcp-server
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
### Kubernetes
|
|
269
|
-
|
|
270
|
-
The server includes Kustomize configs following the same pattern as the backend:
|
|
271
|
-
|
|
272
|
-
```bash
|
|
273
|
-
# Development
|
|
274
|
-
kubectl apply -k mcp/k8/environments/development/ -n opsee-development
|
|
275
|
-
|
|
276
|
-
# Production
|
|
277
|
-
kubectl apply -k mcp/k8/environments/production/ -n opsee-production
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Environment Variables
|
|
281
|
-
|
|
282
|
-
| Variable | Description | Default |
|
|
283
|
-
|----------|-------------|---------|
|
|
284
|
-
| `MCP_SERVER_URL` | Public URL of this MCP server (for OAuth redirects) | `http://localhost:3100` |
|
|
285
|
-
| `MCP_HOST` | Bind host | `0.0.0.0` |
|
|
286
|
-
| `MCP_PORT` | Bind port | `3100` |
|
|
287
|
-
| `OPSEE_API_URL` | Backend Connect RPC URL | `https://grpc.api.opsee.ai` |
|
|
288
|
-
| `OPSEE_APP_URL` | Frontend URL (OAuth login page) | `https://opsee.ai` |
|
|
289
|
-
| `OPSEE_API_TOKEN` | Direct JWT token for local mode | - |
|
|
290
|
-
| `OPSEE_CREDENTIALS_PATH` | Override credential file path | `~/.opsee/credentials.json` |
|
|
291
|
-
|
|
292
|
-
## Local Development
|
|
293
|
-
|
|
294
|
-
```bash
|
|
295
|
-
# Start backend + frontend locally, then:
|
|
296
|
-
|
|
297
|
-
# Local mode (stdio):
|
|
298
|
-
OPSEE_APP_URL=http://localhost:5173 OPSEE_API_URL=http://localhost:9990 npx @opsee/mcp-server login
|
|
299
|
-
claude mcp add opsee -e OPSEE_API_URL=http://localhost:9990 -- npx @opsee/mcp-server serve
|
|
300
|
-
|
|
301
|
-
# Remote mode (HTTP):
|
|
302
|
-
MCP_SERVER_URL=http://localhost:3100 OPSEE_API_URL=http://localhost:9990 OPSEE_APP_URL=http://localhost:5173 npx @opsee/mcp-server serve-remote
|
|
303
|
-
claude mcp add opsee-local --transport http http://localhost:3100/mcp
|
|
304
|
-
```
|