systa-mcp 1.0.9 → 1.1.1

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.
Files changed (3) hide show
  1. package/SKILL.md +60 -39
  2. package/package.json +4 -2
  3. package/server.js +771 -55
package/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 1.0.9
2
+ version: 1.1.1
3
3
  name: systa
4
4
  description: |
5
5
  Drive SysTa (Talep Takip Sistemi / SysTa) — a
@@ -94,36 +94,45 @@ yanıtındaki `glossary` alias'larının ayna kopyasıdır (MCP yokken de geçer
94
94
  ASCII'dir — eşleştirmede aksanı yok say. Modülü bulduktan sonra `describe_module(modül)`
95
95
  veya `describe_endpoint` ile gövdeyi netleştir.
96
96
 
97
- | Doğal dil (alias) | Modül | Birincil giriş |
98
- | --------------------------------------------------------- | --------------- | -------------------------------------------------------- |
99
- | talep, talepler, is, destek talebi | request | list_requests / create_request |
100
- | gorev, task, kart, card | kanban | manage_request_card / move_request_card |
101
- | pano, board, kanban / kolon, column, asama, liste | kanban | get_request_board |
102
- | efor, durum, oncelik, atanan, planla, denetim, gecmis | request | get_request / list_requests |
103
- | proje, klonla | project | list_projects / create_project |
104
- | onaya sunma, snapshot, sprint, sprinti kapat, optimize et | plan | submit_plan_for_approval / create_plan_baseline |
105
- | onay, onayla, reddet | approvals | list_pending_approvals / respond_to_approval |
106
- | sla, sure taahhudu | sla | (sla endpoints) |
107
- | release, surum, go/no-go, deployment | release | create_release_list / add_release_item |
108
- | musaitlik, izin (leave), vekil | user/automation | systa_api_call /user-availability, /user-leaves |
109
- | vendor, sirket, musteri | company | systa_api_call GET /companies |
110
- | organizasyon birimi, departman, bolum, birim | department | GET /api/departments |
111
- | takim, ekip, team | team | GET /api/teams |
112
- | yetki paketi, izin (permission) | security | (bundle/permission endpoints) |
113
- | alt form, sub-form | forms | systa_api_call GET /forms/:id |
114
- | rapor, raporlama, disa aktar, export, excel, csv | report | systa_api_call /reports/preview,/reports/export |
115
- | ozel sorgu, sorgu, query builder, ad-hoc, veri cek | query-builder | querybuilder_describe_fields / \_execute_query |
116
- | hesap tablosu, gridpad, tablo, excel, spreadsheet, hucre | spreadsheet | GET /api/spreadsheets |
117
- | dosya, ek, attachment, yukle, upload, indir, download | file | upload_file_to / download_request_file |
118
- | bildirim, notification, uyari, okundu, push | notification | GET /api/notifications |
119
- | yetenek, beceri, skill, otomatik etiketleme | skill | GET /api/skills |
120
- | duyuru, announcement, bildiri, ilan | announcement | GET /api/announcements |
121
- | urun guncellemesi, surum notu, yenilikler, changelog | product-update | GET /api/product-updates |
122
- | not, kisisel not, hatirlatma | note | GET /api/dashboard/notes |
123
- | yer imi, favori, bookmark, kisayol | bookmark | GET /api/dashboard/bookmarks |
124
- | degerlendirme, puanlama, anket, memnuniyet, geri bildirim | ratings | POST /api/ratings |
125
- | github, repo, pull request, commit, issue aktar | github | systa_api_call /github/me/{connection,repos,import,push} |
126
- | gosterge paneli, dashboard, widget, ozet, grafik, panel | dashboard | GET /api/dashboard/init |
97
+ | Doğal dil (alias) | Modül | Birincil giriş |
98
+ | ------------------------------------------------------------ | --------------- | ----------------------------------------------------------------- |
99
+ | talep, talepler, is, destek talebi | request | list_requests / create_request |
100
+ | geciken, overdue, gecikmis, sla ihlali, deadline gecmis | request/sla | list_requests(overdue:true / slaBreached:true) |
101
+ | toplu guncelle, hepsini, birden cok talep | request | bulk_update_requests |
102
+ | kullanici bul, isim cozumle, kime, hangi ahmet | user | resolve_user_by_name / list_users |
103
+ | sirket bul, kategori bul, ekip bul, id cozumle, lookup | multiple | list_lookup(kind:'company'/'category'/'team'/...) |
104
+ | projeyi klonla, projeyi cogalt, duplicate project | project | duplicate_project |
105
+ | snapshot fark, baseline karsilastir, plan diff | plan | compare_plan_baseline |
106
+ | yorumlar, ekler, degisiklik gecmisi, denetim | request | list_request_comments / list_request_files / list_request_history |
107
+ | gorev, task, kart, card | kanban | manage_request_card / move_request_card |
108
+ | pano, board, kanban / kolon, column, asama, liste | kanban | get_request_board / create_request_board |
109
+ | durum gecisi onizle, izinli gecisler, hangi duruma gecebilir | request | list_allowed_transitions |
110
+ | plan modu, sprint kapsami, plan mode setup | plan | create_plan_mode |
111
+ | efor, durum, oncelik, atanan, planla, denetim, gecmis | request | get_request / list_requests |
112
+ | proje, klonla | project | list_projects / create_project |
113
+ | onaya sunma, snapshot, sprint, sprinti kapat, optimize et | plan | submit_plan_for_approval / create_plan_baseline |
114
+ | onay, onayla, reddet | approvals | list_pending_approvals / respond_to_approval |
115
+ | sla, sure taahhudu | sla | (sla endpoints) |
116
+ | release, surum, go/no-go, deployment | release | create_release_list / add_release_item |
117
+ | musaitlik, izin (leave), vekil | user/automation | systa_api_call /user-availability, /user-leaves |
118
+ | vendor, sirket, musteri | company | systa_api_call GET /companies |
119
+ | organizasyon birimi, departman, bolum, birim | department | GET /api/departments |
120
+ | takim, ekip, team | team | GET /api/teams |
121
+ | yetki paketi, izin (permission) | security | (bundle/permission endpoints) |
122
+ | alt form, sub-form | forms | systa_api_call GET /forms/:id |
123
+ | rapor, raporlama, disa aktar, export, excel, csv | report | systa_api_call /reports/preview,/reports/export |
124
+ | ozel sorgu, sorgu, query builder, ad-hoc, veri cek | query-builder | querybuilder_describe_fields / \_execute_query |
125
+ | hesap tablosu, gridpad, tablo, excel, spreadsheet, hucre | spreadsheet | GET /api/spreadsheets |
126
+ | dosya, ek, attachment, yukle, upload, indir, download | file | upload_file_to / download_request_file |
127
+ | bildirim, notification, uyari, okundu, push | notification | GET /api/notifications |
128
+ | yetenek, beceri, skill, otomatik etiketleme | skill | GET /api/skills |
129
+ | duyuru, announcement, bildiri, ilan | announcement | GET /api/announcements |
130
+ | urun guncellemesi, surum notu, yenilikler, changelog | product-update | GET /api/product-updates |
131
+ | not, kisisel not, hatirlatma | note | GET /api/dashboard/notes |
132
+ | yer imi, favori, bookmark, kisayol | bookmark | GET /api/dashboard/bookmarks |
133
+ | degerlendirme, puanlama, anket, memnuniyet, geri bildirim | ratings | POST /api/ratings |
134
+ | github, repo, pull request, commit, issue aktar | github | systa_api_call /github/me/{connection,repos,import,push} |
135
+ | gosterge paneli, dashboard, widget, ozet, grafik, panel | dashboard | GET /api/dashboard/init |
127
136
 
128
137
  > Bu tablo, katalogdaki ~25 modülün kullanıcı-yüzeyli olanlarından bir alt küme kapsar. Listede olmayan
129
138
  > bir kelime için `list_capabilities` çağır — anahtarının erişebildiği güncel modül/alias
@@ -154,20 +163,26 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
154
163
  ### Talep aç (open a request) — `create_request` → `POST /api/requests`
155
164
 
156
165
  1. Need `title` (min 5 chars) — if missing or too short, ask once. Never invent it.
157
- 2. Defaults: `channel='API'` (AI/MCP source), `companyId` ← who-am-i, `statusId` ← default open/new status (resolve via `GET /api/statuses`, pick the non-final entry status), `priority='normal'` unless the user signals "acil/critical". `description` can be a plain string.
158
- 3. Submit; report "Talep #<num> açıldı — '<title>'". If the user also said "bana ata", chain assignment below.
166
+ 2. Defaults: `channel='API'` (AI/MCP source), `statusId` ← default open/new status (resolve via `GET /api/statuses`, pick the non-final entry status), `priority='normal'` unless the user signals "acil/critical". `description` can be a plain string. **`companyId` — DIKKAT:** genelde `user.companyId` DEĞİL — B2B talepler MÜŞTERİ şirketi için açılır. Şirket adı netlesmediyse `systa_api_call GET /companies?search=<ad>` ile çöz veya kullanıcıya SOR. `prepare_create_request(companyId)` proaktif seçenekleri döner.
167
+ 3. **Efor** (`estimatedEffort`/`personDay`): KANONIK GÜN. Kullanıcı "N saat" derse ÖNCE `list_capabilities.effortPolicy.hoursPerDay` ile böl (varsayılan 8), sonra yaz. "8 saat" → `estimatedEffort=1` (8/8=1 gün), "4 saat" → `estimatedEffort=0.5`. Aynı kural `manage_request_card.estimatedEffort` ve `bulk_update_requests.updates.estimatedEffort` için de geçerli.
168
+ 4. Submit; report "Talep #<num> açıldı — '<title>'". If the user also said "bana ata", chain assignment below.
159
169
 
160
170
  ### Üzerimdeki işler / talepleri listele (what's on my plate) — `list_requests` → `GET /api/requests`
161
171
 
162
- - "taleplerim / on me" → `assignedTo=<my userId>`. "açık/open" → final olmayan durumlar. Default `limit=20-50`, sort `created_at desc`, `companyId` from scope. Summarize as a short list (numara + başlık + durum), not raw rows.
172
+ - "taleplerim / on me" → `assignedTo=<my userId>`. "açık/open" → `openOnly:true`. "geciken/overdue" → `overdue:true` (deadline geçmiş + non-final auto-composed) veya `slaBreached:true` (SLA ihlali). "SLA durumu" → `slaStatus:['breached','active']`. **"dosyalı talepler / eki olan" → `hasAttachments:true`** (top-level talep eki). Deadline'a göre → `deadlineFrom`/`deadlineTo` (YYYY-MM-DD) ya da `deadlineWithinDays:N` (önümüzdeki N gün). Search kapsamını genişlet: `searchIn:['assignee','creator','comments',...]`. **Response boyutu:** `fields` varsayılan `'summary'` (satır başına ~1KB); `'detail'` (description/effort/counts eklenir) veya `'full'` (customFields/stakeHolders dahil, çok büyük). Default `limit=20-50`, sort `created_at desc`. Yanıt zarfı: `{items, total, limit, offset}` — sayfa sayısı `= ceil(total/limit)`. Summarize as a short list (numara + başlık + durum), not raw rows.
163
173
 
164
174
  ### Talep detayı (show a request) — `get_request` → `GET /api/requests/:requestNumber`
165
175
 
166
176
  - Resolve the user's number/title to `requestNumber`. If ambiguous, `list_requests` first to find candidates, then fetch.
177
+ - **Alt kaynaklar (dedicated tools):** yorumlar → `list_request_comments(requestNumber)` (her yorumun `fileIds[]`'ı vardır — yorum ekleri buradan bulunur); ekler → `list_request_files(requestNumber)` (yalnız talep-birinci-seviye ekler; yorum/kart ekleri için `download_request_file` description'ındaki 7 kaynaklık listeye bak); değişiklik geçmişi → `list_request_history(requestNumber)` (kim ne değiştirdi, actor/timestamp/field/oldValue/newValue).
167
178
 
168
179
  ### Bana / birine ata (assign) — `assign_request` → `PUT /api/requests/:requestNumber`
169
180
 
170
- - "bana/üzerime" → `assignedTo=<my userId>`. A name → resolve via user lookup (`GET /api/users?search=`) to `userId`. Partial update — only send `assignedTo`.
181
+ - "bana/üzerime" → `assignedTo=<my userId>` (list_capabilities.user.id). A name → **`resolve_user_by_name(name)`** — döner {matched, ambiguity, userId | candidates[]}. Ambiguity=true (birden çok eşleşme) durumunda kullanıcıya SOR, rastgele seçme. `list_users(search, companyId, ...)` browsing/multi-hit için; tek-isim çözümü için resolve_user_by_name tercih edilir. Partial update — only send `assignedTo`.
182
+
183
+ ### Toplu güncelleme (bulk update) — `bulk_update_requests` → `PUT /api/requests/bulk`
184
+
185
+ - Birden fazla talebe AYNI değişikliği uygula ("bunları kritik yap", "5 talebi tamamla") — `requestIds:[...] + updates:{...}` şeklinde tek çağrı. `update_request`'i loop içinde çağırma. `updates` kabul eder: statusId, assignedTo, priority, deadline, categoryId, departmentId, projectId, estimatedEffort (**GÜN**), personDay (**GÜN**), plannedStartDate, plannedEndDate. Farklı update grupları için ayrı bulk çağrıları yap. Undo isteği: `systa_api_call POST /api/requests/bulk/undo` yanıtta dönen `batchId` ile.
171
186
 
172
187
  ### Yorum ekle (comment) — `add_request_comment` → `POST /api/requests/:requestNumber/comments`
173
188
 
@@ -175,7 +190,7 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
175
190
 
176
191
  ### Durum değiştir / tamamla / kapat (change status) — `change_request_status` → `PUT /api/requests/:requestNumber`
177
192
 
178
- - "tamamla/kapat" → pick the appropriate `is_final` status (resolve via `GET /api/statuses`). A named status → resolve to `statusId`. If the user wrote a closing note, add it as a comment; if not, don't.
193
+ - **ÖNCE** `list_allowed_transitions(requestNumber)` çağır — transition kuralları hedef statüleri sınırlar (kategori/şirket/departman/öncelik bazlı). Dönen listede kullanıcının doğal-dil hedefini (`test asamasi`, `tamamla`, `yeniden ac`) `statusKey` / `statusId` ile eşleştir. `change_request_status` `statusId` VEYA `statusKey` alır (statusKey verirsen tool içeride list_allowed_transitions ile çözer, aksi halde global `/statuses` fallback). "tamamla/kapat" → `is_final=true` bir statü. Kullanıcı kapanış notu yazdıysa `comment` argümanı olarak geç.
179
194
 
180
195
  ### Proje aç (start a project) — `create_project` → `POST /api/projects`
181
196
 
@@ -183,16 +198,22 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
183
198
 
184
199
  ### Projemi / projeleri gör + durum (list / health) — `list_projects` / `get_project_health`
185
200
 
186
- - List: `GET /api/projects` (default `page=1, limit=20`, recent first). "durum/sağlık" → combine `health-score` + `risk-score` + `sla-status` into one summary.
201
+ - List: `list_projects` (default `limit=20`, recent first). Filtreler: `search`, `companyId`, `parentProjectId` (subtree — immediate children), `status`. "durum/sağlık" → combine `health-score` + `risk-score` + `sla-status` into one summary.
202
+ - **Klonla / cogalt:** `duplicate_project(id, newProjectName?)` — kaynak projenin tanımını (isim/company/dep/org/dates/weight/planned effort/governance) yeni bir projeye kopyalar. KOPYALANMAZ: alt proje, talep, board, milestone, snapshot, gerçekleşen efor/ilerleme (bunlar yeni projede sıfırdan başlar).
187
203
 
188
204
  ### Kanban kart ekle (add a task card) — `manage_request_card` (create)
189
205
 
190
- - `boardId` is always tied to a `requestId` — first resolve the board (`get_request_board`, or `GET /api/requests/:requestId/kanban/hierarchy`). Defaults: first/entry column if `columnId` omitted, `priority='normal'`, unassigned unless "bana ata", `cardTitle` min 3 chars (ask to clarify if shorter). `manage_request_card` handles create / update / complete (the action is in the body) — call `describe_endpoint` if unsure of the fields.
206
+ - `boardId` is always tied to a `requestId` — first resolve the board (`get_request_board`, or `GET /api/requests/:requestId/kanban/hierarchy`). **Board yoksa** (`get_request_board` veya `manage_request_card` yanıtı 404 `KANBAN_BOARD_NOT_FOUND` / kod 2031) — ÖNCE `create_request_board(requestId)` çağır (opsiyonel `templateId` ile şablon; şablonları `systa_api_call GET /task-boards/templates` listeler), sonra kartı ekle. Defaults: first/entry column if `columnId` omitted, `priority='normal'`, unassigned unless "bana ata", `cardTitle` min 3 chars (ask to clarify if shorter). `manage_request_card` handles create / update / complete (the action is in the body) — call `describe_endpoint` if unsure of the fields.
191
207
 
192
208
  ### Kartı taşı / tamamla (move / complete card) — `move_request_card` / `manage_request_card`
193
209
 
194
210
  - Move: `move_request_card` — resolve the target column name → `columnId` from the board detail; append to end if no position given. Complete: `manage_request_card` with the complete action — server sets `isCompleted=true`, 100%.
195
211
 
212
+ ### Sprint / plan modu + snapshot (plan lifecycle) — `create_plan_mode` → `create_plan_baseline` → `compare_plan_baseline`
213
+
214
+ - **Sırayla:** (1) `create_plan_mode(name, boards?)` — sprint kapsamını aç. `boards` verirsen filterCriteria ile talepleri filtreleyen boardlar oluşur; **omit edersen tool otomatik tek varsayılan board ekler** (boş baseline'a düşmezsin). (2) `create_plan_baseline(modeId, baselineName)` — mode'un canlı durumunu snapshot alır ve `baselineId` döner. (3) `submit_plan_for_approval(baselineId, approverUserIds)` — onaya sunar. "sprinti kapat" ise `systa_api_call PATCH /api/plan/baselines/:baselineId/lifecycle` `{lifecycleStatus:'completed'}`. "plan optimize et / dagit" ise `distribute_plan_schedule`.
215
+ - **Snapshot farkı / baseline karşılaştırması:** "son snapshot ile bugünkü durum arasında ne değişmiş" → `compare_plan_baseline(baselineId)`. Ham snapshot'ı çekmeye çalışırsan `get_plan_baseline(baselineId)` **varsayılan olarak `fields:'summary'`** kullanır ve `snapshotData`'yı DAHİL ETMEZ — metadata + counts (totalCards, totalEffort, approvalStatus, hasSnapshot) döner. Gerçekten ham snapshot lazımsa `fields:'full'` (nadir, ~800KB).
216
+
196
217
  ### Onaya sun / onayla / bekleyenler (approvals)
197
218
 
198
219
  - Submit (request approval): `request_approval` → `POST /api/approvals/batch`, `strategy='any_one'` default, approver names → ids. Plan onayı ayrı: `submit_plan_for_approval` → `POST /api/plan/approvals/submit` (`strategy='all_required'`).
@@ -232,7 +253,7 @@ SysTa returns structured errors. Recover deterministically instead of giving up.
232
253
  - Retry once after a fix. If it fails again with the same field, surface it to the user rather than looping.
233
254
  2. **`UNEXPECTED_FILE_FIELD` / `UNKNOWN_PARAM`** — you sent a flag/field the endpoint doesn't accept. Re-check with `describe_endpoint` and drop it. (Web/UI-only fields aren't valid on the API surface.)
234
255
  3. **`404` on a `:requestNumber` / `:id`** — the handle didn't resolve. Run the matching list/search (`list_requests`, `list_projects`, or `systa_api_call` `GET /users?search=`, `GET /tags/search`) to map name/number → id, then retry. Never hard-code an id.
235
- 4. **ID resolution chain (general).** Most mutations need an id the user gave you as a name. There are NO dedicated lookup tools — use `systa_api_call`: company → `GET /companies`, team → `GET /teams` (name→`teamCode`), user → `GET /users?search=` (name→`userId`, "ben/bana"→self via who-am-i), status → `GET /statuses` (name→`statusId`), tag → `GET /tags/search`, board → request's kanban hierarchy (`get_request_board`; requestId→`boardId`→`columnId`). Resolve first, act second.
256
+ 4. **ID resolution chain (general).** Most mutations need an id the user gave you as a name. Use dedicated tools when available (each returns rows with `{id, name/key}`); fall back to `systa_api_call` for kinds not covered. **company / team / department / organization / category / status → `list_lookup(kind:'<kind>', search:'<name>')`** — one tool covers six reference entities. **user → `resolve_user_by_name(name)`** — returns `{userId}` on unique match or `{ambiguity:true, candidates}` on multi-match (ASK user, do NOT auto-pick); `list_users(search, ...)` for browsing. "ben/bana" → `list_capabilities.user.id`. tag → `systa_api_call GET /tags/search`. board → request's kanban hierarchy (`get_request_board`; requestId→boardId→columnId). Resolve first, act second.
236
257
  5. **`401/403`** — expired or out-of-scope key. Don't retry blindly; tell the user the auth/permission problem (in their language) and stop. For `403 permission`, name the capability the key lacks; don't attempt a privilege workaround.
237
258
  6. **`409 conflict`** (optimistic concurrency on cards/edits) — re-read the entity to get the latest `updatedAt`, then retry the mutation once with the fresh value; warn the user only if it conflicts again.
238
259
  7. **Dual-path "list vs create" endpoints** (e.g. `POST /api/sla/definitions`, `POST /api/sla/standby-configs`) — an empty/filter-only body lists; a full body creates. Choose the body shape that matches the intent; don't accidentally create when the user asked to list.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systa-mcp",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "MCP stdio server for SysTa (Talep Takip Sistemi) — lets AI agents (Claude, Codex) use the SysTa REST API via a scoped API key. Zero npm dependencies (native Node).",
5
5
  "type": "commonjs",
6
6
  "main": "server.js",
@@ -13,7 +13,9 @@
13
13
  "README.md"
14
14
  ],
15
15
  "scripts": {
16
- "start": "node server.js"
16
+ "start": "node server.js",
17
+ "check:version-sync": "node scripts/check-version-sync.js",
18
+ "prepublishOnly": "node scripts/check-version-sync.js"
17
19
  },
18
20
  "engines": {
19
21
  "node": ">=18"
package/server.js CHANGED
@@ -358,6 +358,10 @@ const shapeCompact = (caps) => ({
358
358
  ...(caps.user ? { user: caps.user } : {}),
359
359
  scopes: caps.scopes,
360
360
  isSuperAdmin: caps.isSuperAdmin,
361
+ // Efor birim politikasi (MCP AIX v3 F1 H1): kanonik depolama GUN, vendor
362
+ // display unit + hoursPerDay ile saat<->gun cevrimi. Kor AI 'N saat' istegi
363
+ // aldiginda buradaki hoursPerDay'e boler, sonra estimatedEffort'a YAZAR.
364
+ ...(caps.effortPolicy ? { effortPolicy: caps.effortPolicy } : {}),
361
365
  // Kavramsal oryantasyon (ai_orientation_catalog) — kod-kor ajan icin SysTa'yi
362
366
  // anlama katmani: platform ozeti + TR glossary + modul aciklamalari.
363
367
  ...(caps.overview ? { overview: caps.overview } : {}),
@@ -392,6 +396,8 @@ const shapeSummary = (caps) => ({
392
396
  ...(caps.user ? { user: caps.user } : {}),
393
397
  scopes: caps.scopes,
394
398
  isSuperAdmin: caps.isSuperAdmin,
399
+ // Efor birim politikasi (MCP AIX v3 F1 H1) — bkz. shapeCompact yorumu.
400
+ ...(caps.effortPolicy ? { effortPolicy: caps.effortPolicy } : {}),
395
401
  ...(caps.overview ? { overview: caps.overview } : {}),
396
402
  ...(caps.glossary ? { glossary: caps.glossary } : {}),
397
403
  moduleCount: caps.moduleCount,
@@ -420,12 +426,72 @@ const shapeOneModule = (caps, target) => {
420
426
  };
421
427
  };
422
428
 
429
+ // list_requests: 'openOnly' + 'overdue' both need the NON-final status ids -- GET
430
+ // /requests accepts statusId as an array, but has no excludeStatusIds. Fetch
431
+ // /statuses once and INCLUDE the non-final ones.
432
+ const resolveListRequestsStatusFilter = async (args) => {
433
+ const wantOnlyOpen = args.openOnly || args.overdue === true;
434
+ if (!wantOnlyOpen) {
435
+ return args.statusId;
436
+ }
437
+ const st = await systaFetch('GET', '/statuses').catch(() => null);
438
+ const rows = st && st.ok ? (st.data && (st.data.data || st.data)) || [] : [];
439
+ const openIds = (Array.isArray(rows) ? rows : []).filter((s) => s && !s.isFinal).map((s) => s.id);
440
+ return openIds.length > 0 ? openIds : args.statusId;
441
+ };
442
+
443
+ // list_requests: assemble the querystring the backend querySchema.js normalizes.
444
+ // Bracket-form (deadlineDaysRemaining[operator]) and comma-separated arrays are
445
+ // canonical for that validator; url.searchParams encodes brackets literally.
446
+ const buildListRequestsQuery = (args, statusId) => {
447
+ const query = {
448
+ limit: args.limit ?? 20,
449
+ offset: args.offset,
450
+ search: args.search,
451
+ statusId,
452
+ assignedTo: args.assignedTo,
453
+ companyId: args.companyId,
454
+ categoryId: args.categoryId,
455
+ departmentId: args.departmentId,
456
+ priority: args.priority,
457
+ // Blind-AI-friendly default: return the lightweight 'summary' shape
458
+ // (~1KB/row) unless the caller explicitly asks for detail/full.
459
+ fields: args.fields ?? 'summary',
460
+ };
461
+ if (Array.isArray(args.searchIn) && args.searchIn.length > 0) {
462
+ query.searchIn = args.searchIn.join(',');
463
+ }
464
+ if (Array.isArray(args.slaStatus) && args.slaStatus.length > 0) {
465
+ query.slaStatus = args.slaStatus.join(',');
466
+ }
467
+ if (args.slaBreached === true) {
468
+ query.slaBreached = true;
469
+ }
470
+ if (typeof args.hasAttachments === 'boolean') {
471
+ query.hasAttachments = args.hasAttachments;
472
+ }
473
+ if (args.deadlineFrom) {
474
+ query.deadlineFrom = args.deadlineFrom;
475
+ }
476
+ if (args.deadlineTo) {
477
+ query.deadlineTo = args.deadlineTo;
478
+ }
479
+ if (args.overdue === true) {
480
+ query['deadlineDaysRemaining[operator]'] = 'lt';
481
+ query['deadlineDaysRemaining[value]'] = 0;
482
+ } else if (typeof args.deadlineWithinDays === 'number') {
483
+ query['deadlineDaysRemaining[operator]'] = 'lte';
484
+ query['deadlineDaysRemaining[value]'] = args.deadlineWithinDays;
485
+ }
486
+ return query;
487
+ };
488
+
423
489
  // ── Tool definitions ─────────────────────────────────────────────────────────
424
490
  const TOOLS = [
425
491
  {
426
492
  name: 'list_requests',
427
493
  description:
428
- 'List/search SysTa requests (talepler) with pagination and rich filters: status, assignee, company, category, department, priority, free-text. "uzerimdeki acik isler / my open work" => assignedTo=<me> (resolve via list_capabilities user.id) + openOnly:true (one call — excludes final/"Tamamlandi" statuses automatically). Multiple statuses: statusId can be an array [1,3,5]. Returns only requests the API key scope permits. For filters beyond these, use systa_api_call GET /requests (this endpoint rejects unknown params with a 400 naming the valid set).',
494
+ "List/search SysTa requests (talepler) with pagination and rich filters: status, assignee, company, category, department, priority, free-text, OVERDUE / SLA / DEADLINE. Common intents: 'uzerimdeki acik isler / my open work' => assignedTo=<me> + openOnly:true (excludes final statuses). 'geciken isler / overdue' => overdue:true (deadline gecmis + non-final) OR slaBreached:true (SLA ihlali). 'bu ay kapatilan' => finalOnly:true + completedFrom/completedTo (NOT YET SUPPORTED — use systa_api_call). search covers title/requestNumber/description/externalRef by default (NOT assignee/creator); use searchIn to expand. Multiple statuses: statusId can be an array [1,3,5]. Returns only requests the API key scope permits. Response envelope: {items:[], total, limit, offset} — totalPages = ceil(total/limit). RESPONSE SIZE: fields controls how much each row carries -- default 'summary' returns lightweight rows (~1KB each: id, requestNumber, title, statusName, priority, assignedTo, deadline, isOverdue, slaStatus, commentCount, fileCount). Use fields:'detail' when you need description/effort/counts, or fields:'full' for everything incl. customFields/stakeHolders (LARGE — S-A2 kanit: 44 talep tam moduyla ~300KB).",
429
495
  inputSchema: {
430
496
  type: 'object',
431
497
  additionalProperties: false,
@@ -437,7 +503,29 @@ const TOOLS = [
437
503
  description: 'Page size (1-100, default 20)',
438
504
  },
439
505
  offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
440
- search: { type: 'string', description: 'Free-text search' },
506
+ search: {
507
+ type: 'string',
508
+ description:
509
+ 'Free-text; by default matches title, requestNumber, description, externalRef. For ATANAN (assignee) name lookup use resolve_user_by_name first, then filter by assignedTo. Broaden matching via searchIn.',
510
+ },
511
+ searchIn: {
512
+ type: 'array',
513
+ items: {
514
+ type: 'string',
515
+ enum: [
516
+ 'title',
517
+ 'description',
518
+ 'requestnumber',
519
+ 'externalref',
520
+ 'category',
521
+ 'creator',
522
+ 'assignee',
523
+ 'comments',
524
+ ],
525
+ },
526
+ description:
527
+ 'Expand which fields the search string is matched against (default: title+description+requestnumber+externalref).',
528
+ },
441
529
  statusId: {
442
530
  type: ['integer', 'array'],
443
531
  items: { type: 'integer' },
@@ -455,37 +543,212 @@ const TOOLS = [
455
543
  companyId: { type: 'integer', description: 'Filter by company id' },
456
544
  categoryId: { type: 'integer', description: 'Filter by category id' },
457
545
  departmentId: { type: 'integer', description: 'Filter by department id' },
458
- priority: { type: 'string', description: 'Filter by priority key/level' },
546
+ priority: {
547
+ type: 'string',
548
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
549
+ description: 'Filter by priority key/level',
550
+ },
551
+ overdue: {
552
+ type: 'boolean',
553
+ description:
554
+ 'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly.',
555
+ },
556
+ slaStatus: {
557
+ type: 'array',
558
+ items: { type: 'string', enum: ['active', 'paused', 'completed', 'breached'] },
559
+ description: 'SLA status filter (multi-value). Backend accepts comma-separated string.',
560
+ },
561
+ slaBreached: {
562
+ type: 'boolean',
563
+ description: 'true => only requests whose SLA has been breached.',
564
+ },
565
+ hasAttachments: {
566
+ type: 'boolean',
567
+ description:
568
+ 'true => only requests with at least one active (non-deleted) top-level file attachment (files_table.request_id). Note: comment attachments and kanban card attachments live under separate tables -- this filter targets request-level uploads. Combine with fileCount>0 on the response for the same effect. Response row already carries fileCount so client-side filtering is possible on smaller pages.',
569
+ },
570
+ deadlineFrom: {
571
+ type: 'string',
572
+ description: 'Deadline >= this date (YYYY-MM-DD).',
573
+ },
574
+ deadlineTo: {
575
+ type: 'string',
576
+ description: 'Deadline <= this date (YYYY-MM-DD).',
577
+ },
578
+ deadlineWithinDays: {
579
+ type: 'integer',
580
+ description:
581
+ 'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N).',
582
+ },
583
+ fields: {
584
+ type: 'string',
585
+ enum: ['summary', 'detail', 'full'],
586
+ description:
587
+ "Response projection profile: 'summary' (default, lightweight ~1KB/row: id/requestNumber/title/statusName/priority/assignee/deadline/isOverdue/slaStatus/commentCount/fileCount), 'detail' (adds description/effort/all counts), 'full' (everything incl. customFields/stakeHolders — LARGE).",
588
+ },
459
589
  },
460
590
  },
461
591
  handler: async (args) => {
462
- let statusId = args.statusId;
463
- // openOnly: resolve the NON-final status ids and filter to them. GET /requests
464
- // accepts statusId as an array (validator normalizes a comma list); it does NOT
465
- // support excludeStatusIds, so we INCLUDE the open ones rather than exclude final.
466
- if (args.openOnly) {
467
- const st = await systaFetch('GET', '/statuses').catch(() => null);
468
- const rows = st && st.ok ? (st.data && (st.data.data || st.data)) || [] : [];
469
- const openIds = (Array.isArray(rows) ? rows : [])
470
- .filter((s) => s && !s.isFinal)
471
- .map((s) => s.id);
472
- if (openIds.length > 0) {
473
- statusId = openIds;
474
- }
475
- }
476
- return systaFetch('GET', '/requests', {
592
+ const statusId = await resolveListRequestsStatusFilter(args);
593
+ const query = buildListRequestsQuery(args, statusId);
594
+ return systaFetch('GET', '/requests', { query });
595
+ },
596
+ },
597
+ {
598
+ name: 'list_users',
599
+ description:
600
+ 'List/search SysTa users (kullanicilar) with pagination and filters (search, companyId, departmentId). Wraps GET /api/users?search=... Prefer resolve_user_by_name when you have a single name and want the id -- this tool is for browsing/multiple hits. Returns {id, fullName, email, companyName, departmentName} per user (subset).',
601
+ inputSchema: {
602
+ type: 'object',
603
+ additionalProperties: false,
604
+ properties: {
605
+ search: {
606
+ type: 'string',
607
+ description: 'Free-text search over fullName/email/username (case-insensitive).',
608
+ },
609
+ companyId: { type: 'integer', description: 'Filter by company id' },
610
+ departmentId: { type: 'integer', description: 'Filter by department id' },
611
+ limit: {
612
+ type: 'integer',
613
+ minimum: 1,
614
+ maximum: 100,
615
+ description: 'Page size (1-100, default 20)',
616
+ },
617
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
618
+ },
619
+ },
620
+ handler: (args) =>
621
+ systaFetch('GET', '/users', {
477
622
  query: {
478
- limit: args.limit ?? 20,
479
- offset: args.offset,
480
623
  search: args.search,
481
- statusId,
482
- assignedTo: args.assignedTo,
483
624
  companyId: args.companyId,
484
- categoryId: args.categoryId,
485
625
  departmentId: args.departmentId,
486
- priority: args.priority,
626
+ limit: args.limit ?? 20,
627
+ offset: args.offset,
628
+ },
629
+ }),
630
+ },
631
+ {
632
+ name: 'resolve_user_by_name',
633
+ description:
634
+ "Resolve a person's name (isim -> userId) for assignments and filters. Returns {userId, fullName, ambiguity} when a UNIQUE match is found; when MULTIPLE users match (e.g. 3 'Ahmet' users) returns {ambiguity:true, candidates:[{id, fullName, companyName, departmentName}], hint:'ASK the user which one'} -- DO NOT auto-pick the first candidate. Use userId with assign_request / assignedTo. When 'me/bana' is meant, use list_capabilities user.id instead (no resolve needed).",
635
+ inputSchema: {
636
+ type: 'object',
637
+ additionalProperties: false,
638
+ required: ['name'],
639
+ properties: {
640
+ name: { type: 'string', description: 'Person name to resolve (partial match allowed)' },
641
+ companyId: {
642
+ type: 'integer',
643
+ description: 'Restrict search to a specific company (optional)',
487
644
  },
645
+ },
646
+ },
647
+ handler: async (args) => {
648
+ const res = await systaFetch('GET', '/users', {
649
+ query: { search: args.name, companyId: args.companyId, limit: 10 },
488
650
  });
651
+ if (!res.ok) {
652
+ return res;
653
+ }
654
+ const users =
655
+ res.data?.data?.users || res.data?.data?.items || res.data?.data || res.data || [];
656
+ const arr = Array.isArray(users) ? users : [];
657
+ const trimmed = String(args.name).trim().toLowerCase();
658
+ const exact = arr.filter(
659
+ (u) =>
660
+ String(u.fullName || '')
661
+ .trim()
662
+ .toLowerCase() === trimmed,
663
+ );
664
+ const pool = exact.length > 0 ? exact : arr;
665
+ if (pool.length === 0) {
666
+ return {
667
+ status: 200,
668
+ ok: true,
669
+ data: {
670
+ success: true,
671
+ data: {
672
+ matched: false,
673
+ hint: `No user found matching "${args.name}". Ask the user for the full name or a different spelling.`,
674
+ candidates: [],
675
+ },
676
+ },
677
+ };
678
+ }
679
+ if (pool.length === 1) {
680
+ const u = pool[0];
681
+ return {
682
+ status: 200,
683
+ ok: true,
684
+ data: {
685
+ success: true,
686
+ data: {
687
+ matched: true,
688
+ ambiguity: false,
689
+ userId: u.id,
690
+ fullName: u.fullName,
691
+ companyName: u.companyName || null,
692
+ departmentName: u.departmentName || null,
693
+ },
694
+ },
695
+ };
696
+ }
697
+ return {
698
+ status: 200,
699
+ ok: true,
700
+ data: {
701
+ success: true,
702
+ data: {
703
+ matched: true,
704
+ ambiguity: true,
705
+ hint: `Multiple users matched "${args.name}"; ASK the user which one -- do not auto-pick.`,
706
+ candidates: pool.slice(0, 10).map((u) => ({
707
+ id: u.id,
708
+ fullName: u.fullName,
709
+ email: u.email || null,
710
+ companyName: u.companyName || null,
711
+ departmentName: u.departmentName || null,
712
+ })),
713
+ },
714
+ },
715
+ };
716
+ },
717
+ },
718
+ {
719
+ name: 'bulk_update_requests',
720
+ description:
721
+ "Update MULTIPLE SysTa requests in ONE call (topluca guncelle) -- wraps PUT /api/requests/bulk. Use for 'these 5 talepler'i kritik yap' / 'set assignedTo to X for #189, #190, #191'. Provide requestIds (an array of numeric request ids/numbers) and updates (a partial field set applied to ALL of them). Do NOT loop calling update_request -- one bulk call is atomic per group and faster. If you need DIFFERENT updates per request, call bulk once per group. After success the response returns a batchId -- pass it to systa_api_call POST /requests/bulk/undo to revert if needed.",
722
+ inputSchema: {
723
+ type: 'object',
724
+ additionalProperties: false,
725
+ required: ['requestIds', 'updates'],
726
+ properties: {
727
+ requestIds: {
728
+ type: 'array',
729
+ items: { type: 'integer' },
730
+ minItems: 1,
731
+ maxItems: 500,
732
+ description: 'Request numbers to update (1-500).',
733
+ },
734
+ updates: {
735
+ type: 'object',
736
+ additionalProperties: true,
737
+ description:
738
+ 'Partial fields applied to every request in requestIds: statusId, assignedTo, priority, deadline, categoryId, departmentId, projectId, estimatedEffort (DAYS), personDay (DAYS), plannedStartDate, plannedEndDate, description, title.',
739
+ },
740
+ reason: {
741
+ type: 'string',
742
+ description: 'Optional audit reason (why the bulk change).',
743
+ },
744
+ },
745
+ },
746
+ handler: (args) => {
747
+ const body = { requestIds: args.requestIds, updates: args.updates };
748
+ if (args.reason) {
749
+ body.reason = args.reason;
750
+ }
751
+ return systaFetch('PUT', '/requests/bulk', { body });
489
752
  },
490
753
  },
491
754
  {
@@ -508,9 +771,128 @@ const TOOLS = [
508
771
  return systaFetch('GET', `/requests/${encodeURIComponent(rn)}`);
509
772
  },
510
773
  },
774
+ {
775
+ name: 'list_request_files',
776
+ description:
777
+ 'List the ATTACHMENTS (dosya ekleri) of a SysTa request (GET /requests/:requestNumber/files). Returns {id, fileName, fileType, fileSize, uploadedAt, uploadedBy}. Use these fileIds with download_request_file to fetch content. NOTE: request-level attachments are only ONE of the file sources in SysTa -- comment attachments (in comments[].fileIds) and kanban card attachments live separately (see download_request_file for the full source list).',
778
+ inputSchema: {
779
+ type: 'object',
780
+ additionalProperties: false,
781
+ required: ['requestNumber'],
782
+ properties: {
783
+ requestNumber: { type: 'integer', description: 'Request number' },
784
+ },
785
+ },
786
+ handler: (args) =>
787
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/files`),
788
+ },
789
+ {
790
+ name: 'list_request_comments',
791
+ description:
792
+ "List the COMMENTS (yorumlar) of a SysTa request (GET /requests/:requestNumber/comments). Returns each comment's text/JSON, author, timestamp AND fileIds[] (attachment ids on the comment). The request's DESCRIPTION is materialized as comment #1 (commentNo=1). Comment attachments are a common file source -- use fileIds[] with download_request_file. Pagination via limit/offset if supported by the backend.",
793
+ inputSchema: {
794
+ type: 'object',
795
+ additionalProperties: false,
796
+ required: ['requestNumber'],
797
+ properties: {
798
+ requestNumber: { type: 'integer', description: 'Request number' },
799
+ limit: {
800
+ type: 'integer',
801
+ minimum: 1,
802
+ maximum: 100,
803
+ description: 'Page size',
804
+ },
805
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
806
+ },
807
+ },
808
+ handler: (args) =>
809
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/comments`, {
810
+ query: { limit: args.limit, offset: args.offset },
811
+ }),
812
+ },
813
+ {
814
+ name: 'list_request_history',
815
+ description:
816
+ "List the CHANGE HISTORY / AUDIT LOG (degisiklik gecmisi / denetim) of a SysTa request (GET /requests/:requestNumber/history). Returns each change with actor, timestamp, field, oldValue, newValue. This is SysTa's per-entity audit trail -- there is no single 'audit log' endpoint. Use for 'X talebinde neler olmus / who changed the status'.",
817
+ inputSchema: {
818
+ type: 'object',
819
+ additionalProperties: false,
820
+ required: ['requestNumber'],
821
+ properties: {
822
+ requestNumber: { type: 'integer', description: 'Request number' },
823
+ limit: {
824
+ type: 'integer',
825
+ minimum: 1,
826
+ maximum: 100,
827
+ description: 'Page size',
828
+ },
829
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
830
+ },
831
+ },
832
+ handler: (args) =>
833
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/history`, {
834
+ query: { limit: args.limit, offset: args.offset },
835
+ }),
836
+ },
837
+ {
838
+ name: 'list_lookup',
839
+ description:
840
+ "Unified id-resolution / lookup tool for reference entities (company / team / department / organization / category / status). Wraps the per-kind GET endpoints in ONE tool so blind AIs don't juggle 6 separate list tools. Call BEFORE create_request/update_request when you need to turn a NAME into an id ('Atlas Sigorta' -> companyId, 'Hata' -> categoryId, 'Test Ediliyor' -> statusId). Returns rows with {id, name/key} for the chosen kind. For USERS use resolve_user_by_name (name ambiguity handling is different).",
841
+ inputSchema: {
842
+ type: 'object',
843
+ additionalProperties: false,
844
+ required: ['kind'],
845
+ properties: {
846
+ kind: {
847
+ type: 'string',
848
+ enum: ['company', 'team', 'department', 'organization', 'category', 'status'],
849
+ description: 'Which reference entity to look up',
850
+ },
851
+ search: {
852
+ type: 'string',
853
+ description: 'Free-text search (name/code contains)',
854
+ },
855
+ companyId: {
856
+ type: 'integer',
857
+ description:
858
+ 'Scope organization/department lookups to a specific company (ignored for others)',
859
+ },
860
+ limit: {
861
+ type: 'integer',
862
+ minimum: 1,
863
+ maximum: 100,
864
+ description: 'Page size (default 20)',
865
+ },
866
+ },
867
+ },
868
+ handler: (args) => {
869
+ const KIND_TO_PATH = {
870
+ company: '/companies',
871
+ team: '/teams',
872
+ department: '/departments',
873
+ organization: '/organizations',
874
+ category: '/categories',
875
+ status: '/statuses',
876
+ };
877
+ const path = KIND_TO_PATH[args.kind];
878
+ if (!path) {
879
+ throw new Error(`list_lookup: unknown kind '${args.kind}'`);
880
+ }
881
+ const query = {
882
+ search: args.search,
883
+ limit: args.limit ?? 20,
884
+ };
885
+ // company scoping only makes sense for org unit hierarchies.
886
+ if (args.companyId != null && (args.kind === 'department' || args.kind === 'organization')) {
887
+ query.companyId = args.companyId;
888
+ }
889
+ return systaFetch('GET', path, { query });
890
+ },
891
+ },
511
892
  {
512
893
  name: 'list_projects',
513
- description: 'List SysTa projects (projeler). Returns only projects the API key scope permits.',
894
+ description:
895
+ 'List/search SysTa projects (projeler) with pagination and filters. Project hierarchy: a project can have a parent_project_id (alt proje / sub-project). Filter to a subtree with parentProjectId=<id> (immediate children). NOTE: there is no root-only filter yet server-side; if you need only root projects, filter the response by projects with parentProjectId==null. Returns only projects the API key scope permits.',
514
896
  inputSchema: {
515
897
  type: 'object',
516
898
  additionalProperties: false,
@@ -522,10 +904,34 @@ const TOOLS = [
522
904
  description: 'Page size (1-100, default 20)',
523
905
  },
524
906
  offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
907
+ search: { type: 'string', description: 'Free-text search (project name/code/description)' },
908
+ companyId: { type: 'integer', description: 'Filter by company id' },
909
+ parentProjectId: {
910
+ type: 'integer',
911
+ description: 'Filter to the immediate children of this parent project',
912
+ },
913
+ status: {
914
+ type: 'string',
915
+ enum: ['active', 'passive', 'cancelled', 'completed'],
916
+ description: 'Filter by project lifecycle status',
917
+ },
918
+ sortBy: { type: 'string', description: 'Sort field (see project list docs)' },
919
+ sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction' },
525
920
  },
526
921
  },
527
922
  handler: (args) =>
528
- systaFetch('GET', '/projects', { query: { limit: args.limit ?? 20, offset: args.offset } }),
923
+ systaFetch('GET', '/projects', {
924
+ query: {
925
+ limit: args.limit ?? 20,
926
+ offset: args.offset,
927
+ search: args.search,
928
+ companyId: args.companyId,
929
+ parentProjectId: args.parentProjectId,
930
+ status: args.status,
931
+ sortBy: args.sortBy,
932
+ sortOrder: args.sortOrder,
933
+ },
934
+ }),
529
935
  },
530
936
  {
531
937
  name: 'create_request',
@@ -564,27 +970,72 @@ const TOOLS = [
564
970
  description:
565
971
  'Organization-unit id (optional; may be required by the active form; must belong to companyId)',
566
972
  },
973
+ departmentId: { type: 'integer', description: 'Department id (optional)' },
974
+ projectId: { type: 'integer', description: 'Link to a project (optional)' },
975
+ priority: {
976
+ type: 'string',
977
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
978
+ description: "Priority key. Defaults to backend default (typically 'normal').",
979
+ },
980
+ deadline: {
981
+ type: 'string',
982
+ description: 'Deadline (YYYY-MM-DD or ISO-8601 datetime)',
983
+ },
984
+ plannedStartDate: {
985
+ type: 'string',
986
+ description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
987
+ },
988
+ plannedEndDate: {
989
+ type: 'string',
990
+ description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
991
+ },
992
+ estimatedEffort: {
993
+ type: 'number',
994
+ minimum: 0,
995
+ description:
996
+ "Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
997
+ },
998
+ personDay: {
999
+ type: 'number',
1000
+ minimum: 0,
1001
+ description: 'Person-day scoring in CANONICAL DAYS.',
1002
+ },
567
1003
  },
568
1004
  },
569
- handler: (args) =>
570
- systaFetch('POST', '/requests', {
571
- body: {
572
- title: args.title,
573
- description: args.description ?? '',
574
- companyId: args.companyId,
575
- statusId: args.statusId,
576
- channel: args.channel ?? 'API',
577
- ...(args.assignedTo != null ? { assignedTo: args.assignedTo } : {}),
578
- ...(args.assignedTeams != null ? { assignedTeams: args.assignedTeams } : {}),
579
- ...(args.categoryId != null ? { categoryId: args.categoryId } : {}),
580
- ...(args.organizationId != null ? { organizationId: args.organizationId } : {}),
581
- },
582
- }),
1005
+ handler: (args) => {
1006
+ const body = {
1007
+ title: args.title,
1008
+ description: args.description ?? '',
1009
+ companyId: args.companyId,
1010
+ statusId: args.statusId,
1011
+ channel: args.channel ?? 'API',
1012
+ };
1013
+ const passthrough = [
1014
+ 'assignedTo',
1015
+ 'assignedTeams',
1016
+ 'categoryId',
1017
+ 'organizationId',
1018
+ 'departmentId',
1019
+ 'projectId',
1020
+ 'priority',
1021
+ 'deadline',
1022
+ 'plannedStartDate',
1023
+ 'plannedEndDate',
1024
+ 'estimatedEffort',
1025
+ 'personDay',
1026
+ ];
1027
+ for (const key of passthrough) {
1028
+ if (args[key] !== undefined && args[key] !== null) {
1029
+ body[key] = args[key];
1030
+ }
1031
+ }
1032
+ return systaFetch('POST', '/requests', { body });
1033
+ },
583
1034
  },
584
1035
  {
585
1036
  name: 'prepare_create_request',
586
1037
  description:
587
- 'Proactively fetch everything create_request needs for a company: the ACTIVE request form\'s required system fields PLUS valid id options (statuses, categories, organizations, departments). Call this FIRST whenever the user wants to "talep ac / open a request" so you fill categoryId/organizationId/statusId up front and avoid the 400 VALIDATION_FAILED roundtrip. Returns requiredFields[] and options{statusId,categoryId,organizationId,departmentId}. statusId options carry isFinal (true => a "tamamla/kapat" status). companyId is required (org/department options are scoped to it). A missing option list means that lookup is out of this key scope -- ask the user or use systa_api_call.',
1038
+ 'Proactively fetch everything create_request needs for a company: the ACTIVE request form\'s required system fields PLUS valid id options (statuses, categories, organizations, departments). Call this FIRST whenever the user wants to "talep ac / open a request" so you fill categoryId/organizationId/statusId up front and avoid the 400 VALIDATION_FAILED roundtrip. Returns requiredFields[] and options{statusId,categoryId,organizationId,departmentId}. statusId options carry isFinal (true => a "tamamla/kapat" status). companyId is required (org/department options are scoped to it). NOTE ON companyId: this is the MUSTERI (client) company the request is FOR -- often DIFFERENT from your own user.companyId (which is the internal handler company). If the user has not stated a company, resolve by name (systa_api_call GET /companies?search=...) or ASK the user; do NOT default to user.companyId or pick one from the list at random. A missing option list means that lookup is out of this key scope -- ask the user or use systa_api_call.',
588
1039
  inputSchema: {
589
1040
  type: 'object',
590
1041
  additionalProperties: false,
@@ -748,17 +1199,44 @@ const TOOLS = [
748
1199
  });
749
1200
  },
750
1201
  },
1202
+ {
1203
+ name: 'list_allowed_transitions',
1204
+ description:
1205
+ "Preview the target statuses the request's CURRENT status is allowed to move to (durum gecisi onizle). SysTa restricts status transitions by workflow rules (category/company/department/priority scoped); calling this BEFORE change_request_status avoids a 400 TRANSITION_NOT_ALLOWED round-trip and lets you map a natural-language target ('test asamasi', 'tamamla', 'yeniden ac') to a specific statusId / statusKey from the returned list. Wraps GET /requests/:requestNumber/allowed-transitions.",
1206
+ inputSchema: {
1207
+ type: 'object',
1208
+ additionalProperties: false,
1209
+ required: ['requestNumber'],
1210
+ properties: {
1211
+ requestNumber: {
1212
+ type: 'integer',
1213
+ description: "Request number whose current status's allowed transitions to preview",
1214
+ },
1215
+ },
1216
+ },
1217
+ handler: (args) =>
1218
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`),
1219
+ },
751
1220
  {
752
1221
  name: 'change_request_status',
753
1222
  description:
754
- 'Move a SysTa request to a target status (durum degistir / tamamla / kapat / yeniden ac), optionally with a closing comment. Resolve the target statusId by NAME first (list_capabilities or systa_api_call GET /statuses) -- do NOT guess the id. "tamamla/kapat" => a final status; "yeniden ac" => a non-final one. A given comment is posted right after the transition (plain text, auto-wrapped to TipTap). A status change may trigger a workflow gate/approval -- if the API returns such an error, read it and act, do not retry blindly.',
1223
+ 'Move a SysTa request to a target status (durum degistir / tamamla / kapat / yeniden ac), optionally with a closing comment. BEFORE calling, use list_allowed_transitions(requestNumber) to see valid target statuses -- transition rules restrict which statuses are reachable from the current one. Give EITHER statusId OR statusKey (statusKey resolves via GET /statuses; a natural-language user phrase like "test asamasi" maps to statusKey~test). "tamamla/kapat" => a final status (isFinal=true); "yeniden ac" => a non-final one. A given comment is posted right after the transition (plain text, auto-wrapped to TipTap). A status change may trigger a workflow gate/approval -- if the API returns 400 TRANSITION_NOT_ALLOWED read the error.details, pick a valid target and retry; do NOT retry blindly.',
755
1224
  inputSchema: {
756
1225
  type: 'object',
757
1226
  additionalProperties: false,
758
- required: ['requestNumber', 'statusId'],
1227
+ required: ['requestNumber'],
759
1228
  properties: {
760
1229
  requestNumber: { type: 'integer', description: 'Request number to transition' },
761
- statusId: { type: 'integer', description: 'Target status id (resolve by name first)' },
1230
+ statusId: {
1231
+ type: 'integer',
1232
+ description:
1233
+ 'Target status id (from list_allowed_transitions). Provide statusId OR statusKey.',
1234
+ },
1235
+ statusKey: {
1236
+ type: 'string',
1237
+ description:
1238
+ "Target status key (e.g. 'completed', 'testing', 'in_progress'). Provide statusId OR statusKey.",
1239
+ },
762
1240
  comment: {
763
1241
  type: 'string',
764
1242
  description: 'Optional closing/transition comment (plain text)',
@@ -766,8 +1244,45 @@ const TOOLS = [
766
1244
  },
767
1245
  },
768
1246
  handler: async (args) => {
1247
+ let statusId = args.statusId ?? null;
1248
+ if (statusId == null && args.statusKey) {
1249
+ // Resolve statusKey -> statusId via allowed-transitions (workflow-aware:
1250
+ // prefers targets legal from the current status) with a fallback to the
1251
+ // global statuses list if allowed-transitions doesn't match the key.
1252
+ const allowedRes = await systaFetch(
1253
+ 'GET',
1254
+ `/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`,
1255
+ );
1256
+ const allowed = allowedRes?.data?.data?.allowedTransitions || [];
1257
+ const hit = allowed.find(
1258
+ (t) => String(t.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
1259
+ );
1260
+ if (hit) {
1261
+ statusId = hit.id ?? hit.statusId;
1262
+ }
1263
+ if (statusId == null) {
1264
+ const statusesRes = await systaFetch('GET', '/statuses');
1265
+ const all = statusesRes?.data?.data?.statuses || statusesRes?.data?.data || [];
1266
+ const g = all.find(
1267
+ (s) => String(s.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
1268
+ );
1269
+ if (g) {
1270
+ statusId = g.id;
1271
+ }
1272
+ }
1273
+ if (statusId == null) {
1274
+ throw new Error(
1275
+ `change_request_status: statusKey='${args.statusKey}' could not be resolved -- call list_allowed_transitions to see valid keys`,
1276
+ );
1277
+ }
1278
+ }
1279
+ if (statusId == null) {
1280
+ throw new Error(
1281
+ 'change_request_status: provide statusId or statusKey (see list_allowed_transitions)',
1282
+ );
1283
+ }
769
1284
  const res = await systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
770
- body: { statusId: args.statusId },
1285
+ body: { statusId },
771
1286
  });
772
1287
  if (args.comment && String(args.comment).trim()) {
773
1288
  const doc = {
@@ -784,7 +1299,7 @@ const TOOLS = [
784
1299
  {
785
1300
  name: 'update_request',
786
1301
  description:
787
- 'Update fields of an existing SysTa request (talebi guncelle/duzenle) -- PARTIAL: only the fields you pass change. For STATUS use change_request_status, for ASSIGNEE use assign_request, for COMMENTS use add_request_comment (focused intents). Use this for title/description/category/department/priority/deadline/project edits. Resolve categoryId/departmentId/projectId by name first.',
1302
+ "Update fields of an existing SysTa request (talebi guncelle/duzenle) -- PARTIAL: only the fields you pass change. For STATUS use change_request_status, for ASSIGNEE use assign_request, for COMMENTS use add_request_comment (focused intents). Use this for title/description/category/department/priority/deadline/planned dates/effort/project edits. Resolve categoryId/departmentId/projectId by name first. estimatedEffort/actualEffort/personDay are in CANONICAL DAYS -- if the user says 'N hours', divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
788
1303
  inputSchema: {
789
1304
  type: 'object',
790
1305
  additionalProperties: false,
@@ -796,8 +1311,36 @@ const TOOLS = [
796
1311
  categoryId: { type: 'integer', description: 'Category id (resolve by name)' },
797
1312
  departmentId: { type: 'integer', description: 'Department id' },
798
1313
  projectId: { type: 'integer', description: 'Link to project id' },
799
- priority: { type: 'string', description: 'Priority key/level' },
800
- deadline: { type: 'string', description: 'Deadline (ISO date)' },
1314
+ priority: {
1315
+ type: 'string',
1316
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
1317
+ description: 'Priority key/level',
1318
+ },
1319
+ deadline: { type: 'string', description: 'Deadline (ISO date YYYY-MM-DD or ISO-8601)' },
1320
+ plannedStartDate: {
1321
+ type: 'string',
1322
+ description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
1323
+ },
1324
+ plannedEndDate: {
1325
+ type: 'string',
1326
+ description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
1327
+ },
1328
+ estimatedEffort: {
1329
+ type: 'number',
1330
+ minimum: 0,
1331
+ description:
1332
+ "Estimated effort in CANONICAL DAYS (person-days). '8 saat' => 8 / hoursPerDay.",
1333
+ },
1334
+ actualEffort: {
1335
+ type: 'number',
1336
+ minimum: 0,
1337
+ description: 'Actual effort in CANONICAL DAYS (person-days).',
1338
+ },
1339
+ personDay: {
1340
+ type: 'number',
1341
+ minimum: 0,
1342
+ description: 'Person-day scoring in CANONICAL DAYS.',
1343
+ },
801
1344
  },
802
1345
  },
803
1346
  handler: (args) => {
@@ -839,10 +1382,43 @@ const TOOLS = [
839
1382
  return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, { body });
840
1383
  },
841
1384
  },
1385
+ {
1386
+ name: 'create_request_board',
1387
+ description:
1388
+ 'Create the kanban task board (gorev panosu) for a SysTa request when it does NOT yet have one. Call THIS when get_request_board or manage_request_card returns 404 KANBAN_BOARD_NOT_FOUND (code 2031) -- do not retry the read/manage call blindly. Optional templateId picks a preset column layout; without it a default column set is created. Discover templates with systa_api_call GET /task-boards/templates. After board create you can immediately use manage_request_card to add cards.',
1389
+ inputSchema: {
1390
+ type: 'object',
1391
+ additionalProperties: false,
1392
+ required: ['requestId'],
1393
+ properties: {
1394
+ requestId: {
1395
+ type: 'integer',
1396
+ description: 'Request number to attach the board to',
1397
+ },
1398
+ templateId: {
1399
+ type: 'integer',
1400
+ description: 'Optional task-board template id (GET /task-boards/templates lists them)',
1401
+ },
1402
+ boardName: { type: 'string', description: 'Optional board display name' },
1403
+ },
1404
+ },
1405
+ handler: (args) => {
1406
+ const body = {};
1407
+ if (args.templateId != null) {
1408
+ body.templateId = args.templateId;
1409
+ }
1410
+ if (args.boardName) {
1411
+ body.boardName = args.boardName;
1412
+ }
1413
+ return systaFetch('POST', `/requests/${encodeURIComponent(args.requestId)}/kanban`, {
1414
+ body,
1415
+ });
1416
+ },
1417
+ },
842
1418
  {
843
1419
  name: 'get_request_board',
844
1420
  description:
845
- "Read the kanban task board (gorev panosu) of a SysTa request: its columns and the cards in each. READ-only -- to add/edit a card use manage_request_card, to move/reorder use move_request_card; for the request's own fields/comments use get_request/add_request_comment. requestId is the request number.",
1421
+ "Read the kanban task board (gorev panosu) of a SysTa request: its columns and the cards in each. READ-only -- to add/edit a card use manage_request_card, to move/reorder use move_request_card; for the request's own fields/comments use get_request/add_request_comment. requestId is the request number. If the response is 404 KANBAN_BOARD_NOT_FOUND (code 2031) the request has NO board yet -- call create_request_board(requestId) FIRST, then retry.",
846
1422
  inputSchema: {
847
1423
  type: 'object',
848
1424
  additionalProperties: false,
@@ -871,7 +1447,7 @@ const TOOLS = [
871
1447
  {
872
1448
  name: 'manage_request_card',
873
1449
  description:
874
- 'Create a card in a board column (requestId + columnId + cardTitle) OR update an existing card (requestId + cardId, partial fields). Does NOT move/reorder cards -- use move_request_card; read ids first with get_request_board. Resolve columnId and assignee ids before calling.',
1450
+ 'Create a card in a board column (requestId + columnId + cardTitle) OR update an existing card (requestId + cardId, partial fields). Does NOT move/reorder cards -- use move_request_card; read ids first with get_request_board. Resolve columnId and assignee ids before calling. PREREQUISITE: the request MUST have a kanban board -- if get_request_board or this call returns 404 KANBAN_BOARD_NOT_FOUND (code 2031), call create_request_board(requestId) first, then retry.',
875
1451
  inputSchema: {
876
1452
  type: 'object',
877
1453
  additionalProperties: false,
@@ -892,6 +1468,12 @@ const TOOLS = [
892
1468
  },
893
1469
  assignedTeams: { type: 'array', items: { type: 'integer' } },
894
1470
  priority: { type: 'string', description: 'low|normal|high|urgent|critical' },
1471
+ estimatedEffort: {
1472
+ type: 'number',
1473
+ minimum: 0,
1474
+ description:
1475
+ "Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
1476
+ },
895
1477
  tags: { type: 'array', items: { type: 'string' } },
896
1478
  deadline: { type: 'string', description: 'ISO-8601 datetime' },
897
1479
  cardColor: { type: 'string', description: '#RRGGBB hex' },
@@ -1064,6 +1646,32 @@ const TOOLS = [
1064
1646
  return systaFetch('POST', '/projects', { body });
1065
1647
  },
1066
1648
  },
1649
+ {
1650
+ name: 'duplicate_project',
1651
+ description:
1652
+ "Clone/duplicate a SysTa project (POST /projects/:id/duplicate) -- copies the source project's DEFINITION (name, company/department/org, parent, dates, weight, planned effort, governance toggles) into a new project. Optional {newProjectName}. NOT COPIED: sub-projects, requests, board, milestones, snapshots, actual effort/progress -- these are start-fresh on the new project. Returns the new project's id/code. Requires the project.create scope.",
1653
+ inputSchema: {
1654
+ type: 'object',
1655
+ additionalProperties: false,
1656
+ required: ['id'],
1657
+ properties: {
1658
+ id: { type: 'integer', description: 'Source project id to clone from' },
1659
+ newProjectName: {
1660
+ type: 'string',
1661
+ description: "New project's name (optional; server appends '(Copy)' if omitted)",
1662
+ },
1663
+ },
1664
+ },
1665
+ handler: (args) => {
1666
+ const body = {};
1667
+ if (args.newProjectName) {
1668
+ body.newProjectName = args.newProjectName;
1669
+ }
1670
+ return systaFetch('POST', `/projects/${encodeURIComponent(args.id)}/duplicate`, {
1671
+ body,
1672
+ });
1673
+ },
1674
+ },
1067
1675
  {
1068
1676
  name: 'get_project',
1069
1677
  description: 'Get one SysTa project (proje) by its numeric id, including sub-projects.',
@@ -1372,10 +1980,54 @@ const TOOLS = [
1372
1980
  }
1373
1981
  },
1374
1982
  },
1983
+ {
1984
+ name: 'create_plan_mode',
1985
+ description:
1986
+ "Create a plan mode / sprint scope (POST /plan/modes). A plan mode is the CONTAINER for a sprint's boards; create_plan_baseline then snapshots its live state. Boards is TECHNICALLY optional server-side but a mode WITHOUT boards produces empty baselines -- if you omit boards this tool auto-adds a single default board ({boardName:'Gorev Tahtasi'}) so create_plan_baseline works out of the box. Give boards[] explicitly when you want multiple boards or filterCriteria that pre-filters requests per board. Returns the created mode's id (use it for create_plan_baseline modeId).",
1987
+ inputSchema: {
1988
+ type: 'object',
1989
+ additionalProperties: false,
1990
+ required: ['name'],
1991
+ properties: {
1992
+ name: { type: 'string', description: 'Mode / sprint name (min 1 char)' },
1993
+ description: { type: 'string', description: 'Optional description' },
1994
+ companyId: {
1995
+ type: 'integer',
1996
+ description: 'Scope to a specific company (optional)',
1997
+ },
1998
+ boards: {
1999
+ type: 'array',
2000
+ description:
2001
+ 'Boards for the mode. If omitted a single default board is inserted. Each board: {boardName, filterCriteria?, targetCriteria?, displayOrder?}.',
2002
+ items: { type: 'object', additionalProperties: true },
2003
+ },
2004
+ },
2005
+ },
2006
+ handler: (args) => {
2007
+ const body = { name: args.name };
2008
+ if (args.description !== undefined) {
2009
+ body.description = args.description;
2010
+ }
2011
+ if (args.companyId != null) {
2012
+ body.companyId = args.companyId;
2013
+ }
2014
+ // Backend planModeBoardSchema alan adi 'name' (boardName DEĞİL); passthrough
2015
+ // olsa da service layer INSERT'te board.name kullanir -> boardName yollamak
2016
+ // NOT NULL constraint violation'a düşüyordu (F7 kor Haiku S-D2 basarisizligi).
2017
+ // Kullanicidan boards[] gelirse 'boardName' -> 'name' normalize et.
2018
+ const normalizeBoard = (b) =>
2019
+ b && typeof b === 'object' ? { ...b, name: b.name || b.boardName || 'Görev Tahtası' } : b;
2020
+ body.boards =
2021
+ Array.isArray(args.boards) && args.boards.length > 0
2022
+ ? args.boards.map(normalizeBoard)
2023
+ : [{ name: 'Görev Tahtası' }];
2024
+ return systaFetch('POST', '/plan/modes', { body });
2025
+ },
2026
+ },
1375
2027
  {
1376
2028
  name: 'create_plan_baseline',
1377
2029
  description:
1378
- 'Create a plan baseline/snapshot for a plan mode (POST /plan/modes/:modeId/baselines). Omit boards -- the server snapshots the mode live state (built for AI clients with no board state). Returns baselineId for submit_plan_for_approval.',
2030
+ 'Create a plan baseline/snapshot for a plan mode (POST /plan/modes/:modeId/baselines). Omit boards -- the server snapshots the mode live state (built for AI clients with no board state). Returns baselineId for submit_plan_for_approval. modeId comes from create_plan_mode.',
1379
2031
  inputSchema: {
1380
2032
  type: 'object',
1381
2033
  additionalProperties: false,
@@ -1400,6 +2052,46 @@ const TOOLS = [
1400
2052
  });
1401
2053
  },
1402
2054
  },
2055
+ {
2056
+ name: 'get_plan_baseline',
2057
+ description:
2058
+ "Get a single plan baseline (GET /plan/baselines/:baselineId). Blind-AI-friendly: fields defaults to 'summary' which STRIPS snapshotData (can be ~800KB on real plans) and returns just metadata + counts (baselineName, totalCards, totalEffort, approvalStatus, periodStart/End, createdAt, hasSnapshot boolean). Pass fields:'full' ONLY when you truly need the raw board/card snapshot (rare). For 'ne degisti / snapshot fark' use compare_plan_baseline, NOT the raw snapshot.",
2059
+ inputSchema: {
2060
+ type: 'object',
2061
+ additionalProperties: false,
2062
+ required: ['baselineId'],
2063
+ properties: {
2064
+ baselineId: { type: 'integer', description: 'Baseline id' },
2065
+ fields: {
2066
+ type: 'string',
2067
+ enum: ['summary', 'full'],
2068
+ description:
2069
+ "Response profile. Default: 'summary' (snapshot omitted, metadata only). 'full' returns the raw snapshot_data (heavy).",
2070
+ },
2071
+ },
2072
+ },
2073
+ handler: (args) =>
2074
+ systaFetch('GET', `/plan/baselines/${encodeURIComponent(args.baselineId)}`, {
2075
+ query: { fields: args.fields ?? 'summary' },
2076
+ }),
2077
+ },
2078
+ {
2079
+ name: 'compare_plan_baseline',
2080
+ description:
2081
+ "Compare a plan baseline snapshot (snapshot fark / baseline karsilastir) against the CURRENT board state (POST /plan/baselines/:baselineId/compare). Returns the diff (added/removed/changed cards, affected boards) WITHOUT streaming the raw baseline snapshot -- much smaller than GET /plan/baselines/:baselineId (which can be ~800KB). Use this whenever the user asks 'son snapshot ile bugunku durum arasindaki fark' / 'plani onceki halle karsilastir'. baselineId comes from create_plan_baseline or list_plan_baselines (systa_api_call GET /plan/baselines?modeId=). read_only.",
2082
+ inputSchema: {
2083
+ type: 'object',
2084
+ additionalProperties: false,
2085
+ required: ['baselineId'],
2086
+ properties: {
2087
+ baselineId: { type: 'integer', description: 'Baseline id to compare against live state' },
2088
+ },
2089
+ },
2090
+ handler: (args) =>
2091
+ systaFetch('POST', `/plan/baselines/${encodeURIComponent(args.baselineId)}/compare`, {
2092
+ body: {},
2093
+ }),
2094
+ },
1403
2095
  {
1404
2096
  name: 'submit_plan_for_approval',
1405
2097
  description:
@@ -1714,7 +2406,7 @@ const TOOLS = [
1714
2406
  {
1715
2407
  name: 'querybuilder_describe_fields',
1716
2408
  description:
1717
- "Discover queryable fields for a reportType: mode 'fields' (metadata/fieldKeys), 'options' (filter value options), or 'suggest' (q search). Never invent field names -- read mode='fields' first.",
2409
+ "Discover queryable fields for a reportType: mode 'fields' (metadata/fieldKeys), 'options' (filter value options), or 'suggest' (q search). Never invent field names -- read mode='fields' first. LARGE RESPONSE WARNING: mode='fields' can return 60KB+ (dozens of fields per reportType); if you are only looking for a common concept ('kapatilan/completed', 'atanan/assignee', 'kategori', 'tarih'), call mode='suggest' with a q term instead of dumping the whole list. Report reportType common fields: request→statusId/statusKey/completedAt/createdAt/priority/assignedTo/companyId; sla→slaStatus/breachedAt/dueAt; user→userId/isActive/isSuspended; project→statusId/lifecycle/healthScore. 'kapatilmis talep' = statusKey∈(completed,cancelled,rejected) OR isFinal=true (bkz. glossary 'kapatilmis talep').",
1718
2410
  inputSchema: {
1719
2411
  type: 'object',
1720
2412
  additionalProperties: false,
@@ -1855,10 +2547,21 @@ const TOOLS = [
1855
2547
  {
1856
2548
  name: 'download_request_file',
1857
2549
  description:
1858
- 'Download a SysTa attachment (request/task-card/project file) to the LOCAL disk and ' +
2550
+ 'Download a SysTa attachment (any file the API key can access) to the LOCAL disk and ' +
1859
2551
  'return the saved path. Requires the "file.download" scope; all access checks run ' +
1860
- 'server-side. fileId comes from file list endpoints (e.g. GET /requests/:n/files). ' +
1861
- 'Saves into a temp directory unless savePath is given.',
2552
+ 'server-side. Saves into a temp directory unless savePath is given. ' +
2553
+ 'SysTa files live under SEVEN different sources -- fileId is resolved from ONE of these ' +
2554
+ 'list endpoints (most user-visible files are actually COMMENT or KANBAN attachments, not ' +
2555
+ 'top-level request attachments): ' +
2556
+ '(1) request attachment: list_request_files(requestNumber) / GET /requests/:n/files, ' +
2557
+ '(2) comment attachment: list_request_comments(requestNumber) -- each comment carries fileIds[], ' +
2558
+ '(3) kanban card attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/files, ' +
2559
+ '(4) kanban card comment attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/comments (comment.fileIds[]), ' +
2560
+ '(5) project file: systa_api_call GET /projects/:id/files, ' +
2561
+ '(6) sub-form submission attachment: systa_api_call GET /form-submissions/:id/files, ' +
2562
+ '(7) announcement attachment: systa_api_call GET /announcements/:id. ' +
2563
+ 'If list_request_files returns empty, the request likely has NO top-level attachments ' +
2564
+ 'yet -- check comments (source 2) BEFORE concluding there is no file.',
1862
2565
  inputSchema: {
1863
2566
  type: 'object',
1864
2567
  additionalProperties: false,
@@ -2098,20 +2801,30 @@ const TOOL_MAP = new Map(TOOLS.map((tool) => [tool.name, tool]));
2098
2801
  const TOOL_REQUIRED_PERMISSION = {
2099
2802
  list_requests: 'request.read',
2100
2803
  get_request: 'request.tabs.general.view',
2804
+ list_request_files: 'request.tabs.files.view',
2805
+ list_request_comments: 'request.comment.read',
2806
+ list_request_history: 'request.tabs.history.view',
2807
+ list_lookup: 'request.read',
2101
2808
  list_projects: 'project.read',
2809
+ list_users: 'user.read',
2810
+ resolve_user_by_name: 'user.read',
2102
2811
  create_request: 'request.create',
2103
2812
  prepare_create_request: 'request.create',
2104
2813
  add_request_comment: 'request.comment.create',
2105
2814
  change_request_status: 'request.update',
2815
+ list_allowed_transitions: 'request.read',
2106
2816
  update_request: 'request.update',
2817
+ bulk_update_requests: 'request.update',
2107
2818
  assign_request: 'request.update',
2108
2819
  get_request_board: 'request.read',
2820
+ create_request_board: 'request.update',
2109
2821
  manage_request_card: 'request.update',
2110
2822
  move_request_card: 'request.update',
2111
2823
  request_approval: 'request.approval.create',
2112
2824
  respond_to_approval: 'request.approval.respond',
2113
2825
  list_pending_approvals: 'request.approval.view',
2114
2826
  create_project: 'project.create',
2827
+ duplicate_project: 'project.create',
2115
2828
  get_project: 'project.read',
2116
2829
  update_project: 'project.update',
2117
2830
  get_project_health: 'project.read',
@@ -2121,7 +2834,10 @@ const TOOL_REQUIRED_PERMISSION = {
2121
2834
  achieve_milestone: 'project.update',
2122
2835
  manage_project_stakeholders: 'project.update',
2123
2836
  manage_project_notes: 'project.update',
2837
+ create_plan_mode: 'request.update',
2124
2838
  create_plan_baseline: 'request.update',
2839
+ get_plan_baseline: 'request.read',
2840
+ compare_plan_baseline: 'request.read',
2125
2841
  submit_plan_for_approval: 'plan.approval.submit',
2126
2842
  respond_to_plan_approval: 'plan.approval.review',
2127
2843
  get_plan_critical_path: 'request.read',