systa-mcp 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/SKILL.md +70 -39
  2. package/package.json +4 -2
  3. package/server.js +759 -55
package/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 1.0.8
2
+ version: 1.1.0
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, entegrasyon | github | GET /api/github/connection |
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'`).
@@ -211,6 +232,16 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
211
232
 
212
233
  - `releaseType='standard'`, `requireAllStakeholderApprovals=false`, `companyId` from scope. Add items with `add_release_item`. Deployment workflow steps (`close-servers` → `start-db-migration` → `start-server-update` → `open-servers` → `complete`) are live ops — **never auto-advance blindly**; show which step you're on and confirm. Release notes: generate → review → publish (never auto-publish).
213
234
 
235
+ ### GitHub: bağla / issue aktar / push (kişisel entegrasyon) — `github.self`
236
+
237
+ Kullanıcının kendi GitHub bağlantısı üzerinden talep açma/aktarma. Tümü `systa_api_call` (named tool yok); gereken scope `github.self` (anahtarda yoksa bu uçlar katalogda görünmez → kullanıcıya `github.self` scope'lu yeni anahtar üretmesini söyle).
238
+
239
+ - **PAT bağlama AI'dan YAPILMAZ.** Kullanıcı GitHub fine-grained PAT'ını SysTa web arayüzünden (Ayarlar → GitHub) bağlar; AI asla PAT/secret istemez/almaz. Bağlantı yoksa kullanıcıyı oraya yönlendir.
240
+ - **Bağlantı durumu:** `systa_api_call` `GET /github/me/connection` (maskeli meta). Bağlı değilse import/push deneme — önce web'den bağlanmasını söyle.
241
+ - **Repo listele:** `systa_api_call` `GET /github/me/repos` — PAT'ın eriştiği repo'lar.
242
+ - **Issue içe aktar (GitHub → SysTa talep):** `systa_api_call` `POST /github/me/import` — seçili repo'nun issue'larını kullanıcının KENDİ talepleri olarak içe aktarır (idempotent, createdBy=self). "şu repodan issue'ları SysTa'ya aktar / GitHub'dan talep aç" → bu. Mutating + canlı: hangi repo / kaç issue özetini göster, **onay al**.
243
+ - **Talep push (SysTa → GitHub issue):** `systa_api_call` `POST /github/me/push` — kullanıcının talebini kendi repo'suna GitHub issue'su olarak gönderir/günceller (idempotent, node_id). Dış yan-etki (GitHub'a yazar) → **onay al**.
244
+
214
245
  ## Zincirleme & hata kurtarma (chaining & error recovery)
215
246
 
216
247
  SysTa returns structured errors. Recover deterministically instead of giving up.
@@ -222,7 +253,7 @@ SysTa returns structured errors. Recover deterministically instead of giving up.
222
253
  - Retry once after a fix. If it fails again with the same field, surface it to the user rather than looping.
223
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.)
224
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.
225
- 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`; requestIdboardIdcolumnId). Resolve first, act second.
226
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.
227
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.
228
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.8",
3
+ "version": "1.1.0",
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
@@ -420,12 +420,72 @@ const shapeOneModule = (caps, target) => {
420
420
  };
421
421
  };
422
422
 
423
+ // list_requests: 'openOnly' + 'overdue' both need the NON-final status ids -- GET
424
+ // /requests accepts statusId as an array, but has no excludeStatusIds. Fetch
425
+ // /statuses once and INCLUDE the non-final ones.
426
+ const resolveListRequestsStatusFilter = async (args) => {
427
+ const wantOnlyOpen = args.openOnly || args.overdue === true;
428
+ if (!wantOnlyOpen) {
429
+ return args.statusId;
430
+ }
431
+ const st = await systaFetch('GET', '/statuses').catch(() => null);
432
+ const rows = st && st.ok ? (st.data && (st.data.data || st.data)) || [] : [];
433
+ const openIds = (Array.isArray(rows) ? rows : []).filter((s) => s && !s.isFinal).map((s) => s.id);
434
+ return openIds.length > 0 ? openIds : args.statusId;
435
+ };
436
+
437
+ // list_requests: assemble the querystring the backend querySchema.js normalizes.
438
+ // Bracket-form (deadlineDaysRemaining[operator]) and comma-separated arrays are
439
+ // canonical for that validator; url.searchParams encodes brackets literally.
440
+ const buildListRequestsQuery = (args, statusId) => {
441
+ const query = {
442
+ limit: args.limit ?? 20,
443
+ offset: args.offset,
444
+ search: args.search,
445
+ statusId,
446
+ assignedTo: args.assignedTo,
447
+ companyId: args.companyId,
448
+ categoryId: args.categoryId,
449
+ departmentId: args.departmentId,
450
+ priority: args.priority,
451
+ // Blind-AI-friendly default: return the lightweight 'summary' shape
452
+ // (~1KB/row) unless the caller explicitly asks for detail/full.
453
+ fields: args.fields ?? 'summary',
454
+ };
455
+ if (Array.isArray(args.searchIn) && args.searchIn.length > 0) {
456
+ query.searchIn = args.searchIn.join(',');
457
+ }
458
+ if (Array.isArray(args.slaStatus) && args.slaStatus.length > 0) {
459
+ query.slaStatus = args.slaStatus.join(',');
460
+ }
461
+ if (args.slaBreached === true) {
462
+ query.slaBreached = true;
463
+ }
464
+ if (typeof args.hasAttachments === 'boolean') {
465
+ query.hasAttachments = args.hasAttachments;
466
+ }
467
+ if (args.deadlineFrom) {
468
+ query.deadlineFrom = args.deadlineFrom;
469
+ }
470
+ if (args.deadlineTo) {
471
+ query.deadlineTo = args.deadlineTo;
472
+ }
473
+ if (args.overdue === true) {
474
+ query['deadlineDaysRemaining[operator]'] = 'lt';
475
+ query['deadlineDaysRemaining[value]'] = 0;
476
+ } else if (typeof args.deadlineWithinDays === 'number') {
477
+ query['deadlineDaysRemaining[operator]'] = 'lte';
478
+ query['deadlineDaysRemaining[value]'] = args.deadlineWithinDays;
479
+ }
480
+ return query;
481
+ };
482
+
423
483
  // ── Tool definitions ─────────────────────────────────────────────────────────
424
484
  const TOOLS = [
425
485
  {
426
486
  name: 'list_requests',
427
487
  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 callexcludes 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).',
488
+ "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
489
  inputSchema: {
430
490
  type: 'object',
431
491
  additionalProperties: false,
@@ -437,7 +497,29 @@ const TOOLS = [
437
497
  description: 'Page size (1-100, default 20)',
438
498
  },
439
499
  offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
440
- search: { type: 'string', description: 'Free-text search' },
500
+ search: {
501
+ type: 'string',
502
+ description:
503
+ '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.',
504
+ },
505
+ searchIn: {
506
+ type: 'array',
507
+ items: {
508
+ type: 'string',
509
+ enum: [
510
+ 'title',
511
+ 'description',
512
+ 'requestnumber',
513
+ 'externalref',
514
+ 'category',
515
+ 'creator',
516
+ 'assignee',
517
+ 'comments',
518
+ ],
519
+ },
520
+ description:
521
+ 'Expand which fields the search string is matched against (default: title+description+requestnumber+externalref).',
522
+ },
441
523
  statusId: {
442
524
  type: ['integer', 'array'],
443
525
  items: { type: 'integer' },
@@ -455,37 +537,212 @@ const TOOLS = [
455
537
  companyId: { type: 'integer', description: 'Filter by company id' },
456
538
  categoryId: { type: 'integer', description: 'Filter by category id' },
457
539
  departmentId: { type: 'integer', description: 'Filter by department id' },
458
- priority: { type: 'string', description: 'Filter by priority key/level' },
540
+ priority: {
541
+ type: 'string',
542
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
543
+ description: 'Filter by priority key/level',
544
+ },
545
+ overdue: {
546
+ type: 'boolean',
547
+ description:
548
+ 'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly.',
549
+ },
550
+ slaStatus: {
551
+ type: 'array',
552
+ items: { type: 'string', enum: ['active', 'paused', 'completed', 'breached'] },
553
+ description: 'SLA status filter (multi-value). Backend accepts comma-separated string.',
554
+ },
555
+ slaBreached: {
556
+ type: 'boolean',
557
+ description: 'true => only requests whose SLA has been breached.',
558
+ },
559
+ hasAttachments: {
560
+ type: 'boolean',
561
+ description:
562
+ '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.',
563
+ },
564
+ deadlineFrom: {
565
+ type: 'string',
566
+ description: 'Deadline >= this date (YYYY-MM-DD).',
567
+ },
568
+ deadlineTo: {
569
+ type: 'string',
570
+ description: 'Deadline <= this date (YYYY-MM-DD).',
571
+ },
572
+ deadlineWithinDays: {
573
+ type: 'integer',
574
+ description:
575
+ 'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N).',
576
+ },
577
+ fields: {
578
+ type: 'string',
579
+ enum: ['summary', 'detail', 'full'],
580
+ description:
581
+ "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).",
582
+ },
459
583
  },
460
584
  },
461
585
  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', {
586
+ const statusId = await resolveListRequestsStatusFilter(args);
587
+ const query = buildListRequestsQuery(args, statusId);
588
+ return systaFetch('GET', '/requests', { query });
589
+ },
590
+ },
591
+ {
592
+ name: 'list_users',
593
+ description:
594
+ '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).',
595
+ inputSchema: {
596
+ type: 'object',
597
+ additionalProperties: false,
598
+ properties: {
599
+ search: {
600
+ type: 'string',
601
+ description: 'Free-text search over fullName/email/username (case-insensitive).',
602
+ },
603
+ companyId: { type: 'integer', description: 'Filter by company id' },
604
+ departmentId: { type: 'integer', description: 'Filter by department id' },
605
+ limit: {
606
+ type: 'integer',
607
+ minimum: 1,
608
+ maximum: 100,
609
+ description: 'Page size (1-100, default 20)',
610
+ },
611
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
612
+ },
613
+ },
614
+ handler: (args) =>
615
+ systaFetch('GET', '/users', {
477
616
  query: {
478
- limit: args.limit ?? 20,
479
- offset: args.offset,
480
617
  search: args.search,
481
- statusId,
482
- assignedTo: args.assignedTo,
483
618
  companyId: args.companyId,
484
- categoryId: args.categoryId,
485
619
  departmentId: args.departmentId,
486
- priority: args.priority,
620
+ limit: args.limit ?? 20,
621
+ offset: args.offset,
622
+ },
623
+ }),
624
+ },
625
+ {
626
+ name: 'resolve_user_by_name',
627
+ description:
628
+ "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).",
629
+ inputSchema: {
630
+ type: 'object',
631
+ additionalProperties: false,
632
+ required: ['name'],
633
+ properties: {
634
+ name: { type: 'string', description: 'Person name to resolve (partial match allowed)' },
635
+ companyId: {
636
+ type: 'integer',
637
+ description: 'Restrict search to a specific company (optional)',
487
638
  },
639
+ },
640
+ },
641
+ handler: async (args) => {
642
+ const res = await systaFetch('GET', '/users', {
643
+ query: { search: args.name, companyId: args.companyId, limit: 10 },
488
644
  });
645
+ if (!res.ok) {
646
+ return res;
647
+ }
648
+ const users =
649
+ res.data?.data?.users || res.data?.data?.items || res.data?.data || res.data || [];
650
+ const arr = Array.isArray(users) ? users : [];
651
+ const trimmed = String(args.name).trim().toLowerCase();
652
+ const exact = arr.filter(
653
+ (u) =>
654
+ String(u.fullName || '')
655
+ .trim()
656
+ .toLowerCase() === trimmed,
657
+ );
658
+ const pool = exact.length > 0 ? exact : arr;
659
+ if (pool.length === 0) {
660
+ return {
661
+ status: 200,
662
+ ok: true,
663
+ data: {
664
+ success: true,
665
+ data: {
666
+ matched: false,
667
+ hint: `No user found matching "${args.name}". Ask the user for the full name or a different spelling.`,
668
+ candidates: [],
669
+ },
670
+ },
671
+ };
672
+ }
673
+ if (pool.length === 1) {
674
+ const u = pool[0];
675
+ return {
676
+ status: 200,
677
+ ok: true,
678
+ data: {
679
+ success: true,
680
+ data: {
681
+ matched: true,
682
+ ambiguity: false,
683
+ userId: u.id,
684
+ fullName: u.fullName,
685
+ companyName: u.companyName || null,
686
+ departmentName: u.departmentName || null,
687
+ },
688
+ },
689
+ };
690
+ }
691
+ return {
692
+ status: 200,
693
+ ok: true,
694
+ data: {
695
+ success: true,
696
+ data: {
697
+ matched: true,
698
+ ambiguity: true,
699
+ hint: `Multiple users matched "${args.name}"; ASK the user which one -- do not auto-pick.`,
700
+ candidates: pool.slice(0, 10).map((u) => ({
701
+ id: u.id,
702
+ fullName: u.fullName,
703
+ email: u.email || null,
704
+ companyName: u.companyName || null,
705
+ departmentName: u.departmentName || null,
706
+ })),
707
+ },
708
+ },
709
+ };
710
+ },
711
+ },
712
+ {
713
+ name: 'bulk_update_requests',
714
+ description:
715
+ "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.",
716
+ inputSchema: {
717
+ type: 'object',
718
+ additionalProperties: false,
719
+ required: ['requestIds', 'updates'],
720
+ properties: {
721
+ requestIds: {
722
+ type: 'array',
723
+ items: { type: 'integer' },
724
+ minItems: 1,
725
+ maxItems: 500,
726
+ description: 'Request numbers to update (1-500).',
727
+ },
728
+ updates: {
729
+ type: 'object',
730
+ additionalProperties: true,
731
+ description:
732
+ 'Partial fields applied to every request in requestIds: statusId, assignedTo, priority, deadline, categoryId, departmentId, projectId, estimatedEffort (DAYS), personDay (DAYS), plannedStartDate, plannedEndDate, description, title.',
733
+ },
734
+ reason: {
735
+ type: 'string',
736
+ description: 'Optional audit reason (why the bulk change).',
737
+ },
738
+ },
739
+ },
740
+ handler: (args) => {
741
+ const body = { requestIds: args.requestIds, updates: args.updates };
742
+ if (args.reason) {
743
+ body.reason = args.reason;
744
+ }
745
+ return systaFetch('PUT', '/requests/bulk', { body });
489
746
  },
490
747
  },
491
748
  {
@@ -508,9 +765,128 @@ const TOOLS = [
508
765
  return systaFetch('GET', `/requests/${encodeURIComponent(rn)}`);
509
766
  },
510
767
  },
768
+ {
769
+ name: 'list_request_files',
770
+ description:
771
+ '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).',
772
+ inputSchema: {
773
+ type: 'object',
774
+ additionalProperties: false,
775
+ required: ['requestNumber'],
776
+ properties: {
777
+ requestNumber: { type: 'integer', description: 'Request number' },
778
+ },
779
+ },
780
+ handler: (args) =>
781
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/files`),
782
+ },
783
+ {
784
+ name: 'list_request_comments',
785
+ description:
786
+ "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.",
787
+ inputSchema: {
788
+ type: 'object',
789
+ additionalProperties: false,
790
+ required: ['requestNumber'],
791
+ properties: {
792
+ requestNumber: { type: 'integer', description: 'Request number' },
793
+ limit: {
794
+ type: 'integer',
795
+ minimum: 1,
796
+ maximum: 100,
797
+ description: 'Page size',
798
+ },
799
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
800
+ },
801
+ },
802
+ handler: (args) =>
803
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/comments`, {
804
+ query: { limit: args.limit, offset: args.offset },
805
+ }),
806
+ },
807
+ {
808
+ name: 'list_request_history',
809
+ description:
810
+ "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'.",
811
+ inputSchema: {
812
+ type: 'object',
813
+ additionalProperties: false,
814
+ required: ['requestNumber'],
815
+ properties: {
816
+ requestNumber: { type: 'integer', description: 'Request number' },
817
+ limit: {
818
+ type: 'integer',
819
+ minimum: 1,
820
+ maximum: 100,
821
+ description: 'Page size',
822
+ },
823
+ offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
824
+ },
825
+ },
826
+ handler: (args) =>
827
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/history`, {
828
+ query: { limit: args.limit, offset: args.offset },
829
+ }),
830
+ },
831
+ {
832
+ name: 'list_lookup',
833
+ description:
834
+ "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).",
835
+ inputSchema: {
836
+ type: 'object',
837
+ additionalProperties: false,
838
+ required: ['kind'],
839
+ properties: {
840
+ kind: {
841
+ type: 'string',
842
+ enum: ['company', 'team', 'department', 'organization', 'category', 'status'],
843
+ description: 'Which reference entity to look up',
844
+ },
845
+ search: {
846
+ type: 'string',
847
+ description: 'Free-text search (name/code contains)',
848
+ },
849
+ companyId: {
850
+ type: 'integer',
851
+ description:
852
+ 'Scope organization/department lookups to a specific company (ignored for others)',
853
+ },
854
+ limit: {
855
+ type: 'integer',
856
+ minimum: 1,
857
+ maximum: 100,
858
+ description: 'Page size (default 20)',
859
+ },
860
+ },
861
+ },
862
+ handler: (args) => {
863
+ const KIND_TO_PATH = {
864
+ company: '/companies',
865
+ team: '/teams',
866
+ department: '/departments',
867
+ organization: '/organizations',
868
+ category: '/categories',
869
+ status: '/statuses',
870
+ };
871
+ const path = KIND_TO_PATH[args.kind];
872
+ if (!path) {
873
+ throw new Error(`list_lookup: unknown kind '${args.kind}'`);
874
+ }
875
+ const query = {
876
+ search: args.search,
877
+ limit: args.limit ?? 20,
878
+ };
879
+ // company scoping only makes sense for org unit hierarchies.
880
+ if (args.companyId != null && (args.kind === 'department' || args.kind === 'organization')) {
881
+ query.companyId = args.companyId;
882
+ }
883
+ return systaFetch('GET', path, { query });
884
+ },
885
+ },
511
886
  {
512
887
  name: 'list_projects',
513
- description: 'List SysTa projects (projeler). Returns only projects the API key scope permits.',
888
+ description:
889
+ '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
890
  inputSchema: {
515
891
  type: 'object',
516
892
  additionalProperties: false,
@@ -522,10 +898,34 @@ const TOOLS = [
522
898
  description: 'Page size (1-100, default 20)',
523
899
  },
524
900
  offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
901
+ search: { type: 'string', description: 'Free-text search (project name/code/description)' },
902
+ companyId: { type: 'integer', description: 'Filter by company id' },
903
+ parentProjectId: {
904
+ type: 'integer',
905
+ description: 'Filter to the immediate children of this parent project',
906
+ },
907
+ status: {
908
+ type: 'string',
909
+ enum: ['active', 'passive', 'cancelled', 'completed'],
910
+ description: 'Filter by project lifecycle status',
911
+ },
912
+ sortBy: { type: 'string', description: 'Sort field (see project list docs)' },
913
+ sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort direction' },
525
914
  },
526
915
  },
527
916
  handler: (args) =>
528
- systaFetch('GET', '/projects', { query: { limit: args.limit ?? 20, offset: args.offset } }),
917
+ systaFetch('GET', '/projects', {
918
+ query: {
919
+ limit: args.limit ?? 20,
920
+ offset: args.offset,
921
+ search: args.search,
922
+ companyId: args.companyId,
923
+ parentProjectId: args.parentProjectId,
924
+ status: args.status,
925
+ sortBy: args.sortBy,
926
+ sortOrder: args.sortOrder,
927
+ },
928
+ }),
529
929
  },
530
930
  {
531
931
  name: 'create_request',
@@ -564,27 +964,72 @@ const TOOLS = [
564
964
  description:
565
965
  'Organization-unit id (optional; may be required by the active form; must belong to companyId)',
566
966
  },
967
+ departmentId: { type: 'integer', description: 'Department id (optional)' },
968
+ projectId: { type: 'integer', description: 'Link to a project (optional)' },
969
+ priority: {
970
+ type: 'string',
971
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
972
+ description: "Priority key. Defaults to backend default (typically 'normal').",
973
+ },
974
+ deadline: {
975
+ type: 'string',
976
+ description: 'Deadline (YYYY-MM-DD or ISO-8601 datetime)',
977
+ },
978
+ plannedStartDate: {
979
+ type: 'string',
980
+ description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
981
+ },
982
+ plannedEndDate: {
983
+ type: 'string',
984
+ description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
985
+ },
986
+ estimatedEffort: {
987
+ type: 'number',
988
+ minimum: 0,
989
+ description:
990
+ "Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
991
+ },
992
+ personDay: {
993
+ type: 'number',
994
+ minimum: 0,
995
+ description: 'Person-day scoring in CANONICAL DAYS.',
996
+ },
567
997
  },
568
998
  },
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
- }),
999
+ handler: (args) => {
1000
+ const body = {
1001
+ title: args.title,
1002
+ description: args.description ?? '',
1003
+ companyId: args.companyId,
1004
+ statusId: args.statusId,
1005
+ channel: args.channel ?? 'API',
1006
+ };
1007
+ const passthrough = [
1008
+ 'assignedTo',
1009
+ 'assignedTeams',
1010
+ 'categoryId',
1011
+ 'organizationId',
1012
+ 'departmentId',
1013
+ 'projectId',
1014
+ 'priority',
1015
+ 'deadline',
1016
+ 'plannedStartDate',
1017
+ 'plannedEndDate',
1018
+ 'estimatedEffort',
1019
+ 'personDay',
1020
+ ];
1021
+ for (const key of passthrough) {
1022
+ if (args[key] !== undefined && args[key] !== null) {
1023
+ body[key] = args[key];
1024
+ }
1025
+ }
1026
+ return systaFetch('POST', '/requests', { body });
1027
+ },
583
1028
  },
584
1029
  {
585
1030
  name: 'prepare_create_request',
586
1031
  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.',
1032
+ '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
1033
  inputSchema: {
589
1034
  type: 'object',
590
1035
  additionalProperties: false,
@@ -748,17 +1193,44 @@ const TOOLS = [
748
1193
  });
749
1194
  },
750
1195
  },
1196
+ {
1197
+ name: 'list_allowed_transitions',
1198
+ description:
1199
+ "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.",
1200
+ inputSchema: {
1201
+ type: 'object',
1202
+ additionalProperties: false,
1203
+ required: ['requestNumber'],
1204
+ properties: {
1205
+ requestNumber: {
1206
+ type: 'integer',
1207
+ description: "Request number whose current status's allowed transitions to preview",
1208
+ },
1209
+ },
1210
+ },
1211
+ handler: (args) =>
1212
+ systaFetch('GET', `/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`),
1213
+ },
751
1214
  {
752
1215
  name: 'change_request_status',
753
1216
  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.',
1217
+ '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
1218
  inputSchema: {
756
1219
  type: 'object',
757
1220
  additionalProperties: false,
758
- required: ['requestNumber', 'statusId'],
1221
+ required: ['requestNumber'],
759
1222
  properties: {
760
1223
  requestNumber: { type: 'integer', description: 'Request number to transition' },
761
- statusId: { type: 'integer', description: 'Target status id (resolve by name first)' },
1224
+ statusId: {
1225
+ type: 'integer',
1226
+ description:
1227
+ 'Target status id (from list_allowed_transitions). Provide statusId OR statusKey.',
1228
+ },
1229
+ statusKey: {
1230
+ type: 'string',
1231
+ description:
1232
+ "Target status key (e.g. 'completed', 'testing', 'in_progress'). Provide statusId OR statusKey.",
1233
+ },
762
1234
  comment: {
763
1235
  type: 'string',
764
1236
  description: 'Optional closing/transition comment (plain text)',
@@ -766,8 +1238,45 @@ const TOOLS = [
766
1238
  },
767
1239
  },
768
1240
  handler: async (args) => {
1241
+ let statusId = args.statusId ?? null;
1242
+ if (statusId == null && args.statusKey) {
1243
+ // Resolve statusKey -> statusId via allowed-transitions (workflow-aware:
1244
+ // prefers targets legal from the current status) with a fallback to the
1245
+ // global statuses list if allowed-transitions doesn't match the key.
1246
+ const allowedRes = await systaFetch(
1247
+ 'GET',
1248
+ `/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`,
1249
+ );
1250
+ const allowed = allowedRes?.data?.data?.allowedTransitions || [];
1251
+ const hit = allowed.find(
1252
+ (t) => String(t.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
1253
+ );
1254
+ if (hit) {
1255
+ statusId = hit.id ?? hit.statusId;
1256
+ }
1257
+ if (statusId == null) {
1258
+ const statusesRes = await systaFetch('GET', '/statuses');
1259
+ const all = statusesRes?.data?.data?.statuses || statusesRes?.data?.data || [];
1260
+ const g = all.find(
1261
+ (s) => String(s.statusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
1262
+ );
1263
+ if (g) {
1264
+ statusId = g.id;
1265
+ }
1266
+ }
1267
+ if (statusId == null) {
1268
+ throw new Error(
1269
+ `change_request_status: statusKey='${args.statusKey}' could not be resolved -- call list_allowed_transitions to see valid keys`,
1270
+ );
1271
+ }
1272
+ }
1273
+ if (statusId == null) {
1274
+ throw new Error(
1275
+ 'change_request_status: provide statusId or statusKey (see list_allowed_transitions)',
1276
+ );
1277
+ }
769
1278
  const res = await systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
770
- body: { statusId: args.statusId },
1279
+ body: { statusId },
771
1280
  });
772
1281
  if (args.comment && String(args.comment).trim()) {
773
1282
  const doc = {
@@ -784,7 +1293,7 @@ const TOOLS = [
784
1293
  {
785
1294
  name: 'update_request',
786
1295
  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.',
1296
+ "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
1297
  inputSchema: {
789
1298
  type: 'object',
790
1299
  additionalProperties: false,
@@ -796,8 +1305,36 @@ const TOOLS = [
796
1305
  categoryId: { type: 'integer', description: 'Category id (resolve by name)' },
797
1306
  departmentId: { type: 'integer', description: 'Department id' },
798
1307
  projectId: { type: 'integer', description: 'Link to project id' },
799
- priority: { type: 'string', description: 'Priority key/level' },
800
- deadline: { type: 'string', description: 'Deadline (ISO date)' },
1308
+ priority: {
1309
+ type: 'string',
1310
+ enum: ['low', 'normal', 'high', 'urgent', 'critical'],
1311
+ description: 'Priority key/level',
1312
+ },
1313
+ deadline: { type: 'string', description: 'Deadline (ISO date YYYY-MM-DD or ISO-8601)' },
1314
+ plannedStartDate: {
1315
+ type: 'string',
1316
+ description: 'Planned start (YYYY-MM-DD). Must be <= plannedEndDate.',
1317
+ },
1318
+ plannedEndDate: {
1319
+ type: 'string',
1320
+ description: 'Planned end (YYYY-MM-DD). Must be >= plannedStartDate.',
1321
+ },
1322
+ estimatedEffort: {
1323
+ type: 'number',
1324
+ minimum: 0,
1325
+ description:
1326
+ "Estimated effort in CANONICAL DAYS (person-days). '8 saat' => 8 / hoursPerDay.",
1327
+ },
1328
+ actualEffort: {
1329
+ type: 'number',
1330
+ minimum: 0,
1331
+ description: 'Actual effort in CANONICAL DAYS (person-days).',
1332
+ },
1333
+ personDay: {
1334
+ type: 'number',
1335
+ minimum: 0,
1336
+ description: 'Person-day scoring in CANONICAL DAYS.',
1337
+ },
801
1338
  },
802
1339
  },
803
1340
  handler: (args) => {
@@ -839,10 +1376,43 @@ const TOOLS = [
839
1376
  return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, { body });
840
1377
  },
841
1378
  },
1379
+ {
1380
+ name: 'create_request_board',
1381
+ description:
1382
+ '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.',
1383
+ inputSchema: {
1384
+ type: 'object',
1385
+ additionalProperties: false,
1386
+ required: ['requestId'],
1387
+ properties: {
1388
+ requestId: {
1389
+ type: 'integer',
1390
+ description: 'Request number to attach the board to',
1391
+ },
1392
+ templateId: {
1393
+ type: 'integer',
1394
+ description: 'Optional task-board template id (GET /task-boards/templates lists them)',
1395
+ },
1396
+ boardName: { type: 'string', description: 'Optional board display name' },
1397
+ },
1398
+ },
1399
+ handler: (args) => {
1400
+ const body = {};
1401
+ if (args.templateId != null) {
1402
+ body.templateId = args.templateId;
1403
+ }
1404
+ if (args.boardName) {
1405
+ body.boardName = args.boardName;
1406
+ }
1407
+ return systaFetch('POST', `/requests/${encodeURIComponent(args.requestId)}/kanban`, {
1408
+ body,
1409
+ });
1410
+ },
1411
+ },
842
1412
  {
843
1413
  name: 'get_request_board',
844
1414
  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.",
1415
+ "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
1416
  inputSchema: {
847
1417
  type: 'object',
848
1418
  additionalProperties: false,
@@ -871,7 +1441,7 @@ const TOOLS = [
871
1441
  {
872
1442
  name: 'manage_request_card',
873
1443
  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.',
1444
+ '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
1445
  inputSchema: {
876
1446
  type: 'object',
877
1447
  additionalProperties: false,
@@ -892,6 +1462,12 @@ const TOOLS = [
892
1462
  },
893
1463
  assignedTeams: { type: 'array', items: { type: 'integer' } },
894
1464
  priority: { type: 'string', description: 'low|normal|high|urgent|critical' },
1465
+ estimatedEffort: {
1466
+ type: 'number',
1467
+ minimum: 0,
1468
+ description:
1469
+ "Estimated effort in CANONICAL DAYS (person-days). If user says 'N hours' divide by list_capabilities.effortPolicy.hoursPerDay BEFORE writing.",
1470
+ },
895
1471
  tags: { type: 'array', items: { type: 'string' } },
896
1472
  deadline: { type: 'string', description: 'ISO-8601 datetime' },
897
1473
  cardColor: { type: 'string', description: '#RRGGBB hex' },
@@ -1064,6 +1640,32 @@ const TOOLS = [
1064
1640
  return systaFetch('POST', '/projects', { body });
1065
1641
  },
1066
1642
  },
1643
+ {
1644
+ name: 'duplicate_project',
1645
+ description:
1646
+ "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.",
1647
+ inputSchema: {
1648
+ type: 'object',
1649
+ additionalProperties: false,
1650
+ required: ['id'],
1651
+ properties: {
1652
+ id: { type: 'integer', description: 'Source project id to clone from' },
1653
+ newProjectName: {
1654
+ type: 'string',
1655
+ description: "New project's name (optional; server appends '(Copy)' if omitted)",
1656
+ },
1657
+ },
1658
+ },
1659
+ handler: (args) => {
1660
+ const body = {};
1661
+ if (args.newProjectName) {
1662
+ body.newProjectName = args.newProjectName;
1663
+ }
1664
+ return systaFetch('POST', `/projects/${encodeURIComponent(args.id)}/duplicate`, {
1665
+ body,
1666
+ });
1667
+ },
1668
+ },
1067
1669
  {
1068
1670
  name: 'get_project',
1069
1671
  description: 'Get one SysTa project (proje) by its numeric id, including sub-projects.',
@@ -1372,10 +1974,48 @@ const TOOLS = [
1372
1974
  }
1373
1975
  },
1374
1976
  },
1977
+ {
1978
+ name: 'create_plan_mode',
1979
+ description:
1980
+ "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).",
1981
+ inputSchema: {
1982
+ type: 'object',
1983
+ additionalProperties: false,
1984
+ required: ['name'],
1985
+ properties: {
1986
+ name: { type: 'string', description: 'Mode / sprint name (min 1 char)' },
1987
+ description: { type: 'string', description: 'Optional description' },
1988
+ companyId: {
1989
+ type: 'integer',
1990
+ description: 'Scope to a specific company (optional)',
1991
+ },
1992
+ boards: {
1993
+ type: 'array',
1994
+ description:
1995
+ 'Boards for the mode. If omitted a single default board is inserted. Each board: {boardName, filterCriteria?, targetCriteria?, displayOrder?}.',
1996
+ items: { type: 'object', additionalProperties: true },
1997
+ },
1998
+ },
1999
+ },
2000
+ handler: (args) => {
2001
+ const body = { name: args.name };
2002
+ if (args.description !== undefined) {
2003
+ body.description = args.description;
2004
+ }
2005
+ if (args.companyId != null) {
2006
+ body.companyId = args.companyId;
2007
+ }
2008
+ body.boards =
2009
+ Array.isArray(args.boards) && args.boards.length > 0
2010
+ ? args.boards
2011
+ : [{ boardName: 'Görev Tahtası', displayOrder: 1 }];
2012
+ return systaFetch('POST', '/plan/modes', { body });
2013
+ },
2014
+ },
1375
2015
  {
1376
2016
  name: 'create_plan_baseline',
1377
2017
  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.',
2018
+ '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
2019
  inputSchema: {
1380
2020
  type: 'object',
1381
2021
  additionalProperties: false,
@@ -1400,6 +2040,46 @@ const TOOLS = [
1400
2040
  });
1401
2041
  },
1402
2042
  },
2043
+ {
2044
+ name: 'get_plan_baseline',
2045
+ description:
2046
+ "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.",
2047
+ inputSchema: {
2048
+ type: 'object',
2049
+ additionalProperties: false,
2050
+ required: ['baselineId'],
2051
+ properties: {
2052
+ baselineId: { type: 'integer', description: 'Baseline id' },
2053
+ fields: {
2054
+ type: 'string',
2055
+ enum: ['summary', 'full'],
2056
+ description:
2057
+ "Response profile. Default: 'summary' (snapshot omitted, metadata only). 'full' returns the raw snapshot_data (heavy).",
2058
+ },
2059
+ },
2060
+ },
2061
+ handler: (args) =>
2062
+ systaFetch('GET', `/plan/baselines/${encodeURIComponent(args.baselineId)}`, {
2063
+ query: { fields: args.fields ?? 'summary' },
2064
+ }),
2065
+ },
2066
+ {
2067
+ name: 'compare_plan_baseline',
2068
+ description:
2069
+ "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.",
2070
+ inputSchema: {
2071
+ type: 'object',
2072
+ additionalProperties: false,
2073
+ required: ['baselineId'],
2074
+ properties: {
2075
+ baselineId: { type: 'integer', description: 'Baseline id to compare against live state' },
2076
+ },
2077
+ },
2078
+ handler: (args) =>
2079
+ systaFetch('POST', `/plan/baselines/${encodeURIComponent(args.baselineId)}/compare`, {
2080
+ body: {},
2081
+ }),
2082
+ },
1403
2083
  {
1404
2084
  name: 'submit_plan_for_approval',
1405
2085
  description:
@@ -1714,7 +2394,7 @@ const TOOLS = [
1714
2394
  {
1715
2395
  name: 'querybuilder_describe_fields',
1716
2396
  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.",
2397
+ "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
2398
  inputSchema: {
1719
2399
  type: 'object',
1720
2400
  additionalProperties: false,
@@ -1855,10 +2535,21 @@ const TOOLS = [
1855
2535
  {
1856
2536
  name: 'download_request_file',
1857
2537
  description:
1858
- 'Download a SysTa attachment (request/task-card/project file) to the LOCAL disk and ' +
2538
+ 'Download a SysTa attachment (any file the API key can access) to the LOCAL disk and ' +
1859
2539
  '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.',
2540
+ 'server-side. Saves into a temp directory unless savePath is given. ' +
2541
+ 'SysTa files live under SEVEN different sources -- fileId is resolved from ONE of these ' +
2542
+ 'list endpoints (most user-visible files are actually COMMENT or KANBAN attachments, not ' +
2543
+ 'top-level request attachments): ' +
2544
+ '(1) request attachment: list_request_files(requestNumber) / GET /requests/:n/files, ' +
2545
+ '(2) comment attachment: list_request_comments(requestNumber) -- each comment carries fileIds[], ' +
2546
+ '(3) kanban card attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/files, ' +
2547
+ '(4) kanban card comment attachment: systa_api_call GET /requests/:reqId/kanban/cards/:cardId/comments (comment.fileIds[]), ' +
2548
+ '(5) project file: systa_api_call GET /projects/:id/files, ' +
2549
+ '(6) sub-form submission attachment: systa_api_call GET /form-submissions/:id/files, ' +
2550
+ '(7) announcement attachment: systa_api_call GET /announcements/:id. ' +
2551
+ 'If list_request_files returns empty, the request likely has NO top-level attachments ' +
2552
+ 'yet -- check comments (source 2) BEFORE concluding there is no file.',
1862
2553
  inputSchema: {
1863
2554
  type: 'object',
1864
2555
  additionalProperties: false,
@@ -2098,20 +2789,30 @@ const TOOL_MAP = new Map(TOOLS.map((tool) => [tool.name, tool]));
2098
2789
  const TOOL_REQUIRED_PERMISSION = {
2099
2790
  list_requests: 'request.read',
2100
2791
  get_request: 'request.tabs.general.view',
2792
+ list_request_files: 'request.tabs.files.view',
2793
+ list_request_comments: 'request.comment.read',
2794
+ list_request_history: 'request.tabs.history.view',
2795
+ list_lookup: 'request.read',
2101
2796
  list_projects: 'project.read',
2797
+ list_users: 'user.read',
2798
+ resolve_user_by_name: 'user.read',
2102
2799
  create_request: 'request.create',
2103
2800
  prepare_create_request: 'request.create',
2104
2801
  add_request_comment: 'request.comment.create',
2105
2802
  change_request_status: 'request.update',
2803
+ list_allowed_transitions: 'request.read',
2106
2804
  update_request: 'request.update',
2805
+ bulk_update_requests: 'request.update',
2107
2806
  assign_request: 'request.update',
2108
2807
  get_request_board: 'request.read',
2808
+ create_request_board: 'request.update',
2109
2809
  manage_request_card: 'request.update',
2110
2810
  move_request_card: 'request.update',
2111
2811
  request_approval: 'request.approval.create',
2112
2812
  respond_to_approval: 'request.approval.respond',
2113
2813
  list_pending_approvals: 'request.approval.view',
2114
2814
  create_project: 'project.create',
2815
+ duplicate_project: 'project.create',
2115
2816
  get_project: 'project.read',
2116
2817
  update_project: 'project.update',
2117
2818
  get_project_health: 'project.read',
@@ -2121,7 +2822,10 @@ const TOOL_REQUIRED_PERMISSION = {
2121
2822
  achieve_milestone: 'project.update',
2122
2823
  manage_project_stakeholders: 'project.update',
2123
2824
  manage_project_notes: 'project.update',
2825
+ create_plan_mode: 'request.update',
2124
2826
  create_plan_baseline: 'request.update',
2827
+ get_plan_baseline: 'request.read',
2828
+ compare_plan_baseline: 'request.read',
2125
2829
  submit_plan_for_approval: 'plan.approval.submit',
2126
2830
  respond_to_plan_approval: 'plan.approval.review',
2127
2831
  get_plan_critical_path: 'request.read',