@opsee/mcp-server 0.9.16 → 0.9.18

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.
@@ -17,7 +17,7 @@ export function registerCycleTools(
17
17
  server.tool(
18
18
  "opsee_list_cycles",
19
19
  "List cycles/sprints in an Opsee project.",
20
- { projectId: z.number().describe("The project ID") },
20
+ { projectId: z.number().int().min(1).describe("The project ID") },
21
21
  { readOnlyHint: true, destructiveHint: false },
22
22
  async ({ projectId }) => {
23
23
  try {
@@ -33,7 +33,7 @@ export function registerCycleTools(
33
33
  server.tool(
34
34
  "opsee_get_cycle",
35
35
  "Get details of a specific cycle/sprint by ID.",
36
- { cycleId: z.number().describe("The cycle ID") },
36
+ { cycleId: z.number().int().min(1).describe("The cycle ID") },
37
37
  { readOnlyHint: true, destructiveHint: false },
38
38
  async ({ cycleId }) => {
39
39
  try {
@@ -51,7 +51,7 @@ export function registerCycleTools(
51
51
  "opsee_create_cycle",
52
52
  "Create a new cycle/sprint in an Opsee project.",
53
53
  {
54
- projectId: z.number().describe("The project ID"),
54
+ projectId: z.number().int().min(1).describe("The project ID"),
55
55
  name: z.string().describe("Cycle name"),
56
56
  startDate: z.string().describe("Start date (ISO 8601, e.g. 2026-03-25)"),
57
57
  endDate: z.string().describe("End date (ISO 8601, e.g. 2026-04-08)"),
@@ -88,7 +88,7 @@ export function registerCycleTools(
88
88
  "opsee_update_cycle",
89
89
  "Update a cycle's fields (patch semantics — only provided fields change). Fetches the current cycle first, merges your changes, sends the full update.",
90
90
  {
91
- cycleId: z.number().describe("The cycle ID to update"),
91
+ cycleId: z.number().int().min(1).describe("The cycle ID to update"),
92
92
  name: z.string().optional().describe("New name"),
93
93
  goal: z.string().optional().describe("New goal"),
94
94
  startDate: z.string().optional().describe("New start date (ISO 8601)"),
@@ -134,8 +134,8 @@ export function registerCycleTools(
134
134
  "opsee_close_cycle",
135
135
  "Close a cycle (mark inactive) and optionally carry every remaining task forward to a target cycle. Composes a single bulk_move_to_cycle + EditCycle, so /tpm cycle close becomes one call instead of a loop. Pass createNextCycle:true with name/startDate/endDate to also create the next cycle and carry forward into it in the same call.",
136
136
  {
137
- cycleId: z.number().describe("ID of the cycle to close"),
138
- nextCycleId: z.number().optional().describe("Existing cycle to carry remaining tasks into. Mutually exclusive with createNextCycle."),
137
+ cycleId: z.number().int().min(1).describe("ID of the cycle to close"),
138
+ nextCycleId: z.number().int().min(1).optional().describe("Existing cycle to carry remaining tasks into. Mutually exclusive with createNextCycle."),
139
139
  createNextCycle: z.boolean().optional().describe("If true, create the next cycle in this project (requires name/startDate/endDate) and carry tasks into it"),
140
140
  nextCycleName: z.string().optional().describe("Name of the cycle to create when createNextCycle:true"),
141
141
  nextCycleStartDate: z.string().optional().describe("ISO 8601 start date when createNextCycle:true"),
@@ -233,7 +233,7 @@ export function registerCycleTools(
233
233
  server.tool(
234
234
  "opsee_get_active_cycle",
235
235
  "Return the currently-active cycle for a project, or 'No active cycle' if none.",
236
- { projectId: z.number().describe("The project ID") },
236
+ { projectId: z.number().int().min(1).describe("The project ID") },
237
237
  { readOnlyHint: true, destructiveHint: false },
238
238
  async ({ projectId }) => {
239
239
  try {
@@ -253,7 +253,7 @@ export function registerCycleTools(
253
253
  server.tool(
254
254
  "opsee_delete_cycle",
255
255
  "Delete a cycle by ID. This is permanent. Tasks assigned to the cycle are not deleted, but their cycle reference is cleared.",
256
- { cycleId: z.number().describe("The cycle ID") },
256
+ { cycleId: z.number().int().min(1).describe("The cycle ID") },
257
257
  { readOnlyHint: false, destructiveHint: true },
258
258
  async ({ cycleId }) => {
259
259
  try {
package/src/tools/docs.ts CHANGED
@@ -28,7 +28,7 @@ export function registerDocTools(
28
28
  server.tool(
29
29
  "opsee_list_doc_spaces",
30
30
  "List documentation spaces in an Opsee project.",
31
- { projectId: z.number().describe("The project ID") },
31
+ { projectId: z.number().int().min(1).describe("The project ID") },
32
32
  { readOnlyHint: true, destructiveHint: false },
33
33
  async ({ projectId }) => {
34
34
  try {
@@ -55,7 +55,7 @@ export function registerDocTools(
55
55
  server.tool(
56
56
  "opsee_list_doc_pages",
57
57
  "List documentation pages in a doc space.",
58
- { docSpaceId: z.number().describe("The doc space ID") },
58
+ { docSpaceId: z.number().int().min(1).describe("The doc space ID") },
59
59
  { readOnlyHint: true, destructiveHint: false },
60
60
  async ({ docSpaceId }) => {
61
61
  try {
@@ -82,7 +82,7 @@ export function registerDocTools(
82
82
  server.tool(
83
83
  "opsee_get_doc_page",
84
84
  "Read a documentation page's content by ID.",
85
- { pageId: z.number().describe("The doc page ID") },
85
+ { pageId: z.number().int().min(1).describe("The doc page ID") },
86
86
  { readOnlyHint: true, destructiveHint: false },
87
87
  async ({ pageId }) => {
88
88
  try {
@@ -101,10 +101,10 @@ export function registerDocTools(
101
101
  "opsee_create_doc_page",
102
102
  "Create a new documentation page in an Opsee project.",
103
103
  {
104
- projectId: z.number().describe("The project ID"),
104
+ projectId: z.number().int().min(1).describe("The project ID"),
105
105
  title: z.string().describe("Page title"),
106
106
  content: z.string().describe("Page content (text or JSON)"),
107
- parentPageId: z.number().optional().describe("Parent page ID for nested pages"),
107
+ parentPageId: z.number().int().min(1).optional().describe("Parent page ID for nested pages"),
108
108
  },
109
109
  { readOnlyHint: false, destructiveHint: false },
110
110
  async ({ projectId, title, content, parentPageId }) => {
@@ -27,7 +27,7 @@ export function registerInitiativeTools(
27
27
  "opsee_list_initiatives",
28
28
  "List initiatives in an Opsee project. Pass `search` to find a specific initiative by keyword — it matches the title and summary, tokenized, and ranks the best match first (most-recently-updated breaks ties). Use this to resume work by name, e.g. for \"resume the auth-refactor initiative\" call with search: \"auth\". Without `search`, returns all initiatives, most recent first.",
29
29
  {
30
- projectId: z.number().describe("The project ID"),
30
+ projectId: z.number().int().min(1).describe("The project ID"),
31
31
  search: z.string().optional().describe("Keyword to match against initiative title + summary (ranked); omit to list all by recency"),
32
32
  },
33
33
  { readOnlyHint: true, destructiveHint: false },
@@ -49,7 +49,7 @@ export function registerInitiativeTools(
49
49
  server.tool(
50
50
  "opsee_get_initiative",
51
51
  "Get details of a specific initiative by ID (title, status, summary, core idea). To pick up or resume WORK on it — the task tree, memory log, and PRs together — call opsee_get_initiative_context instead.",
52
- { initiativeId: z.number().describe("The initiative ID") },
52
+ { initiativeId: z.number().int().min(1).describe("The initiative ID") },
53
53
  { readOnlyHint: true, destructiveHint: false },
54
54
  async ({ initiativeId }) => {
55
55
  try {
@@ -67,12 +67,12 @@ export function registerInitiativeTools(
67
67
  "opsee_create_initiative",
68
68
  "Create a new initiative in an Opsee project. After creating, expand the plan in the core idea (the coreIdea field); when the plan is ready, propose a decomposition with opsee_decompose_initiative rather than creating tasks by hand.",
69
69
  {
70
- projectId: z.number().describe("The project ID"),
70
+ projectId: z.number().int().min(1).describe("The project ID"),
71
71
  title: z.string().describe("Initiative title"),
72
72
  summary: z.string().optional().describe("Short summary of the initiative"),
73
73
  status: z.enum(INITIATIVE_STATUSES).optional().describe("Initiative status. Defaults to 'draft'"),
74
74
  anchorType: z.enum(ANCHOR_TYPES).optional().describe("Anchor type (none | task | milestone | cycle). Defaults to 'none'"),
75
- anchorId: z.number().optional().describe("Anchor entity ID. Required when anchorType is not 'none'"),
75
+ anchorId: z.number().int().min(1).optional().describe("Anchor entity ID. Required when anchorType is not 'none'"),
76
76
  coreIdea: z.string().optional().describe("Core idea body (the initiative's primary description). Markdown is supported and recommended — headings (##), bullets (-), checklists, code fences, **bold**/*italic*/`code`/links all render as formatted blocks in the UI (same behavior as task descriptions). Written as-is; the UI parses the markdown on read and re-saves it as BlockNote JSON on the next manual edit."),
77
77
  coreIdeaContentType: z.string().optional().describe("Content type of coreIdea: 'text' (markdown — the default) or 'json' (BlockNote). Leave unset and just write markdown."),
78
78
  },
@@ -105,12 +105,12 @@ export function registerInitiativeTools(
105
105
  "opsee_update_initiative",
106
106
  "Update an initiative's fields. Fetches the current initiative first, then applies only the provided changes. Status lifecycle: draft → active (opsee_decompose_initiative flips this automatically) → completed when the work is done. For dropped work, set status to 'abandoned' WITH a reason (in the summary or a memory entry) rather than deleting — that preserves the shared brain.",
107
107
  {
108
- initiativeId: z.number().describe("The initiative ID to update"),
108
+ initiativeId: z.number().int().min(1).describe("The initiative ID to update"),
109
109
  title: z.string().optional().describe("New title"),
110
110
  summary: z.string().optional().describe("New summary"),
111
111
  status: z.enum(INITIATIVE_STATUSES).optional().describe("New status"),
112
112
  anchorType: z.enum(ANCHOR_TYPES).optional().describe("New anchor type"),
113
- anchorId: z.number().optional().describe("New anchor entity ID"),
113
+ anchorId: z.number().int().min(1).optional().describe("New anchor entity ID"),
114
114
  coreIdea: z.string().optional().describe("New core idea body. Markdown is supported and renders as formatted blocks in the UI (same as task descriptions)."),
115
115
  coreIdeaContentType: z.string().optional().describe("Content type for coreIdea: 'text' (markdown — default) or 'json' (BlockNote)."),
116
116
  },
@@ -146,7 +146,7 @@ export function registerInitiativeTools(
146
146
  server.tool(
147
147
  "opsee_delete_initiative",
148
148
  "Delete an initiative permanently. Prefer setting status to 'abandoned' (via opsee_update_initiative) with a reason — that preserves the initiative's memory/shared brain for later reference. Only hard-delete an initiative created in error.",
149
- { initiativeId: z.number().describe("The initiative ID to delete") },
149
+ { initiativeId: z.number().int().min(1).describe("The initiative ID to delete") },
150
150
  { readOnlyHint: false, destructiveHint: true },
151
151
  async ({ initiativeId }) => {
152
152
  try {
@@ -163,8 +163,8 @@ export function registerInitiativeTools(
163
163
  "opsee_get_initiative_context",
164
164
  "Load an entire Initiative work session in one call: the core-idea doc body, the execution task tree with parallel slices (what can run concurrently vs sequentially), the append-only memory log, and rolled-up PRs. Call this FIRST when picking up or resuming work on an Initiative. The response ends with a working agreement: treat this context as the source of truth, log decisions/blockers/learnings as you go with opsee_add_initiative_memory, and call opsee_checkpoint when you finish a unit of work.",
165
165
  {
166
- initiativeId: z.number().describe("The initiative ID"),
167
- memoryLimit: z.number().optional().describe("Cap on memory entries returned (most recent first); 0 or omitted = service default"),
166
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
167
+ memoryLimit: z.number().int().min(0).optional().describe("Cap on memory entries returned (most recent first); 0 or omitted = service default"),
168
168
  },
169
169
  { readOnlyHint: true, destructiveHint: false },
170
170
  async ({ initiativeId, memoryLimit }) => {
@@ -192,7 +192,7 @@ export function registerInitiativeTools(
192
192
  server.tool(
193
193
  "opsee_get_initiative_graph",
194
194
  "Get the task dependency graph for an initiative, including parallel execution batches (slices). Use the slices to choose the next batch of tasks that can be worked concurrently. (opsee_get_initiative_context already includes this graph — call this only when you need the graph on its own.)",
195
- { initiativeId: z.number().describe("The initiative ID") },
195
+ { initiativeId: z.number().int().min(1).describe("The initiative ID") },
196
196
  { readOnlyHint: true, destructiveHint: false },
197
197
  async ({ initiativeId }) => {
198
198
  try {
@@ -210,15 +210,15 @@ export function registerInitiativeTools(
210
210
  "opsee_decompose_initiative",
211
211
  "Materialize a proposed breakdown as real Tasks under the Initiative. PROPOSE FIRST: present the proposed task tree (titles, structure, dependencies) to the human and get their approval BEFORE calling this — that approval is the \"confirm\" step. Nothing is created until you call it, so to let the human decline, simply don't call it. Calling it materializes the tasks onto the board in one atomic operation and moves a draft Initiative to active (the conclude→execute transition). Wire the tree with temporary `ref` ids: set `parentRef` to nest a subtask, and add `edges` (from_ref blocks to_ref) to express sequencing — tasks with no blocking edge between them become parallel slices. All-or-nothing: a bad ref rolls the whole thing back.",
212
212
  {
213
- initiativeId: z.number().describe("The initiative ID"),
213
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
214
214
  tasks: z.array(z.object({
215
215
  ref: z.string().describe("Temporary reference ID used to wire edges and parent relationships"),
216
216
  title: z.string().describe("Task title"),
217
217
  description: z.string().optional().describe("Task description"),
218
218
  parentRef: z.string().optional().describe("Ref of the parent task (to nest this as a subtask)"),
219
- taskTypeId: z.number().optional().describe("Task type ID"),
220
- taskPriorityId: z.number().optional().describe("Task priority ID"),
221
- boardColumnId: z.number().optional().describe("Board column ID (status)"),
219
+ taskTypeId: z.number().int().min(1).optional().describe("Task type ID"),
220
+ taskPriorityId: z.number().int().min(1).optional().describe("Task priority ID"),
221
+ boardColumnId: z.number().int().min(1).optional().describe("Board column ID (status)"),
222
222
  })).describe("Tasks to create under this initiative"),
223
223
  edges: z.array(z.object({
224
224
  fromRef: z.string().describe("Ref of the source task"),
@@ -281,16 +281,16 @@ export function registerInitiativeTools(
281
281
  "6. Call opsee_reconcile_initiative with dryRun:false to apply the approved plan.",
282
282
  ].join("\n"),
283
283
  {
284
- initiativeId: z.number().describe("The initiative ID"),
284
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
285
285
  tasks: z.array(z.object({
286
286
  ref: z.string().describe("Temporary reference ID used to wire edges and parent relationships"),
287
287
  title: z.string().describe("Task title"),
288
288
  description: z.string().optional().describe("Task description"),
289
289
  parentRef: z.string().optional().describe("Ref of the parent task (to nest this as a subtask)"),
290
- taskTypeId: z.number().optional().describe("Task type ID"),
291
- taskPriorityId: z.number().optional().describe("Task priority ID"),
292
- boardColumnId: z.number().optional().describe("Board column ID (status)"),
293
- existingTaskId: z.number().optional().describe("Real task ID from the current Initiative — tag kept tasks with this value (obtained from opsee_get_initiative_context). Omit for new tasks."),
290
+ taskTypeId: z.number().int().min(1).optional().describe("Task type ID"),
291
+ taskPriorityId: z.number().int().min(1).optional().describe("Task priority ID"),
292
+ boardColumnId: z.number().int().min(1).optional().describe("Board column ID (status)"),
293
+ existingTaskId: z.number().int().min(1).optional().describe("Real task ID from the current Initiative — tag kept tasks with this value (obtained from opsee_get_initiative_context). Omit for new tasks."),
294
294
  })).describe("Proposed task tree — include kept tasks with existingTaskId, new tasks without"),
295
295
  edges: z.array(z.object({
296
296
  fromRef: z.string().describe("Ref of the source task"),
@@ -337,12 +337,12 @@ export function registerInitiativeTools(
337
337
  "opsee_add_initiative_memory",
338
338
  "Log a SINGLE decision, outcome, learning, blocker, or context entry to the Initiative's append-only memory the moment it happens — the as-you-go granular logger. Cite the source task or PR where relevant. For end-of-unit wrap-ups, use opsee_checkpoint instead.",
339
339
  {
340
- initiativeId: z.number().describe("The initiative ID"),
340
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
341
341
  kind: z.enum(MEMORY_KINDS).describe("Entry kind: decision | outcome | learning | blocker | context"),
342
342
  body: z.string().describe("The memory entry body text"),
343
343
  contentType: z.string().optional().describe("Content type of body (e.g. 'text'). Defaults to 'text'"),
344
344
  sourceType: z.enum(MEMORY_SOURCE_TYPES).optional().describe("Source type: none | task | pull_request | doc | workflow. Defaults to 'none'"),
345
- sourceId: z.number().optional().describe("ID of the source entity (task ID, PR ID, etc.)"),
345
+ sourceId: z.number().int().min(1).optional().describe("ID of the source entity (task ID, PR ID, etc.)"),
346
346
  sourceUrl: z.string().optional().describe("URL linking to the source"),
347
347
  },
348
348
  { readOnlyHint: false, destructiveHint: false },
@@ -373,10 +373,10 @@ export function registerInitiativeTools(
373
373
  "opsee_checkpoint",
374
374
  "End-of-unit wrap-up for an Initiative: records what a unit of work accomplished to the Initiative's memory and links what it touched. Call this when you finish a coherent chunk of work (a task, a PR, an investigation) — distinct from opsee_add_initiative_memory, which logs a single decision/blocker the moment it happens. Defaults the entry kind to 'outcome'.",
375
375
  {
376
- initiativeId: z.number().describe("The initiative ID"),
376
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
377
377
  summary: z.string().describe("What this unit of work accomplished and what's next"),
378
378
  kind: z.enum(MEMORY_KINDS).optional().describe("Memory entry kind. Defaults to 'outcome'"),
379
- taskId: z.number().optional().describe("The task this work advanced (links the entry to it)"),
379
+ taskId: z.number().int().min(1).optional().describe("The task this work advanced (links the entry to it)"),
380
380
  prUrl: z.string().optional().describe("The PR/MR this work produced (recorded as the entry's source URL)"),
381
381
  },
382
382
  { readOnlyHint: false, destructiveHint: false },
@@ -420,10 +420,10 @@ export function registerInitiativeTools(
420
420
  "opsee_list_initiative_memory",
421
421
  "List memory entries for an initiative (newest first). Optionally filter by kind, source type, or source ID.",
422
422
  {
423
- initiativeId: z.number().describe("The initiative ID"),
423
+ initiativeId: z.number().int().min(1).describe("The initiative ID"),
424
424
  kinds: z.array(z.string()).optional().describe("Filter by entry kinds (decision | outcome | learning | blocker | context)"),
425
425
  sourceType: z.string().optional().describe("Filter by source type (none | task | pull_request | doc | workflow)"),
426
- sourceId: z.number().optional().describe("Filter by source entity ID"),
426
+ sourceId: z.number().int().min(1).optional().describe("Filter by source entity ID"),
427
427
  },
428
428
  { readOnlyHint: true, destructiveHint: false },
429
429
  async ({ initiativeId, kinds, sourceType, sourceId }) => {
@@ -17,9 +17,9 @@ export function registerLabelTools(
17
17
  "opsee_list_labels",
18
18
  "List labels defined in an Opsee project. Use opsee_list_task_labels for labels attached to a specific task.",
19
19
  {
20
- projectId: z.number().describe("The project ID"),
21
- page: z.number().optional().describe("Page number (default: 1)"),
22
- pageSize: z.number().optional().describe("Items per page (default: 50)"),
20
+ projectId: z.number().int().min(1).describe("The project ID"),
21
+ page: z.number().int().min(1).optional().describe("Page number (default: 1)"),
22
+ pageSize: z.number().int().min(1).optional().describe("Items per page (default: 50)"),
23
23
  },
24
24
  { readOnlyHint: true, destructiveHint: false },
25
25
  async ({ projectId, page, pageSize }) => {
@@ -59,7 +59,7 @@ export function registerLabelTools(
59
59
  "opsee_create_label",
60
60
  "Create a new label in an Opsee project.",
61
61
  {
62
- projectId: z.number().describe("The project ID"),
62
+ projectId: z.number().int().min(1).describe("The project ID"),
63
63
  name: z.string().min(1).max(50).describe("Label name"),
64
64
  color: z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("Hex color, e.g. #ffaa00 or ffaa00"),
65
65
  description: z.string().optional().describe("Label description"),
@@ -88,7 +88,7 @@ export function registerLabelTools(
88
88
  "opsee_update_label",
89
89
  "Update a label's fields (patch semantics - only provided fields are changed). Fetches the current label first, merges your changes, and sends the full update.",
90
90
  {
91
- labelId: z.number().describe("The label ID to update"),
91
+ labelId: z.number().int().min(1).describe("The label ID to update"),
92
92
  name: z.string().min(1).max(50).optional().describe("New label name"),
93
93
  color: z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("New hex color, e.g. #ffaa00 or ffaa00"),
94
94
  description: z.string().optional().describe("New description"),
@@ -120,7 +120,7 @@ export function registerLabelTools(
120
120
  "opsee_delete_label",
121
121
  "Delete a label permanently. Attached TaskLabel rows are not cleaned up automatically by this tool — backend behavior dictates whether cascade applies.",
122
122
  {
123
- labelId: z.number().describe("The label ID to delete"),
123
+ labelId: z.number().int().min(1).describe("The label ID to delete"),
124
124
  },
125
125
  { readOnlyHint: false, destructiveHint: true },
126
126
  async ({ labelId }) => {
@@ -138,8 +138,8 @@ export function registerLabelTools(
138
138
  "opsee_attach_label_to_task",
139
139
  "Attach an existing label to a task. Returns the TaskLabel join-row ID needed for opsee_detach_label_from_task.",
140
140
  {
141
- taskId: z.number().describe("The task ID"),
142
- labelId: z.number().describe("The label ID to attach"),
141
+ taskId: z.number().int().min(1).describe("The task ID"),
142
+ labelId: z.number().int().min(1).describe("The label ID to attach"),
143
143
  },
144
144
  { readOnlyHint: false, destructiveHint: false },
145
145
  async ({ taskId, labelId }) => {
@@ -160,7 +160,7 @@ export function registerLabelTools(
160
160
  "opsee_detach_label_from_task",
161
161
  "Pass the TaskLabel join-row id (returned from opsee_list_task_labels), not the labelId. Proto DeleteTaskLabelRequest takes the join PK.",
162
162
  {
163
- taskLabelId: z.number().describe("The TaskLabel join-row ID (from opsee_list_task_labels), not the label ID"),
163
+ taskLabelId: z.number().int().min(1).describe("The TaskLabel join-row ID (from opsee_list_task_labels), not the label ID"),
164
164
  },
165
165
  { readOnlyHint: false, destructiveHint: true },
166
166
  async ({ taskLabelId }) => {
@@ -178,9 +178,9 @@ export function registerLabelTools(
178
178
  "opsee_list_task_labels",
179
179
  "List labels attached to a specific task (returns TaskLabel join rows with embedded Label data).",
180
180
  {
181
- taskId: z.number().describe("The task ID"),
182
- page: z.number().optional().describe("Page number (default: 1)"),
183
- pageSize: z.number().optional().describe("Items per page (default: 50)"),
181
+ taskId: z.number().int().min(1).describe("The task ID"),
182
+ page: z.number().int().min(1).optional().describe("Page number (default: 1)"),
183
+ pageSize: z.number().int().min(1).optional().describe("Items per page (default: 50)"),
184
184
  },
185
185
  { readOnlyHint: true, destructiveHint: false },
186
186
  async ({ taskId, page, pageSize }) => {
@@ -20,7 +20,7 @@ export function registerMilestoneTools(
20
20
  server.tool(
21
21
  "opsee_list_milestones",
22
22
  "List milestones in an Opsee project.",
23
- { projectId: z.number().describe("The project ID") },
23
+ { projectId: z.number().int().min(1).describe("The project ID") },
24
24
  { readOnlyHint: true, destructiveHint: false },
25
25
  async ({ projectId }) => {
26
26
  try {
@@ -39,7 +39,7 @@ export function registerMilestoneTools(
39
39
  server.tool(
40
40
  "opsee_get_milestone",
41
41
  "Get details of a specific milestone by ID.",
42
- { milestoneId: z.number().describe("The milestone ID") },
42
+ { milestoneId: z.number().int().min(1).describe("The milestone ID") },
43
43
  { readOnlyHint: true, destructiveHint: false },
44
44
  async ({ milestoneId }) => {
45
45
  try {
@@ -57,7 +57,7 @@ export function registerMilestoneTools(
57
57
  "opsee_create_milestone",
58
58
  "Create a new milestone in an Opsee project.",
59
59
  {
60
- projectId: z.number().describe("The project ID"),
60
+ projectId: z.number().int().min(1).describe("The project ID"),
61
61
  name: z.string().describe("Milestone name"),
62
62
  status: z.enum(MILESTONE_STATUSES).optional().describe("Milestone status. Defaults to 'not_started'"),
63
63
  description: z.string().optional().describe("Milestone description"),
@@ -91,7 +91,7 @@ export function registerMilestoneTools(
91
91
  "opsee_update_milestone",
92
92
  "Update a milestone's fields. Fetches the current milestone first, then applies only the provided changes.",
93
93
  {
94
- milestoneId: z.number().describe("The milestone ID to update"),
94
+ milestoneId: z.number().int().min(1).describe("The milestone ID to update"),
95
95
  name: z.string().optional().describe("New name"),
96
96
  status: z.enum(MILESTONE_STATUSES).optional().describe("New status"),
97
97
  description: z.string().optional().describe("New description"),
@@ -128,7 +128,7 @@ export function registerMilestoneTools(
128
128
  server.tool(
129
129
  "opsee_list_milestone_tasks",
130
130
  "List tasks attached to a specific milestone.",
131
- { milestoneId: z.number().describe("The milestone ID") },
131
+ { milestoneId: z.number().int().min(1).describe("The milestone ID") },
132
132
  { readOnlyHint: true, destructiveHint: false },
133
133
  async ({ milestoneId }) => {
134
134
  try {
@@ -150,8 +150,8 @@ export function registerMilestoneTools(
150
150
  "opsee_attach_task_to_milestone",
151
151
  "Attach an existing task to a milestone.",
152
152
  {
153
- milestoneId: z.number().describe("The milestone ID"),
154
- taskId: z.number().describe("The task ID to attach"),
153
+ milestoneId: z.number().int().min(1).describe("The milestone ID"),
154
+ taskId: z.number().int().min(1).describe("The task ID to attach"),
155
155
  },
156
156
  { readOnlyHint: false, destructiveHint: false },
157
157
  async ({ milestoneId, taskId }) => {
@@ -173,7 +173,7 @@ export function registerMilestoneTools(
173
173
  "opsee_detach_task_from_milestone",
174
174
  "Detach a task from a milestone. Requires the MilestoneTask ID (not the task ID) — use opsee_list_milestone_tasks to find it.",
175
175
  {
176
- milestoneTaskId: z.number().describe("The MilestoneTask ID (from opsee_list_milestone_tasks)"),
176
+ milestoneTaskId: z.number().int().min(1).describe("The MilestoneTask ID (from opsee_list_milestone_tasks)"),
177
177
  },
178
178
  { readOnlyHint: false, destructiveHint: true },
179
179
  async ({ milestoneTaskId }) => {
@@ -43,8 +43,8 @@ export function registerNotificationTools(
43
43
  "Inbox-style listing of the authenticated user's notifications, newest first. Common notification_type values: task_assigned, comment_added, mention, task_blocked_unblocked, ai_workflow_status_change. Pass unreadOnly:true to see just the inbox.",
44
44
  {
45
45
  unreadOnly: z.boolean().optional().describe("If true, only return notifications where is_read=false"),
46
- page: z.number().optional().describe("Page number (default: 1)"),
47
- pageSize: z.number().optional().describe("Items per page (default: 50)"),
46
+ page: z.number().int().min(1).optional().describe("Page number (default: 1)"),
47
+ pageSize: z.number().int().min(1).optional().describe("Items per page (default: 50)"),
48
48
  },
49
49
  { readOnlyHint: true, destructiveHint: false },
50
50
  async ({ unreadOnly, page, pageSize }) => {
@@ -78,7 +78,7 @@ export function registerNotificationTools(
78
78
  "opsee_mark_notification_read",
79
79
  "Mark a notification as read. Read-modify-write: fetches the current notification and re-issues EditNotification with is_read=true so other fields aren't clobbered.",
80
80
  {
81
- notificationId: z.number().describe("The notification ID"),
81
+ notificationId: z.number().int().min(1).describe("The notification ID"),
82
82
  },
83
83
  { readOnlyHint: false, destructiveHint: false },
84
84
  async ({ notificationId }) => {
@@ -26,7 +26,7 @@ export function registerProjectTools(
26
26
  server.tool(
27
27
  "opsee_get_project",
28
28
  "Get details of a specific Opsee project by ID.",
29
- { projectId: z.number().describe("The project ID") },
29
+ { projectId: z.number().int().min(1).describe("The project ID") },
30
30
  { readOnlyHint: true, destructiveHint: false },
31
31
  async ({ projectId }) => {
32
32
  try {
@@ -10,7 +10,7 @@ export function registerRepositoryTools(
10
10
  server.tool(
11
11
  "opsee_list_repositories",
12
12
  "List connected VCS repositories (GitHub/GitLab) for an Opsee project.",
13
- { projectId: z.number().describe("The project ID") },
13
+ { projectId: z.number().int().min(1).describe("The project ID") },
14
14
  { readOnlyHint: true, destructiveHint: false },
15
15
  async ({ projectId }) => {
16
16
  try {
@@ -26,8 +26,8 @@ export function registerTaskDependencyTools(
26
26
  "opsee_create_task_dependency",
27
27
  "Create a dependency relationship between two tasks. Use BLOCKS when the from-task must be completed before the to-task. Use BLOCKED_BY for the inverse. Use DUPLICATES when tasks are duplicates. Use RELATES_TO for a general association.",
28
28
  {
29
- fromTaskId: z.number().describe("The ID of the source task"),
30
- toTaskId: z.number().describe("The ID of the target task"),
29
+ fromTaskId: z.number().int().min(1).describe("The ID of the source task"),
30
+ toTaskId: z.number().int().min(1).describe("The ID of the target task"),
31
31
  type: dependencyTypeSchema.describe("Dependency type: BLOCKS, BLOCKED_BY, DUPLICATES, or RELATES_TO"),
32
32
  },
33
33
  { readOnlyHint: false, destructiveHint: false },
@@ -57,9 +57,9 @@ export function registerTaskDependencyTools(
57
57
  "opsee_list_task_dependencies",
58
58
  "List all dependencies for a task — returns both outgoing (task blocks others) and incoming (task is blocked by others) relationships.",
59
59
  {
60
- taskId: z.number().describe("The task ID"),
61
- page: z.number().optional().describe("Page number (default: 1)"),
62
- pageSize: z.number().optional().describe("Items per page (default: 50)"),
60
+ taskId: z.number().int().min(1).describe("The task ID"),
61
+ page: z.number().int().min(1).optional().describe("Page number (default: 1)"),
62
+ pageSize: z.number().int().min(1).optional().describe("Items per page (default: 50)"),
63
63
  },
64
64
  { readOnlyHint: true, destructiveHint: false },
65
65
  async ({ taskId, page, pageSize }) => {
@@ -81,7 +81,7 @@ export function registerTaskDependencyTools(
81
81
  server.tool(
82
82
  "opsee_delete_task_dependency",
83
83
  "Delete a task dependency by its ID. This is permanent.",
84
- { dependencyId: z.number().describe("The task dependency ID") },
84
+ { dependencyId: z.number().int().min(1).describe("The task dependency ID") },
85
85
  { readOnlyHint: false, destructiveHint: true },
86
86
  async ({ dependencyId }) => {
87
87
  try {
@@ -98,7 +98,7 @@ export function registerTaskDependencyTools(
98
98
  "opsee_get_dependency_chain",
99
99
  "Server-side BFS over the BLOCKS/BLOCKED_BY graph from a task. Returns the reachable tasks and the edges that connect them. Use direction='blocks' for downstream (what does this task unblock), 'blocked_by' for upstream (what is this task waiting on), 'both' for the full neighborhood. Depth caps at 10.",
100
100
  {
101
- taskId: z.number().describe("The starting task ID"),
101
+ taskId: z.number().int().min(1).describe("The starting task ID"),
102
102
  direction: z.enum(["blocks", "blocked_by", "both"]).optional().describe("Direction to walk (default: both)"),
103
103
  depth: z.number().int().min(1).max(10).optional().describe("Max hops from the source (default: 5, capped at 10)"),
104
104
  },
@@ -10,7 +10,7 @@ export function registerTaskMetadataTools(
10
10
  server.tool(
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
- { projectId: z.number().describe("The project ID") },
13
+ { projectId: z.number().int().min(1).describe("The project ID") },
14
14
  { readOnlyHint: true, destructiveHint: false },
15
15
  async ({ projectId }) => {
16
16
  try {
@@ -32,7 +32,7 @@ export function registerTaskMetadataTools(
32
32
  server.tool(
33
33
  "opsee_list_task_priorities",
34
34
  "Get priority levels (Critical, High, Medium, Low, etc.) for an Opsee project. Use these IDs when creating or updating tasks.",
35
- { projectId: z.number().describe("The project ID") },
35
+ { projectId: z.number().int().min(1).describe("The project ID") },
36
36
  { readOnlyHint: true, destructiveHint: false },
37
37
  async ({ projectId }) => {
38
38
  try {
@@ -54,7 +54,7 @@ export function registerTaskMetadataTools(
54
54
  server.tool(
55
55
  "opsee_list_boards",
56
56
  "List Kanban boards for an Opsee project. Use the board ID to list board columns.",
57
- { projectId: z.number().describe("The project ID") },
57
+ { projectId: z.number().int().min(1).describe("The project ID") },
58
58
  { readOnlyHint: true, destructiveHint: false },
59
59
  async ({ projectId }) => {
60
60
  try {
@@ -76,7 +76,7 @@ export function registerTaskMetadataTools(
76
76
  server.tool(
77
77
  "opsee_list_board_columns",
78
78
  "Get board columns/statuses (To Do, In Progress, Done, etc.) for a board. Use these IDs when creating or updating tasks.",
79
- { boardId: z.number().describe("The board ID") },
79
+ { boardId: z.number().int().min(1).describe("The board ID") },
80
80
  { readOnlyHint: true, destructiveHint: false },
81
81
  async ({ boardId }) => {
82
82
  try {