systa-mcp 1.5.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -1
- package/SKILL.md +52 -3
- package/package.json +1 -1
- package/server.js +697 -32
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ claude mcp add systa \
|
|
|
101
101
|
|
|
102
102
|
## Araçlar (tools)
|
|
103
103
|
|
|
104
|
-
Toplam **
|
|
104
|
+
Toplam **90 araç** vardır; `tools/list` yanıtı anahtarın kapsamına göre filtrelenir
|
|
105
105
|
(bkz. "Scope-aware araç listesi"). Aşağıdakiler en sık kullanılan çekirdek araçlardır —
|
|
106
106
|
tam katalog `list_capabilities` ile keşfedilir.
|
|
107
107
|
|
|
@@ -119,6 +119,33 @@ tam katalog `list_capabilities` ile keşfedilir.
|
|
|
119
119
|
| `describe_module` | Bir modülün (örn. request/kanban/plan) amacı, kavramları ve çağrılabilir endpoint'leri — kullanıcı niyetini doğru modüle eşlemek için (oryantasyon) |
|
|
120
120
|
| `describe_endpoint` | Tek endpoint detayı: alan tipleri, güvenlik sınıfı ve tam HTTP Response Contract v2 — çağrı gövdesini kurmadan ve yanıtı zincirlemeden önce |
|
|
121
121
|
|
|
122
|
+
### Agent koordinasyonu (çalışma oturumu ve sohbet)
|
|
123
|
+
|
|
124
|
+
İki agent aynı kayıtta habersiz çalışırsa birbirinin işini ezer. Bu araçlar tam olarak
|
|
125
|
+
onu önler. Sıra: **bak → konuş → sahiplen → sinyal ver → bırak.**
|
|
126
|
+
|
|
127
|
+
| Araç | Açıklama |
|
|
128
|
+
| ----------------------------- | ----------------------------------------------------------------------------------------- |
|
|
129
|
+
| `list_active_work_sessions` | Şu anda kim neyin üzerinde çalışıyor — düzenlemeden ÖNCE bak — `work-session.view-active` |
|
|
130
|
+
| `start_work_session` | Kaydı sahiplen (talep ya da kart, tam biri); nota YERİNİ yaz — `work-session.manage` |
|
|
131
|
+
| `send_work_session_heartbeat` | Çalışırken sinyal ver; 15 dakika sessizlik oturumu kapatır — `work-session.manage` |
|
|
132
|
+
| `update_work_session_note` | Yer değiştirdiysen notu tazele (`null` notu siler) — `work-session.manage` |
|
|
133
|
+
| `end_work_session` | İddiayı bırak; gövde göndermek gerekmez — `work-session.manage` |
|
|
134
|
+
| `list_my_work_sessions` | Kendi açık oturumların — `work-session.manage` |
|
|
135
|
+
| `list_work_session_history` | Bir kaydın geçmiş oturumları (imleç geriye gider) — `work-session.view-active` |
|
|
136
|
+
| `send_work_session_message` | Birinin açık oturumuna yaz (kapalı oturum 409 döner) — `collaboration.message` |
|
|
137
|
+
| `send_room_message` | Kaydın kendisine kalıcı not: `project` / `request` / `card` — `collaboration.message` |
|
|
138
|
+
| `read_messages` | Konuşmayı oku; `afterId` ile "son okuduğumdan beri ne geldi" |
|
|
139
|
+
| `listen_room` | Sınırlı süre canlı dinle (varsayılan 30 sn, en çok 120) — `work-session.view-active` |
|
|
140
|
+
|
|
141
|
+
**Odaya katılma diye bir adım yoktur.** Oda abone olunan kanal değil, kaydın kendisidir;
|
|
142
|
+
erişim kapsamın yetiyorsa okur ve yazarsın. Gantt ve k&k chart üzerindeki düğüm proje,
|
|
143
|
+
talep ya da kart olabilir — mesaj düğümün **temsil ettiği şeyin** odasına gider.
|
|
144
|
+
|
|
145
|
+
**Sürekli dinleme (monitor) kipi yoktur.** MCP istemcisine push gelmez. Bekliyorsan
|
|
146
|
+
`listen_room`, çalışıyorsan uğrayış başına tek `read_messages(afterId)`. İkisini birden
|
|
147
|
+
yapmak ya da `listen_room` çağrılarını zincirlemek turu boşa harcar.
|
|
148
|
+
|
|
122
149
|
`systa_api_call` ile, özel bir aracı olmayan herhangi bir endpoint çağrılabilir
|
|
123
150
|
(örn. `POST /requests`, `POST /requests/42/comments`). Anahtarın scope'u dışındaki
|
|
124
151
|
çağrılar `403` döner.
|
|
@@ -226,3 +253,9 @@ npm publish # publishConfig.access=public ile public yayınlanır
|
|
|
226
253
|
```
|
|
227
254
|
|
|
228
255
|
Sürüm `package.json` ve `server.js` (`SERVER_VERSION`) içinde **birlikte** yükseltilir.
|
|
256
|
+
|
|
257
|
+
### Yayınlanmamış değişiklikler (sonraki yayına girer)
|
|
258
|
+
|
|
259
|
+
- `resolve_user_by_name` çıktısı `list_users` ile aynı nested şekli ilan eder: `company{id,name}`,
|
|
260
|
+
`department{id,name}` (eski düz `companyName`/`departmentName` kaldırıldı — Faz 33).
|
|
261
|
+
- SKILL.md: `stakeHolders` → `stakeholders` (tek yazım; arka uç `stakeHolders` kabul etmez).
|
package/SKILL.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
version: 1.
|
|
2
|
+
version: 1.8.0
|
|
3
3
|
name: systa
|
|
4
4
|
description: |
|
|
5
5
|
Drive SysTa (Talep Takip Sistemi / SysTa) — a
|
|
@@ -79,7 +79,7 @@ Short shared vocabulary. Use these words with the user; never expose raw REST pa
|
|
|
79
79
|
| --------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
80
|
| Talep | Request / ticket | Core unit of work. Has `title` (min 5), `companyId`, `statusId`, priority, category, assignee, comments, files, relations. Çoğu iş bir talebin etrafında döner. |
|
|
81
81
|
| Durum | Status | Lifecycle state (Açık/Open, Devam Ediyor/In Progress, Tamamlandı/Done). `is_final` durumlar kapanışı temsil eder. Resolved via `GET /api/statuses`. |
|
|
82
|
-
| Öncelik | Priority | normal / high / critical. Belirtilmezse `normal`.
|
|
82
|
+
| Öncelik | Priority | low / normal / high / urgent / critical. Belirtilmezse `normal`. |
|
|
83
83
|
| Kategori | Category | Talep sınıflandırması (catalog/config). |
|
|
84
84
|
| Yorum | Comment | Talebe eklenen not/yorum (`commentText` plain string ok). |
|
|
85
85
|
| Atama | Assignment | `assignedTo` = bir kullanıcı. "bana/üzerime" → mevcut `userId`. |
|
|
@@ -163,6 +163,7 @@ veya `describe_endpoint` ile gövdeyi netleştir.
|
|
|
163
163
|
5. **Resolve dynamic required fields up front.** Before a create/mutation, resolve the entity's required fields _for this vendor_ (e.g. `statusId` via `GET /api/statuses`, `companyId` from `who-am-i`, approver name → `userId` via user lookup). Don't submit a half-filled body and let it 400 — pre-resolve, then submit. For a main request form use the `form` module; for an alt form use `describe_endpoint('/api/sub-forms/:id','GET')` and then read `GET /api/sub-forms/:id`. Never substitute `/forms/:id` for an alt form.
|
|
164
164
|
6. **Safety tiers gate confirmation.** `read_only` → just do it. `mutating`/`idempotent` → do it, then report. **`destructive`** (revoke access, delete, hard removal) and live side-effecting actions (publish, send invite, advance release workflow, test webhook) → show a one-line "şunu yapacağım" and get an explicit OK first. Publishing (announcement/release notes/product update) is **never** auto-published — show a summary, get approval.
|
|
165
165
|
7. **Don't over-ask scope.** `companyId`/`userId`/`vendorSchema` come from `who-am-i`; don't ask the user for them when a single sane value exists.
|
|
166
|
+
8. **Write bodies are NESTED (raw `systa_api_call` POST/PUT/PATCH).** Identity references go as groups, exactly as `describe_endpoint(...).fields` shows: `{ "status": { "id": 9 }, "company": { "id": 12 }, "priority": { "key": "high" }, "project": null }` — never flat `statusId`/`companyId` (transitional, will be rejected). `null` on a group clears the link. GET query/filter keys stay flat (`statusId=9`). Named tools (`create_request`, `update_request`, `change_request_status`, `bulk_update_requests`, `create_project`, `update_project`, `create_release_list`, `add_release_item`, `log_effort`, `update_effort_entry`, `request_approval`) take flat args and convert for you.
|
|
166
167
|
|
|
167
168
|
## Discovery
|
|
168
169
|
|
|
@@ -200,6 +201,54 @@ So the flow is: **user intent → `list_capabilities` (module) → `describe_mod
|
|
|
200
201
|
|
|
201
202
|
Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from the catalog. Resolve ids first, act, then summarize in the user's language.
|
|
202
203
|
|
|
204
|
+
### Aynı iş üzerinde çakışmamak (agent koordinasyonu) — `list_active_work_sessions` → mesajlaşma
|
|
205
|
+
|
|
206
|
+
**Bir talebi ya da görev kartını düzenlemeden ÖNCE bu akışı işlet.** İki agentin habersiz aynı dosyada çalışması, çakışan değişiklik ve tekrar eden iş üretir; bu araçlar tam olarak onu önlemek için var.
|
|
207
|
+
|
|
208
|
+
1. **Bak.** `list_active_work_sessions` — o kayıtta çalışan var mı? Her satır kişiyi, hedefi (talep ya da kart), başlangıcı, son canlılık sinyalini ve kişinin bıraktığı **serbest notu** taşır. En değerli alan nottur: "src/models/foo.js içindeyim" gibi. Karar vermeden önce onu oku. Bir oturum durdurulana ya da on beş dakika sessiz kalana kadar listede durur, yani listelenen oturum canlı bir iddiadır.
|
|
209
|
+
2. **Konuş.** Çakışma varsa `send_work_session_message(sessionId, body)` ile o kişiye yaz. İşe yarayan cümleler somut olanlardır: "src/models/foo.js bende, controller sende", "1042 migration'ını sen indirene kadar bekliyorum", "API sözleşmem `POST /api/x -> {id}`, buna göre devam edebilirsin".
|
|
210
|
+
3. **Başla.** `start_work_session(requestId | cardId, description)` ile kendi oturumunu aç. Hedef olarak **tam biri** verilir; ikisi birden ya da hiçbiri `EXACTLY_ONE_TARGET` ile reddedilir. `cardId` kartın **genel** kimliğidir (`get_request_board` içindeki `card.id`), pano içindeki "#3" sırası değildir. Nota niyet değil **yer** yaz: "src/models/request/requestCrudModel.js, create yolu" cümlesi "talep üzerinde çalışıyorum" cümlesinden çok daha işe yarar; çünkü senden sonra gelen agent kararını o nota bakarak verir.
|
|
211
|
+
4. **Çalışırken sinyal ver.** `send_work_session_heartbeat(sessionId)`. On beş dakika sessiz kalan oturum otomatik kapanır ve sen hâlâ dosyadayken iddian düşer. MCP çağrıları kendiliğinden sinyal sayılmaz: uzun bir derleme, test koşusu ya da büyük bir refactor sırasında adımlar arasında bunu çağır. Otomatik kapanışta bitiş zamanı **son sinyal anı** yazılır, kapanma anı değil; yani terk edilmiş oturum kimsenin eforunu şişirmez.
|
|
212
|
+
5. **Notunu güncel tut.** Yer değiştirdiysen `update_work_session_note(sessionId, description)`. Model katmanını alıp controller'a geçtiysen eski not, senden kaçınmaya çalışan agenti tam da girdiğin dosyaya yollar. `description: null` notu tamamen siler, oturum açık kalır.
|
|
213
|
+
6. **Bitince bırak.** `end_work_session(sessionId)`. "Bitti" demek görevi bitirmek değil, **oturmayı** bitirmektir: bir saatliğine ara vermek de bırakmaktır. Açık kalan oturum göstergeyi yalancı yapar ve sıradaki agenti boşuna bekletir. Gövde göndermene gerek yok; `endedAt` yalnız geçmişte olmuş bir durmayı kaydetmek için vardır.
|
|
214
|
+
|
|
215
|
+
**Kendi oturumlarını bulmak.** `list_my_work_sessions` yalnız _senin_ açık oturumlarını verir; `sessionId`'yi kaybettiysen ya da daha önce kapatmayı unuttuğun bir iddia olup olmadığını merak ediyorsan buraya bak. Başkalarının canlı iddiaları için `list_active_work_sessions`, bir kaydın geçmişi için `list_work_session_history(requestId | cardId)` kullanılır; geçmiş yeniden eskiye sıralıdır, bu yüzden imleç **geriye** gider (`beforeId`).
|
|
216
|
+
|
|
217
|
+
**Birini bulmak durmak demek değildir.** Aynı talepte farklı dosyalarda iki agent olması normaldir. Kabul edilmeyen şey, haber vermeden başlamaktır.
|
|
218
|
+
|
|
219
|
+
**Hangi araç ne zaman.** Mesaj kişinin _o anki oturumuna_ aitse `send_work_session_message`; kaydın _kendisine_ ait kalıcı bir not ise `send_room_message`. "Şu an o dosyadayım" oturuma aittir; "migration numaralandırması 1042'den devam ediyor" odaya aittir. Oda mesajı oturumdan uzun yaşar ve sonra gelen okur.
|
|
220
|
+
|
|
221
|
+
**Odaya "katılma" diye bir adım yoktur.** Oda abone olunan bir kanal değil, kaydın kendisidir. Erişim kapsamın o projeyi, talebi ya da kartı görmene izin veriyorsa odasını da okur ve yazarsın: oluşturulacak bir şey, katılma çağrısı, üyelik kaydı yoktur. Kapsam dışı bir oda boş küme değil doğrudan ret döner; yani ret "senin değil" demektir, "yok" demek değil.
|
|
222
|
+
|
|
223
|
+
**Hangi ekran hangi oda.** İnsan senin mesajını kaydın kendi ekranında görür, o yüzden odayı konuşmanın ait olduğu yere göre seç:
|
|
224
|
+
|
|
225
|
+
| roomType | roomId | Kullanicinin gordugu yer |
|
|
226
|
+
| --------- | -------------------- | ------------------------------------------------------------------------ |
|
|
227
|
+
| `request` | talep numarasi | Talep detay ekranindaki sohbet |
|
|
228
|
+
| `card` | kartin GENEL kimligi | Gorev kartinin sohbeti (`card.id`, pano ici "#3" degil) |
|
|
229
|
+
| `project` | proje kimligi | Proje sohbeti; Gantt ve k&k chart proje dugumune tiklandiginda bunu acar |
|
|
230
|
+
|
|
231
|
+
Gantt ve k&k chart üzerinde bir düğüm proje, talep ya da kart olabilir. Mesajı **düğümün temsil ettiği şeyin** odasına gönder: tek bir talebi ilgilendiren not, okuyucu grafiğe bakıyor diye proje odasına ait olmaz.
|
|
232
|
+
|
|
233
|
+
**Yeni mesajı nasıl alırsın: iki yol var, ikisini birden yapma.**
|
|
234
|
+
|
|
235
|
+
Önce şunu bil: MCP istemcisine **push yoktur**. Arka planda çalışan bir abonelik, seni uyandıran bir olay yoktur. Ya beklersin ya dönüp sorarsın.
|
|
236
|
+
|
|
237
|
+
1. **Bekliyorsan → `listen_room(roomType, roomId, seconds)`.** Bir soru sordun ve cevabı gelmeden devam edemiyorsun; ya da birinin dosyayı bırakmasını bekliyorsun. `listen_room` canlı kanalı açar, **sınırlı süre** bekler (varsayılan 30 saniye, en çok 120) ve o sırada geleni döndürür. `stoppedBy` neden döndüğünü söyler: `timeout` | `maxEvents` | `streamEnded`. Beklemeye devam etmek için yeniden çağırırsın.
|
|
238
|
+
2. **Çalışıyorsan → `read_messages(..., afterId)`.** Yapacak işin var, sadece sen meşgulken ne olduğunu öğrenmek istiyorsun. Her uğrayışta **tek çağrı**, döngü değil. Uğrayışlar arasında asıl işini yap.
|
|
239
|
+
|
|
240
|
+
**Sürekli dinleme (monitor) diye bir kip yoktur.** `listen_room` çağrılarını arka arkaya zincirleyip odayı "izlemeye almak" turunu boşa harcar: bir saat sessiz kalan oda sana kırk kez boş sonuç döndürür. Aynı şekilde `read_messages`'ı sıkı döngüde çağırmak da yanlış cevaptır. Doğru soru şudur: **şu an bekliyor muyum, yoksa çalışıyor muyum?**
|
|
241
|
+
|
|
242
|
+
**İmleç.** `afterId` = okuduğun son mesajın kimliği; yalnız ondan sonrakileri alırsın. **Zaman damgasını imleç olarak kullanma** — API milisaniye yazar, kolon mikrosaniye tutar ve aynı satır iki kez gelir (ölçüldü 2026-09-08); yanıttaki `pagination.nextAfterId` alanını kullan. İlk çağrıyı imleçsiz yap, yakın geçmişi al, `nextAfterId` değerini sakla ve bundan sonra onu kullan.
|
|
243
|
+
|
|
244
|
+
**Hiçbir yolda mesaj kaybolmaz.** Olaylar `read_messages` ile aynı biçimi taşır, ayrı bir çözümleme gerekmez. Akış koparsa `read_messages(afterId = gördüğün son id)` ile tam kaldığın yerden devam edersin. Canlılık sinyalleri taşıma katmanına aittir ve olay olarak görünmez; yani boş sonuç gerçekten "kimse bir şey söylemedi" demektir.
|
|
245
|
+
|
|
246
|
+
**Oturum konuşması oturumla biter.** Yarın yeniden çalışmaya başlamak **yeni** bir konuşma açar, dünkü satırlar geri gelmez. Oda konuşması ise kalıcıdır ve her oturumdan uzun yaşar. Kalıcı olması gereken not odaya yazılır.
|
|
247
|
+
|
|
248
|
+
**Yetkiler ayrıdır.** Görmek `work-session.view-active`, yazmak `collaboration.message` ister. Koordinasyonu izleyip katılmayan bir rol meşru bir yapılandırmadır; yazma izni yoksa uç boş küme değil doğrudan yetki hatası döner ve hangi iznin gerektiğini söyler.
|
|
249
|
+
|
|
250
|
+
**Kapanmış oturuma yazılmaz.** `WORK_SESSION_ENDED` (409) tekrar denenecek bir hata değildir: kişi çalışmayı bırakmıştır. Ya kaydı oku ya kendi oturumunu aç.
|
|
251
|
+
|
|
203
252
|
### Talep aç (open a request) — `create_request` → `POST /api/requests`
|
|
204
253
|
|
|
205
254
|
1. Need `title` (min 5 chars) — if missing or too short, ask once. Never invent it.
|
|
@@ -209,7 +258,7 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
|
|
|
209
258
|
|
|
210
259
|
### Üzerimdeki işler / talepleri listele (what's on my plate) — `list_requests` → `GET /api/requests`
|
|
211
260
|
|
|
212
|
-
- "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/
|
|
261
|
+
- "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). **`overdue` ile `deadlineWithinDays` birlikte kullanılmaz** — ikisi aynı arka uç parametresine yazar, birlikte verilirse `overdue` kazanır ve diğeri sessizce yok sayılır (zaten ters soruları sorarlar: geçmiş vs. yaklaşan). 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ı: `data.items[]` + `data.pagination{total, limit, offset, page, totalPages}` — dizinin anahtarı `items`'tır ve ikisi de `data` altındadır (canlı ölçüldü 2026-09-05). Summarize as a short list (numara + başlık + durum), not raw rows.
|
|
213
262
|
|
|
214
263
|
### Talep detayı (show a request) — `get_request` → `GET /api/requests/:requestNumber`
|
|
215
264
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "systa-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
package/server.js
CHANGED
|
@@ -44,7 +44,7 @@ const INSTRUCTIONS = [
|
|
|
44
44
|
'cagri yine de sunucu tarafinda 403 doner. Bir isi yapmadan ONCE:',
|
|
45
45
|
' 1) `list_capabilities` cagir — anahtarin erisebildigi modul/endpoint katalogunu',
|
|
46
46
|
' (scope-filtreli) gorursun. GENIS/WILDCARD anahtarda ONCE `list_capabilities({summary:true})`',
|
|
47
|
-
' cagir (oryantasyon + modul indeksi,
|
|
47
|
+
' cagir (oryantasyon + modul indeksi, ~57 KB; compact katalog ~483 KB, projeksiyonsuz cagri megabaytlara cikar), sonra',
|
|
48
48
|
' ilgili modul icin `list_capabilities({module:"<ad>"})` ile derinles. Yetkisiz endpoint listede GORUNMEZ. Yanit AYRICA SysTa',
|
|
49
49
|
' platform OZETI (overview), TR GLOSSARY, her modulun ACIKLAMASINI (description/concepts)',
|
|
50
50
|
' ve SENIN kullanicini (user) icerir — "ben / bana ata / uzerimdeki" niyetinde assignedTo',
|
|
@@ -199,6 +199,19 @@ const logErr = (message) => process.stderr.write(`[${SERVER_NAME}] ${message}\n`
|
|
|
199
199
|
* @param {{query?:Object, body?:*}} [opts]
|
|
200
200
|
* @returns {Promise<{status:number, ok:boolean, data:*}>}
|
|
201
201
|
*/
|
|
202
|
+
/**
|
|
203
|
+
* Nested grup atfi (`{id, name}`): API'nin dondurdugu grubu aynen gecirir,
|
|
204
|
+
* yoksa `null`. MCP araclari ayni kavrami TEK sekilde ilan eder (Faz 33):
|
|
205
|
+
* `list_users` API satirini oldugu gibi verir, `resolve_user_by_name` de bu
|
|
206
|
+
* yardimci ile ayni grubu verir.
|
|
207
|
+
*/
|
|
208
|
+
function kullaniciGrubu(grup) {
|
|
209
|
+
if (!grup || typeof grup !== 'object') {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
return { id: grup.id ?? null, name: grup.name ?? null };
|
|
213
|
+
}
|
|
214
|
+
|
|
202
215
|
const systaFetch = async (method, path, opts = {}) => {
|
|
203
216
|
if (!BASE_URL) {
|
|
204
217
|
throw new Error('SYSTA_API_BASE_URL is not configured');
|
|
@@ -383,6 +396,125 @@ const systaFetchBinary = async (apiPath) => {
|
|
|
383
396
|
};
|
|
384
397
|
};
|
|
385
398
|
|
|
399
|
+
/**
|
|
400
|
+
* SSE akisini SINIRLI bir sure dinler ve toplanan olaylari dondurur.
|
|
401
|
+
*
|
|
402
|
+
* MCP araclari istek/yanit calisir; sonsuz bir akis dondurulemez. Bu yuzden
|
|
403
|
+
* dinleme "su kadar saniye dinle, ne geldiyse ver" seklindedir. Agent bir isi
|
|
404
|
+
* beklerken bunu cagirir, dondugunde ya olaylari alir ya da bos donerek "bu
|
|
405
|
+
* surede bir sey olmadi" bilgisini alir.
|
|
406
|
+
*
|
|
407
|
+
* SIFIR BAGIMLILIK: Node'un yerlesik fetch'i yanit govdesini akitir, EventSource
|
|
408
|
+
* kutuphanesi gerekmez. Zaten SSE'nin socket yerine secilme sebebi de buydu.
|
|
409
|
+
*
|
|
410
|
+
* Kalp atisi satirlari (`:` ile baslayan yorumlar) olay DEGILDIR ve atlanir;
|
|
411
|
+
* baglantinin canli oldugunu gosterirler.
|
|
412
|
+
*/
|
|
413
|
+
/**
|
|
414
|
+
* Tek bir SSE kaydini (bos satirla ayrilmis blok) olaya cevirir.
|
|
415
|
+
*
|
|
416
|
+
* Kalp atisi satirlari (`:` ile baslayan yorumlar) olay DEGILDIR ve atlanir;
|
|
417
|
+
* baglantinin canli oldugunu gosterirler. `data:` satiri yoksa kayit bir olay
|
|
418
|
+
* tasimiyordur ve `null` doner.
|
|
419
|
+
*/
|
|
420
|
+
const parseSseRecord = (raw) => {
|
|
421
|
+
let eventName = 'message';
|
|
422
|
+
const dataLines = [];
|
|
423
|
+
for (const line of raw.split('\n')) {
|
|
424
|
+
if (line.startsWith(':')) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (line.startsWith('event:')) {
|
|
428
|
+
eventName = line.slice(6).trim();
|
|
429
|
+
} else if (line.startsWith('data:')) {
|
|
430
|
+
dataLines.push(line.slice(5).trim());
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (dataLines.length === 0) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const text = dataLines.join('\n');
|
|
437
|
+
let payload;
|
|
438
|
+
try {
|
|
439
|
+
payload = JSON.parse(text);
|
|
440
|
+
} catch {
|
|
441
|
+
payload = text;
|
|
442
|
+
}
|
|
443
|
+
return { event: eventName, data: payload };
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* SSE akisini SINIRLI bir sure dinler ve toplanan olaylari dondurur.
|
|
448
|
+
*
|
|
449
|
+
* MCP araclari istek/yanit calisir; sonsuz bir akis dondurulemez. Bu yuzden
|
|
450
|
+
* dinleme "su kadar saniye dinle, ne geldiyse ver" seklindedir. Agent bir isi
|
|
451
|
+
* beklerken bunu cagirir; dondugunde ya olaylari alir ya da bos donerek "bu
|
|
452
|
+
* surede bir sey olmadi" bilgisini alir.
|
|
453
|
+
*
|
|
454
|
+
* SIFIR BAGIMLILIK: Node'un yerlesik fetch'i yanit govdesini akitir, EventSource
|
|
455
|
+
* kutuphanesi gerekmez. Zaten SSE'nin soket yerine secilme sebebi de buydu.
|
|
456
|
+
*/
|
|
457
|
+
const systaStream = async (apiPath, { seconds = 30, maxEvents = 50 } = {}) => {
|
|
458
|
+
if (!BASE_URL) {
|
|
459
|
+
throw new Error('SYSTA_API_BASE_URL is not configured');
|
|
460
|
+
}
|
|
461
|
+
if (!API_KEY) {
|
|
462
|
+
throw new Error('SYSTA_API_KEY is not configured');
|
|
463
|
+
}
|
|
464
|
+
const headers = { Authorization: `Bearer ${API_KEY}`, Accept: 'text/event-stream' };
|
|
465
|
+
if (VENDOR_CODE) {
|
|
466
|
+
headers['X-Vendor-Code'] = VENDOR_CODE;
|
|
467
|
+
}
|
|
468
|
+
const controller = new AbortController();
|
|
469
|
+
const timer = setTimeout(() => controller.abort(), Math.max(1, seconds) * 1000);
|
|
470
|
+
const events = [];
|
|
471
|
+
try {
|
|
472
|
+
const response = await fetch(new URL(BASE_URL + apiPath), {
|
|
473
|
+
method: 'GET',
|
|
474
|
+
headers,
|
|
475
|
+
signal: controller.signal,
|
|
476
|
+
});
|
|
477
|
+
if (!response.ok) {
|
|
478
|
+
const text = await response.text();
|
|
479
|
+
let data;
|
|
480
|
+
try {
|
|
481
|
+
data = text ? JSON.parse(text) : null;
|
|
482
|
+
} catch {
|
|
483
|
+
data = text;
|
|
484
|
+
}
|
|
485
|
+
return { status: response.status, ok: false, data };
|
|
486
|
+
}
|
|
487
|
+
const decoder = new TextDecoder();
|
|
488
|
+
let buffer = '';
|
|
489
|
+
for await (const chunk of response.body) {
|
|
490
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
491
|
+
// SSE kayitlari BOS SATIRLA ayrilir; yarim kalan son parca tamponda kalir.
|
|
492
|
+
let index = buffer.indexOf('\n\n');
|
|
493
|
+
while (index !== -1) {
|
|
494
|
+
const event = parseSseRecord(buffer.slice(0, index));
|
|
495
|
+
buffer = buffer.slice(index + 2);
|
|
496
|
+
if (event !== null) {
|
|
497
|
+
events.push(event);
|
|
498
|
+
}
|
|
499
|
+
if (events.length >= maxEvents) {
|
|
500
|
+
controller.abort();
|
|
501
|
+
return { status: 200, ok: true, data: { events, stoppedBy: 'maxEvents' } };
|
|
502
|
+
}
|
|
503
|
+
index = buffer.indexOf('\n\n');
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return { status: 200, ok: true, data: { events, stoppedBy: 'streamEnded' } };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
// Sure dolunca akisi BIZ kesiyoruz; bu bir hata degil, beklenen son.
|
|
509
|
+
if (error.name === 'AbortError') {
|
|
510
|
+
return { status: 200, ok: true, data: { events, stoppedBy: 'timeout' } };
|
|
511
|
+
}
|
|
512
|
+
throw error;
|
|
513
|
+
} finally {
|
|
514
|
+
clearTimeout(timer);
|
|
515
|
+
}
|
|
516
|
+
};
|
|
517
|
+
|
|
386
518
|
/** Multipart yukleme — native FormData/Blob (Node 18+), alan adi backend'le ayni: 'file'. */
|
|
387
519
|
const systaUploadMultipart = async (apiPath, method, filePaths) => {
|
|
388
520
|
if (!BASE_URL) {
|
|
@@ -434,6 +566,136 @@ const capabilityProjectionPath = (projection, filters = {}) => {
|
|
|
434
566
|
return `/api-keys/me/capabilities?${query.toString()}`;
|
|
435
567
|
};
|
|
436
568
|
|
|
569
|
+
/**
|
|
570
|
+
* ═══ NESTED YAZMA GOVDESI (karar 6.2 / Faz 21 / BF-92) ═══
|
|
571
|
+
*
|
|
572
|
+
* API sozlesmesi okumada da yazmada da nested'dir: `PUT /requests/:n` govdesi
|
|
573
|
+
* `{ status: { id: 9 } }` tasir, `{ statusId: 9 }` degil. Arac girdileri (AI
|
|
574
|
+
* ajaninin verdigi args) DUZ kalir — `statusId`, `companyId` ajan icin dogal
|
|
575
|
+
* anahtarlardir ve `prepare_create_request` bu adlarla id dondurur; donusum
|
|
576
|
+
* yalniz tel govdesine yazilirken burada yapilir. Duz kabul sunucuda
|
|
577
|
+
* KAPANACAKTIR; bu yardimci olmadan gonderilen govde o zaman 400 alir.
|
|
578
|
+
*
|
|
579
|
+
* Bu paket bagimsiz yayinlanir (npm systa-mcp), arka uc `fieldGroupSpec`i
|
|
580
|
+
* import edemez; harita KOPYADIR ve `tests/mcp-server/server.test.js`
|
|
581
|
+
* spec-sync testiyle arka uca kilitlidir.
|
|
582
|
+
*/
|
|
583
|
+
// Yalniz MCP'nin yazdigi iki varligin (talep, proje) gruplari; kullanici/takim
|
|
584
|
+
// araclari eklenirse harita ve YAZMA_GRUPLARI birlikte buyur (spec-sync testi).
|
|
585
|
+
const YAZMA_YOLLARI = Object.freeze({
|
|
586
|
+
statusId: ['status', 'id'],
|
|
587
|
+
// Onay kaydinin gidecegi/geldigi durum atiflari (Faz 29 okuma sozlesmesi);
|
|
588
|
+
// FE `nestedWriteBody` ile ayni yollar.
|
|
589
|
+
pendingStatusId: ['pendingStatus', 'id'],
|
|
590
|
+
previousStatusId: ['previousStatus', 'id'],
|
|
591
|
+
categoryId: ['category', 'id'],
|
|
592
|
+
priority: ['priority', 'key'],
|
|
593
|
+
companyId: ['company', 'id'],
|
|
594
|
+
departmentId: ['department', 'id'],
|
|
595
|
+
organizationId: ['organization', 'id'],
|
|
596
|
+
subOrganizationId: ['subOrganization', 'id'],
|
|
597
|
+
projectId: ['project', 'id'],
|
|
598
|
+
parentProjectId: ['parentProject', 'id'],
|
|
599
|
+
mainProjectId: ['mainProject', 'id'],
|
|
600
|
+
formId: ['form', 'id'],
|
|
601
|
+
formVersion: ['form', 'version'],
|
|
602
|
+
addToNextReleaseList: ['release', 'addToNext'],
|
|
603
|
+
targetReleaseListId: ['release', 'targetListId'],
|
|
604
|
+
// NOT: `slaId` yazma yolu YOK. Arka uc `sla` grubunun kimligi `definitionId`
|
|
605
|
+
// (`slaDefinitionId`); `slaId` yalnizca ES ANLAMLI eski addir. Harita
|
|
606
|
+
// `slaId -> sla.id` ilan ediyordu: hicbir arac bu argumani kabul etmiyordu
|
|
607
|
+
// (olu yol) ve kabul etseydi arka ucun tanimadigi bir grup uretecekti.
|
|
608
|
+
// Spec-sync kapisi bunu yakaliyordu (HEAD'de KIRMIZI, 2026-09-05).
|
|
609
|
+
// Faz 22 — surum, efor, SLA tanimi, onay
|
|
610
|
+
requestId: ['request', 'id'],
|
|
611
|
+
activityTypeId: ['activityType', 'id'],
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
/** Varlik basina nested yazilan gruplar — arka uc ENTITY_GROUPS ile ayni. */
|
|
615
|
+
const YAZMA_GRUPLARI = Object.freeze({
|
|
616
|
+
// SIRA ve UYELIK arka uctaki `ENTITY_GROUPS.request_entity` ile BIREBIR
|
|
617
|
+
// aynidir (spec-sync kapisi). `team` buradaydi ama talep yazma sozlesmesinde
|
|
618
|
+
// YOKTU: hicbir arac `teamId` argumani almiyordu ve alsaydi arka ucun
|
|
619
|
+
// tanimadigi bir grup uretecekti (olu yol, 2026-09-05). `subOrganization`
|
|
620
|
+
// (mig 980) ise sozlesmede VARDI, haritada YOKTU.
|
|
621
|
+
request: [
|
|
622
|
+
'status',
|
|
623
|
+
'category',
|
|
624
|
+
'priority',
|
|
625
|
+
'company',
|
|
626
|
+
'department',
|
|
627
|
+
'organization',
|
|
628
|
+
'subOrganization',
|
|
629
|
+
'project',
|
|
630
|
+
'parentProject',
|
|
631
|
+
'mainProject',
|
|
632
|
+
'form',
|
|
633
|
+
'release',
|
|
634
|
+
'sla',
|
|
635
|
+
],
|
|
636
|
+
project: [
|
|
637
|
+
'company',
|
|
638
|
+
'department',
|
|
639
|
+
'organization',
|
|
640
|
+
'subOrganization',
|
|
641
|
+
'parentProject',
|
|
642
|
+
'mainProject',
|
|
643
|
+
],
|
|
644
|
+
// Faz 22 — arac yazan aileler (okuma sozlesmesi nested olanlar)
|
|
645
|
+
release: ['company', 'category', 'department', 'organization', 'status', 'request'],
|
|
646
|
+
effort: ['category', 'project', 'request', 'activityType'],
|
|
647
|
+
slaDefinition: ['form', 'company', 'department', 'organization', 'subOrganization'],
|
|
648
|
+
approval: [
|
|
649
|
+
'request',
|
|
650
|
+
'status',
|
|
651
|
+
'category',
|
|
652
|
+
'priority',
|
|
653
|
+
'company',
|
|
654
|
+
'department',
|
|
655
|
+
'organization',
|
|
656
|
+
'subOrganization',
|
|
657
|
+
'project',
|
|
658
|
+
// Onay kaydinin bekleyen/onceki durumu (Faz 29): okuma sozlesmesinde grup,
|
|
659
|
+
// yazma haritasinda da ayni kapsam.
|
|
660
|
+
'pendingStatus',
|
|
661
|
+
'previousStatus',
|
|
662
|
+
],
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const COKLU_ALANLI_GRUPLAR = new Set(
|
|
666
|
+
Object.values(YAZMA_YOLLARI)
|
|
667
|
+
.map(([grup]) => grup)
|
|
668
|
+
.filter((grup, i, dizi) => dizi.indexOf(grup) !== i),
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Duz anahtarli govdeyi varligin kapsamindaki gruplar icin nested'a cevirir.
|
|
673
|
+
* `null` = baglantiyi kaldir (tek alanli grupta grup null), `undefined` girmez.
|
|
674
|
+
* Kapsam disi anahtarlar (oz alanlar, `assignedTo`, `title`) aynen gecer.
|
|
675
|
+
*/
|
|
676
|
+
const toNestedWriteBody = (varlik, duz) => {
|
|
677
|
+
const kapsam = YAZMA_GRUPLARI[varlik] || [];
|
|
678
|
+
const sonuc = {};
|
|
679
|
+
const gruplar = {};
|
|
680
|
+
for (const [anahtar, deger] of Object.entries(duz || {})) {
|
|
681
|
+
const yol = YAZMA_YOLLARI[anahtar];
|
|
682
|
+
if (!yol || !kapsam.includes(yol[0])) {
|
|
683
|
+
sonuc[anahtar] = deger;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (deger === undefined) {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const [grup, alan] = yol;
|
|
690
|
+
gruplar[grup] = { ...(gruplar[grup] || {}), [alan]: deger };
|
|
691
|
+
}
|
|
692
|
+
for (const [grup, nesne] of Object.entries(gruplar)) {
|
|
693
|
+
const hepsiNull = Object.values(nesne).every((v) => v === null);
|
|
694
|
+
sonuc[grup] = hepsiNull && !COKLU_ALANLI_GRUPLAR.has(grup) ? null : nesne;
|
|
695
|
+
}
|
|
696
|
+
return sonuc;
|
|
697
|
+
};
|
|
698
|
+
|
|
437
699
|
const RESPONSE_TRANSPORTS = new Set([
|
|
438
700
|
'json',
|
|
439
701
|
'binary',
|
|
@@ -757,8 +1019,9 @@ const shapeCompact = (caps) => ({
|
|
|
757
1019
|
/**
|
|
758
1020
|
* Index-only view: orientation (overview/glossary/user) + module names with
|
|
759
1021
|
* endpoint COUNTS, but NO per-endpoint detail. For a wildcard '*' key the full
|
|
760
|
-
* compact catalog is ~
|
|
761
|
-
* buffers; this summary is
|
|
1022
|
+
* compact catalog is ~483KB (69 modules / 1663 endpoint-methods, olculdu 2026-08-30)
|
|
1023
|
+
* which overflows agent buffers; this summary is ~57KB. Drill into one module with
|
|
1024
|
+
* module='x' (~54KB) or straight to describe_endpoint (~22KB).
|
|
762
1025
|
*/
|
|
763
1026
|
const shapeSummary = (caps) => ({
|
|
764
1027
|
sessionType: caps.sessionType,
|
|
@@ -872,7 +1135,11 @@ const resolveStatusIdByKeyForRequest = async (requestNumber, statusKey) => {
|
|
|
872
1135
|
() => null,
|
|
873
1136
|
);
|
|
874
1137
|
const reqData = reqRes?.data?.data?.request ?? reqRes?.data?.data ?? null;
|
|
875
|
-
|
|
1138
|
+
// Talep detayi `form: {id, key, name, version}` grubunu doner (Faz 3+);
|
|
1139
|
+
// duz `formId` artik YOK. Eski okuma her zaman null donuyor ve durumlar
|
|
1140
|
+
// form suzgeci OLMADAN cekiliyordu -> yanlis durum kumesi (BF-55).
|
|
1141
|
+
const formGrupId = reqData?.form?.id;
|
|
1142
|
+
const reqFormId = typeof formGrupId === 'number' ? formGrupId : null;
|
|
876
1143
|
const statusesRes = await systaFetch('GET', '/statuses', {
|
|
877
1144
|
query: reqFormId != null ? { formId: reqFormId } : undefined,
|
|
878
1145
|
});
|
|
@@ -1093,7 +1360,7 @@ const MODULE_GATE_NOTE =
|
|
|
1093
1360
|
const TOOLS = [
|
|
1094
1361
|
{
|
|
1095
1362
|
name: 'list_requests',
|
|
1096
|
-
description: `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:
|
|
1363
|
+
description: `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: data.items[] + data.pagination{total,limit,offset,page,totalPages} — the array key is 'items' and both live under 'data'. Measured live 2026-09-05 against GET /api/requests: data roots are exactly ['items','pagination']. RESPONSE SIZE: fields controls how much each row carries -- default 'summary' returns lightweight rows (~1KB each: id, title, status{id,name,color}, priority{key,name,color}, company{...}, department{...}, assignedTo, deadline, isOverdue, sla, commentCount, fileCount). NOTE: identity fields are NESTED GROUPS in responses (status.name, priority.key, company.id) — but QUERY/FILTER keys stay FLAT (statusId, companyId, statusName in Query Builder). 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).${MASKING_NOTE}${EMPTY_SCOPE_NOTE}`,
|
|
1097
1364
|
inputSchema: {
|
|
1098
1365
|
type: 'object',
|
|
1099
1366
|
additionalProperties: false,
|
|
@@ -1161,7 +1428,8 @@ const TOOLS = [
|
|
|
1161
1428
|
overdue: {
|
|
1162
1429
|
type: 'boolean',
|
|
1163
1430
|
description:
|
|
1164
|
-
'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly.'
|
|
1431
|
+
'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly. ' +
|
|
1432
|
+
'MUTUALLY EXCLUSIVE with deadlineWithinDays (same backend parameter pair); when both are set, overdue WINS.',
|
|
1165
1433
|
},
|
|
1166
1434
|
slaStatus: {
|
|
1167
1435
|
type: 'array',
|
|
@@ -1188,13 +1456,16 @@ const TOOLS = [
|
|
|
1188
1456
|
deadlineWithinDays: {
|
|
1189
1457
|
type: 'integer',
|
|
1190
1458
|
description:
|
|
1191
|
-
'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N).'
|
|
1459
|
+
'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N). ' +
|
|
1460
|
+
'MUTUALLY EXCLUSIVE with overdue: both map to the same backend parameter pair, so ' +
|
|
1461
|
+
'if overdue is also set this one is IGNORED. Pick one — they ask opposite questions ' +
|
|
1462
|
+
'(already late vs. due soon).',
|
|
1192
1463
|
},
|
|
1193
1464
|
fields: {
|
|
1194
1465
|
type: 'string',
|
|
1195
1466
|
enum: ['summary', 'detail', 'full'],
|
|
1196
1467
|
description:
|
|
1197
|
-
"Response projection profile: 'summary' (default, lightweight ~1KB/row: id/
|
|
1468
|
+
"Response projection profile: 'summary' (default, lightweight ~1KB/row: id/title/status{id,name,color}/priority{key,name,color}/company/department/assignedTo/deadline/isOverdue/sla/commentCount/fileCount), 'detail' (adds description/effort/all counts), 'full' (everything incl. customFields/stakeholders — LARGE). Identity fields are NESTED GROUPS; filter keys stay FLAT.",
|
|
1198
1469
|
},
|
|
1199
1470
|
},
|
|
1200
1471
|
},
|
|
@@ -1206,7 +1477,7 @@ const TOOLS = [
|
|
|
1206
1477
|
{
|
|
1207
1478
|
name: 'list_users',
|
|
1208
1479
|
description:
|
|
1209
|
-
'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,
|
|
1480
|
+
'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, company{id,code,name,type}, department{id,code,name}} per user — identity fields are NESTED GROUPS (company.name, NOT companyName). Filter keys stay FLAT (companyId, departmentId).',
|
|
1210
1481
|
inputSchema: {
|
|
1211
1482
|
type: 'object',
|
|
1212
1483
|
additionalProperties: false,
|
|
@@ -1240,7 +1511,7 @@ const TOOLS = [
|
|
|
1240
1511
|
{
|
|
1241
1512
|
name: 'resolve_user_by_name',
|
|
1242
1513
|
description:
|
|
1243
|
-
"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,
|
|
1514
|
+
"Resolve a person's name (isim -> userId) for assignments and filters. Returns {userId, fullName, company{id,name}, department{id,name}, ambiguity} (same nested shape as list_users) when a UNIQUE match is found; when MULTIPLE users match (e.g. 3 'Ahmet' users) returns {ambiguity:true, candidates:[{id, fullName, email, company{id,name}, department{id,name}}], 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).",
|
|
1244
1515
|
inputSchema: {
|
|
1245
1516
|
type: 'object',
|
|
1246
1517
|
additionalProperties: false,
|
|
@@ -1297,8 +1568,11 @@ const TOOLS = [
|
|
|
1297
1568
|
ambiguity: false,
|
|
1298
1569
|
userId: u.id,
|
|
1299
1570
|
fullName: u.fullName,
|
|
1300
|
-
|
|
1301
|
-
|
|
1571
|
+
// `list_users` ile AYNI sekil: API'nin nested gruplari aynen
|
|
1572
|
+
// gecirilir (Faz 33). Duz `companyName` ilan etmek ayni kavrami
|
|
1573
|
+
// iki aracta iki sekilde ogretiyordu.
|
|
1574
|
+
company: kullaniciGrubu(u.company),
|
|
1575
|
+
department: kullaniciGrubu(u.department),
|
|
1302
1576
|
},
|
|
1303
1577
|
},
|
|
1304
1578
|
};
|
|
@@ -1316,8 +1590,8 @@ const TOOLS = [
|
|
|
1316
1590
|
id: u.id,
|
|
1317
1591
|
fullName: u.fullName,
|
|
1318
1592
|
email: u.email || null,
|
|
1319
|
-
|
|
1320
|
-
|
|
1593
|
+
company: kullaniciGrubu(u.company),
|
|
1594
|
+
department: kullaniciGrubu(u.department),
|
|
1321
1595
|
})),
|
|
1322
1596
|
},
|
|
1323
1597
|
},
|
|
@@ -1362,7 +1636,11 @@ const TOOLS = [
|
|
|
1362
1636
|
},
|
|
1363
1637
|
},
|
|
1364
1638
|
handler: async (args) => {
|
|
1365
|
-
|
|
1639
|
+
// `updates` tek nesne (tum taleplere ayni degisiklik); nested gider.
|
|
1640
|
+
const body = {
|
|
1641
|
+
requestIds: args.requestIds,
|
|
1642
|
+
updates: toNestedWriteBody('request', args.updates),
|
|
1643
|
+
};
|
|
1366
1644
|
if (args.reason) {
|
|
1367
1645
|
body.reason = args.reason;
|
|
1368
1646
|
}
|
|
@@ -1600,7 +1878,7 @@ const TOOLS = [
|
|
|
1600
1878
|
},
|
|
1601
1879
|
{
|
|
1602
1880
|
name: 'create_request',
|
|
1603
|
-
description: `Create a SysTa request (talep). Requires the "request.create" scope. Returns the created request under data.request. PROACTIVE (recommended): call prepare_create_request(companyId) FIRST -- it returns the active form's required fields AND valid ids (statuses/categories/organizations) so you fill them up front and avoid the 400 roundtrip. The required set is DYNAMIC and PER-COMPANY: title/companyId/statusId are the baseline (+ at least one assignment: assignedTo OR assignedTeams), but the request form that applies to THIS companyId may require more system fields (e.g. categoryId, organizationId). This vendor may run several active request forms at once; statusId/categoryId are FORM-ISOLATED and an id from another company's form is REJECTED, so resolve them via prepare_create_request(companyId) rather than reusing ids. If you skip prepare_, on a missing field the API returns 400 VALIDATION_FAILED naming it via details[].fieldKey -- add it and retry. For fields beyond this tool, use systa_api_call POST /requests. ONEMLI (talep 166 #60): verdigin \`description\` talebin ILK YORUMU (commentNo 1) olarak materyalize edilir; ZENGIN icerik istiyorsan (tablo/kod blogu/bicimli metin) \`description\` yerine systa_api_call POST /requests ile \`descriptionJson\` alanina TipTap doc gonder (tablo dahil) -- bu da comment #1 olur. Yani tabloyu talep ACARKEN descriptionJson icinde ver; AYRICA ayri bir yorum EKLEME (yoksa cift yorum olusur). Icerigi yalnizca ayri yorumda istiyorsan description bos birak.${RULE_ENGINE_NOTE}`,
|
|
1881
|
+
description: `Create a SysTa request (talep). Requires the "request.create" scope. Returns the created request under data.request. PROACTIVE (recommended): call prepare_create_request(companyId) FIRST -- it returns the active form's required fields AND valid ids (statuses/categories/organizations) so you fill them up front and avoid the 400 roundtrip. The required set is DYNAMIC and PER-COMPANY: title/companyId/statusId are the baseline (+ at least one assignment: assignedTo OR assignedTeams), but the request form that applies to THIS companyId may require more system fields (e.g. categoryId, organizationId). This vendor may run several active request forms at once; statusId/categoryId are FORM-ISOLATED and an id from another company's form is REJECTED, so resolve them via prepare_create_request(companyId) rather than reusing ids. If you skip prepare_, on a missing field the API returns 400 VALIDATION_FAILED naming it via details[].fieldKey -- add it and retry. For fields beyond this tool, use systa_api_call POST /requests with a NESTED body ({ status:{id}, company:{id}, category:{id}, priority:{key} }) -- this tool converts its flat args for you; the raw endpoint does not. ONEMLI (talep 166 #60): verdigin \`description\` talebin ILK YORUMU (commentNo 1) olarak materyalize edilir; ZENGIN icerik istiyorsan (tablo/kod blogu/bicimli metin) \`description\` yerine systa_api_call POST /requests ile \`descriptionJson\` alanina TipTap doc gonder (tablo dahil) -- bu da comment #1 olur. Yani tabloyu talep ACARKEN descriptionJson icinde ver; AYRICA ayri bir yorum EKLEME (yoksa cift yorum olusur). Icerigi yalnizca ayri yorumda istiyorsan description bos birak.${RULE_ENGINE_NOTE}`,
|
|
1604
1882
|
inputSchema: {
|
|
1605
1883
|
type: 'object',
|
|
1606
1884
|
additionalProperties: false,
|
|
@@ -1634,6 +1912,11 @@ const TOOLS = [
|
|
|
1634
1912
|
description:
|
|
1635
1913
|
'Organization-unit id (optional; may be required by the active form; must belong to companyId)',
|
|
1636
1914
|
},
|
|
1915
|
+
subOrganizationId: {
|
|
1916
|
+
type: 'integer',
|
|
1917
|
+
description:
|
|
1918
|
+
'Sub-organization-unit id (optional; must be a DIRECT child of organizationId -- the org tree is single-level).',
|
|
1919
|
+
},
|
|
1637
1920
|
departmentId: { type: 'integer', description: 'Department id (optional)' },
|
|
1638
1921
|
projectId: { type: 'integer', description: 'Link to a project (optional)' },
|
|
1639
1922
|
priority: {
|
|
@@ -1693,7 +1976,7 @@ const TOOLS = [
|
|
|
1693
1976
|
body[key] = args[key];
|
|
1694
1977
|
}
|
|
1695
1978
|
}
|
|
1696
|
-
return systaFetch('POST', '/requests', { body });
|
|
1979
|
+
return systaFetch('POST', '/requests', { body: toNestedWriteBody('request', body) });
|
|
1697
1980
|
},
|
|
1698
1981
|
},
|
|
1699
1982
|
{
|
|
@@ -1712,6 +1995,12 @@ const TOOLS = [
|
|
|
1712
1995
|
},
|
|
1713
1996
|
handler: async (args) => {
|
|
1714
1997
|
const companyId = args.companyId;
|
|
1998
|
+
// Lookup satirlari KENDI adlarini tasir (`statusName` bir durum listesinde
|
|
1999
|
+
// durumun kendi adidir — oz-referans, DUZ kalir). Canli olculdu
|
|
2000
|
+
// (2026-09-01): /statuses `statusName`, /categories `categoryName`,
|
|
2001
|
+
// /organizations `unitName`, /departments `departmentName`. Nested
|
|
2002
|
+
// `x.status?.name` dallari OLU KODDU — bu liste atif degil varlik listesi;
|
|
2003
|
+
// shim karar 1.2 geregi kaldirildi (BF-57).
|
|
1715
2004
|
const nameOf = (x) =>
|
|
1716
2005
|
x.name ??
|
|
1717
2006
|
x.statusName ??
|
|
@@ -1900,6 +2189,364 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
1900
2189
|
});
|
|
1901
2190
|
},
|
|
1902
2191
|
},
|
|
2192
|
+
{
|
|
2193
|
+
name: 'start_work_session',
|
|
2194
|
+
description: `Claim a record before you touch it: opens YOUR work session on one request or one task card.
|
|
2195
|
+
|
|
2196
|
+
WHY THIS EXISTS. SysTa shows a live "working on it" indicator to every human and every agent looking at that record. Without a session your edits are invisible to the people around you, and two agents silently rewrite each other's work. Starting a session is the cheapest collision prevention SysTa has.
|
|
2197
|
+
|
|
2198
|
+
THE COORDINATION LOOP, in order:
|
|
2199
|
+
1. list_active_work_sessions -> is somebody already on this record?
|
|
2200
|
+
2. if yes -> send_work_session_message and agree who takes what
|
|
2201
|
+
3. start_work_session -> claim it, with a note saying WHERE you are
|
|
2202
|
+
4. work; call send_work_session_heartbeat while you work (see below)
|
|
2203
|
+
5. end_work_session -> release the claim when you stop
|
|
2204
|
+
|
|
2205
|
+
TARGET. Give EXACTLY ONE of requestId or cardId. Both or neither is refused with EXACTLY_ONE_TARGET. cardId is the card's GLOBAL id (card.id from get_request_board), not the per-board "#3" sequence.
|
|
2206
|
+
|
|
2207
|
+
THE NOTE IS THE POINT. description is free text (max 2000 chars) and it is what other agents actually read before deciding to wait or proceed. Write the file or the area, not the intention: "src/models/request/requestCrudModel.js, create path" beats "working on request stuff".
|
|
2208
|
+
|
|
2209
|
+
LIFETIME. A session stays live until you end it or it goes silent for 15 minutes. Silence closes it automatically, and the auto-close records the LAST SIGNAL time as the end, not the moment of closing — so an abandoned session does not inflate anybody's effort.
|
|
2210
|
+
|
|
2211
|
+
Requires "work-session.manage". Reading other people's sessions is a separate, weaker privilege ("work-session.view-active").`,
|
|
2212
|
+
inputSchema: {
|
|
2213
|
+
type: 'object',
|
|
2214
|
+
additionalProperties: false,
|
|
2215
|
+
properties: {
|
|
2216
|
+
requestId: {
|
|
2217
|
+
type: 'integer',
|
|
2218
|
+
description: 'Request number. Give this OR cardId, never both.',
|
|
2219
|
+
},
|
|
2220
|
+
cardId: {
|
|
2221
|
+
type: 'integer',
|
|
2222
|
+
description: 'Card GLOBAL id from get_request_board. Give this OR requestId, never both.',
|
|
2223
|
+
},
|
|
2224
|
+
description: {
|
|
2225
|
+
type: 'string',
|
|
2226
|
+
description:
|
|
2227
|
+
'What you are doing, ideally the file or area. Max 2000 characters. Other agents read this before deciding to wait.',
|
|
2228
|
+
},
|
|
2229
|
+
},
|
|
2230
|
+
},
|
|
2231
|
+
handler: (args) => {
|
|
2232
|
+
const body = {};
|
|
2233
|
+
if (args.requestId !== undefined) {
|
|
2234
|
+
body.requestId = args.requestId;
|
|
2235
|
+
}
|
|
2236
|
+
if (args.cardId !== undefined) {
|
|
2237
|
+
body.cardId = args.cardId;
|
|
2238
|
+
}
|
|
2239
|
+
if (args.description !== undefined) {
|
|
2240
|
+
body.description = args.description;
|
|
2241
|
+
}
|
|
2242
|
+
return systaFetch('POST', '/work-sessions', { body });
|
|
2243
|
+
},
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
name: 'send_work_session_heartbeat',
|
|
2247
|
+
description: `Tell SysTa you are still working. Refreshes your session's last-signal time.
|
|
2248
|
+
|
|
2249
|
+
WHEN TO CALL. A session with no signal for 15 minutes is closed automatically and your claim disappears while you are still editing. MCP calls are not a heartbeat by themselves, so on any stretch of work longer than about ten minutes — a long build, a test run, a big refactor — call this between steps. A cheap call now beats losing the claim and colliding later.
|
|
2250
|
+
|
|
2251
|
+
WHAT IT DOES NOT DO. It does not extend effort, change status or touch the record. It only says "still here".
|
|
2252
|
+
|
|
2253
|
+
sessionId comes from start_work_session or list_my_work_sessions. Ending an already-ended session is not retryable: if you get WORK_SESSION_ENDED (409) the claim is gone — start a new session instead of retrying the heartbeat.
|
|
2254
|
+
|
|
2255
|
+
Requires "work-session.manage".`,
|
|
2256
|
+
inputSchema: {
|
|
2257
|
+
type: 'object',
|
|
2258
|
+
additionalProperties: false,
|
|
2259
|
+
required: ['sessionId'],
|
|
2260
|
+
properties: {
|
|
2261
|
+
sessionId: { type: 'integer', description: 'Your open session id.' },
|
|
2262
|
+
},
|
|
2263
|
+
},
|
|
2264
|
+
handler: (args) =>
|
|
2265
|
+
systaFetch('POST', `/work-sessions/${encodeURIComponent(args.sessionId)}/heartbeat`),
|
|
2266
|
+
},
|
|
2267
|
+
{
|
|
2268
|
+
name: 'end_work_session',
|
|
2269
|
+
description: `Release your claim: closes YOUR work session on a record.
|
|
2270
|
+
|
|
2271
|
+
CALL THIS WHEN YOU STOP, not when the task is finished — pausing for an hour is also stopping. A live session means "I am in this file right now"; leaving it open while you are elsewhere makes the indicator lie and makes the next agent wait for nothing.
|
|
2272
|
+
|
|
2273
|
+
endedAt is optional and almost never needed: omit it and the server stamps the moment of the call. Pass it only to record a stop that already happened, as an ISO-8601 timestamp with offset.
|
|
2274
|
+
|
|
2275
|
+
Ending a session does NOT delete its conversation — read_messages(sessionId) still returns what was said. But a closed session accepts no new messages: send_work_session_message returns WORK_SESSION_ENDED (409).
|
|
2276
|
+
|
|
2277
|
+
Requires "work-session.manage".`,
|
|
2278
|
+
inputSchema: {
|
|
2279
|
+
type: 'object',
|
|
2280
|
+
additionalProperties: false,
|
|
2281
|
+
required: ['sessionId'],
|
|
2282
|
+
properties: {
|
|
2283
|
+
sessionId: { type: 'integer', description: 'Your open session id.' },
|
|
2284
|
+
endedAt: {
|
|
2285
|
+
type: 'string',
|
|
2286
|
+
description: 'Optional ISO-8601 timestamp with offset. Omit to stamp now.',
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2289
|
+
},
|
|
2290
|
+
handler: (args) => {
|
|
2291
|
+
const body = {};
|
|
2292
|
+
if (args.endedAt !== undefined) {
|
|
2293
|
+
body.endedAt = args.endedAt;
|
|
2294
|
+
}
|
|
2295
|
+
return systaFetch('POST', `/work-sessions/${encodeURIComponent(args.sessionId)}/end`, {
|
|
2296
|
+
body,
|
|
2297
|
+
});
|
|
2298
|
+
},
|
|
2299
|
+
},
|
|
2300
|
+
{
|
|
2301
|
+
name: 'update_work_session_note',
|
|
2302
|
+
description: `Rewrite the note on your own open session — the text other agents read before deciding whether to wait for you.
|
|
2303
|
+
|
|
2304
|
+
WHEN TO USE. You moved. You claimed the model layer, you are now in the controller; the stale note sends the next agent into the file you just entered. Updating the note is how a long session stays honest.
|
|
2305
|
+
|
|
2306
|
+
Pass description: null to clear the note entirely (the session stays open). An empty string is not the way to clear it.
|
|
2307
|
+
|
|
2308
|
+
Requires "work-session.manage".`,
|
|
2309
|
+
inputSchema: {
|
|
2310
|
+
type: 'object',
|
|
2311
|
+
additionalProperties: false,
|
|
2312
|
+
required: ['sessionId', 'description'],
|
|
2313
|
+
properties: {
|
|
2314
|
+
sessionId: { type: 'integer', description: 'Your open session id.' },
|
|
2315
|
+
description: {
|
|
2316
|
+
type: ['string', 'null'],
|
|
2317
|
+
description: 'New note, max 2000 characters. null clears it.',
|
|
2318
|
+
},
|
|
2319
|
+
},
|
|
2320
|
+
},
|
|
2321
|
+
handler: (args) =>
|
|
2322
|
+
systaFetch('PATCH', `/work-sessions/${encodeURIComponent(args.sessionId)}`, {
|
|
2323
|
+
body: { description: args.description },
|
|
2324
|
+
}),
|
|
2325
|
+
},
|
|
2326
|
+
{
|
|
2327
|
+
name: 'list_my_work_sessions',
|
|
2328
|
+
description: `Your OWN open sessions. Use it to find a sessionId you forgot, or to check you are not holding a claim you already walked away from.
|
|
2329
|
+
|
|
2330
|
+
Typical uses: you started a session, did a long piece of work, and need the id for send_work_session_heartbeat or end_work_session; or you are starting fresh and want to close whatever you left open earlier.
|
|
2331
|
+
|
|
2332
|
+
This is the "mine" view. For everybody else's live claims use list_active_work_sessions.
|
|
2333
|
+
|
|
2334
|
+
Requires "work-session.manage".`,
|
|
2335
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
2336
|
+
handler: () => systaFetch('GET', '/work-sessions/active'),
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: 'list_work_session_history',
|
|
2340
|
+
description: `Past sessions on one record, newest first — who worked on this before, when, and what note they left.
|
|
2341
|
+
|
|
2342
|
+
Give EXACTLY ONE of requestId or cardId, same rule as start_work_session.
|
|
2343
|
+
|
|
2344
|
+
WHEN TO USE. Before picking up an unfamiliar record: the last note often says where the previous person stopped. Also for effort questions ("how much sitting did this card take") — each row carries its start and end.
|
|
2345
|
+
|
|
2346
|
+
PAGING. Results run newest to oldest, so the cursor moves BACKWARD: pass beforeId = the id of the oldest row you have, and you get the ones before it. limit defaults to 20, max 100.
|
|
2347
|
+
|
|
2348
|
+
Requires "work-session.view-active".`,
|
|
2349
|
+
inputSchema: {
|
|
2350
|
+
type: 'object',
|
|
2351
|
+
additionalProperties: false,
|
|
2352
|
+
properties: {
|
|
2353
|
+
requestId: { type: 'integer', description: 'Request number. Give this OR cardId.' },
|
|
2354
|
+
cardId: { type: 'integer', description: 'Card GLOBAL id. Give this OR requestId.' },
|
|
2355
|
+
limit: { type: 'integer', description: 'Page size, default 20, max 100.' },
|
|
2356
|
+
beforeId: {
|
|
2357
|
+
type: 'integer',
|
|
2358
|
+
description: 'Oldest session id you already have; returns the ones before it.',
|
|
2359
|
+
},
|
|
2360
|
+
},
|
|
2361
|
+
},
|
|
2362
|
+
handler: (args) =>
|
|
2363
|
+
systaFetch('GET', '/work-sessions/history', {
|
|
2364
|
+
query: {
|
|
2365
|
+
requestId: args.requestId,
|
|
2366
|
+
cardId: args.cardId,
|
|
2367
|
+
limit: args.limit,
|
|
2368
|
+
beforeId: args.beforeId,
|
|
2369
|
+
},
|
|
2370
|
+
}),
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
name: 'list_active_work_sessions',
|
|
2374
|
+
description: `Who is working on what RIGHT NOW — everybody's live claims, not just yours. Call this BEFORE you start editing a request or a task card. It is step one of the coordination loop:
|
|
2375
|
+
|
|
2376
|
+
1. list_active_work_sessions -> is somebody already on this record?
|
|
2377
|
+
2. if yes -> send_work_session_message and agree who takes what
|
|
2378
|
+
3. start_work_session -> claim it, with a note saying WHERE you are
|
|
2379
|
+
4. send_work_session_heartbeat -> while you work, on anything longer than ~10 minutes
|
|
2380
|
+
5. end_work_session -> release the claim when you stop
|
|
2381
|
+
|
|
2382
|
+
Each item identifies the person, the target (request or card), when they started, their last signal, and the free-text note they left ("I am in src/models/foo.js"). That note is the most useful field for avoiding collisions; read it before deciding.
|
|
2383
|
+
|
|
2384
|
+
A session stays listed until it is stopped or goes silent for 15 minutes, so a listed session is a live claim, not history. For finished sessions on one record use list_work_session_history; for your own open claims use list_my_work_sessions.
|
|
2385
|
+
|
|
2386
|
+
FINDING SOMEBODY IS NOT A STOP SIGN. It means talk first. Two agents on the same request but different files is normal and fine — what is not fine is starting silently.
|
|
2387
|
+
|
|
2388
|
+
Requires "work-session.view-active". You only see sessions inside your access scope; sessions on records you cannot see are filtered out rather than refused.`,
|
|
2389
|
+
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
2390
|
+
handler: () => systaFetch('GET', '/work-sessions/active/all'),
|
|
2391
|
+
},
|
|
2392
|
+
{
|
|
2393
|
+
name: 'send_work_session_message',
|
|
2394
|
+
description: `Write to somebody's OPEN work session — the way two agents avoid stepping on each other.
|
|
2395
|
+
|
|
2396
|
+
WHEN TO USE. You called list_active_work_sessions (or get_request_board) and found someone already working on the record you were about to touch. Do not silently start beside them: say what you need. Typical lines that actually prevent conflicts:
|
|
2397
|
+
* "I am editing src/models/foo.js — leaving the controller to you"
|
|
2398
|
+
* "Holding db/migrations/1042 until you land yours"
|
|
2399
|
+
* "My API contract is {POST /api/x -> {id}} — you can build against it"
|
|
2400
|
+
|
|
2401
|
+
Only OPEN sessions accept messages. A session that has ended returns WORK_SESSION_ENDED (409); that is not a retryable error — the person stopped working, so read the record instead, leave a lasting note with send_room_message, or claim it yourself with start_work_session.
|
|
2402
|
+
|
|
2403
|
+
WAITING FOR THE ANSWER. A session message does not notify you back. If you cannot continue without the reply, wait inside listen_room on that record's room; if you can keep working, come back later with read_messages(sessionId, afterId).
|
|
2404
|
+
|
|
2405
|
+
Reading needs "work-session.view-active"; WRITING needs "collaboration.message". Seeing coordination and joining it are separate privileges.
|
|
2406
|
+
|
|
2407
|
+
Body is plain text, at most 2000 characters. Trailing and leading spaces are trimmed; an empty body is refused.`,
|
|
2408
|
+
inputSchema: {
|
|
2409
|
+
type: 'object',
|
|
2410
|
+
additionalProperties: false,
|
|
2411
|
+
required: ['sessionId', 'body'],
|
|
2412
|
+
properties: {
|
|
2413
|
+
sessionId: {
|
|
2414
|
+
type: 'integer',
|
|
2415
|
+
description: 'Open work session id — read it from list_active_work_sessions.',
|
|
2416
|
+
},
|
|
2417
|
+
body: { type: 'string', description: 'Plain text, 1..2000 characters.' },
|
|
2418
|
+
},
|
|
2419
|
+
},
|
|
2420
|
+
handler: (args) =>
|
|
2421
|
+
systaFetch('POST', `/work-sessions/${encodeURIComponent(args.sessionId)}/messages`, {
|
|
2422
|
+
body: { body: args.body },
|
|
2423
|
+
}),
|
|
2424
|
+
},
|
|
2425
|
+
{
|
|
2426
|
+
name: 'send_room_message',
|
|
2427
|
+
description: `Write to a ROOM: a project, a request or a task card. Unlike a session message this outlives any single work session — use it when the note should stay for whoever comes next, or when nobody is working right now.
|
|
2428
|
+
|
|
2429
|
+
THERE IS NO "JOIN". A room is not a channel you subscribe to; it is the record itself. If your access scope already lets you see that project, request or card, you can read and write its room immediately. Nothing to create, nothing to join, no membership call. Out-of-scope rooms are refused outright rather than returned empty, so a refusal means "not yours", not "not there".
|
|
2430
|
+
|
|
2431
|
+
WHICH ROOM MATCHES WHICH SCREEN. A human reading your message sees it on the record's own surface, so pick the room by where the conversation belongs:
|
|
2432
|
+
* roomType "request" + the request NUMBER -> the request's chat, visible on the request detail screen
|
|
2433
|
+
* roomType "card" + the card's GLOBAL id -> the task card's chat (card.id from get_request_board, NOT the per-board "#3" sequence)
|
|
2434
|
+
* roomType "project" + the project id -> the project-level chat, which is what the Gantt view and the k&k chart open when somebody clicks the project node
|
|
2435
|
+
|
|
2436
|
+
On the Gantt and the k&k chart a node can be a project, a request or a card. Send to the room of the thing the node represents: a message about one request does not belong in the project room just because the reader is looking at a chart.
|
|
2437
|
+
|
|
2438
|
+
ROOM OR SESSION. The message is about the record -> room. The message is about somebody's current sitting -> session. "The migration numbering restarts at 1042" belongs to the room; "I am in that file right now" belongs to the session.
|
|
2439
|
+
|
|
2440
|
+
Requires "collaboration.message". Reading is a weaker privilege than writing: a role may watch coordination without joining it.
|
|
2441
|
+
|
|
2442
|
+
Body is plain text, at most 2000 characters.`,
|
|
2443
|
+
inputSchema: {
|
|
2444
|
+
type: 'object',
|
|
2445
|
+
additionalProperties: false,
|
|
2446
|
+
required: ['roomType', 'roomId', 'body'],
|
|
2447
|
+
properties: {
|
|
2448
|
+
roomType: { type: 'string', enum: ['project', 'request', 'card'] },
|
|
2449
|
+
roomId: { type: 'integer', description: 'Project id, request number, or card GLOBAL id.' },
|
|
2450
|
+
body: { type: 'string', description: 'Plain text, 1..2000 characters.' },
|
|
2451
|
+
},
|
|
2452
|
+
},
|
|
2453
|
+
handler: (args) =>
|
|
2454
|
+
systaFetch(
|
|
2455
|
+
'POST',
|
|
2456
|
+
`/rooms/${encodeURIComponent(args.roomType)}/${encodeURIComponent(args.roomId)}/messages`,
|
|
2457
|
+
{ body: { body: args.body } },
|
|
2458
|
+
),
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
name: 'read_messages',
|
|
2462
|
+
description: `Read a conversation, or ask "what arrived since I last looked".
|
|
2463
|
+
|
|
2464
|
+
Give EITHER sessionId (one work session's conversation) OR roomType+roomId (a room's conversation). Giving both is refused rather than guessed. A session conversation ends with that session: starting work again tomorrow opens a NEW conversation, so yesterday's lines do not reappear. A room conversation is permanent and survives every session.
|
|
2465
|
+
|
|
2466
|
+
CURSOR. Pass afterId = the id of the last message you already read; you get only what came after. Do not use timestamps as a cursor — the API renders milliseconds while the column stores microseconds, so a timestamp cursor returns the same row twice (measured 2026-09-08). The response carries pagination.nextAfterId for the next call and pagination.hasMore.
|
|
2467
|
+
|
|
2468
|
+
HOW TO GET NEW MESSAGES — pick one, do not do both:
|
|
2469
|
+
* You are WAITING right now (you asked a question, you need the answer before you continue) -> listen_room. It blocks for a bounded time and returns what arrived. No polling loop, no wasted calls.
|
|
2470
|
+
* You are WORKING and will check back later (you just want to know what happened while you were busy) -> read_messages with afterId. One call per check-in, not a loop. Between check-ins do your actual work.
|
|
2471
|
+
|
|
2472
|
+
There is no push channel to an MCP client and no background subscription: nothing wakes you up on its own. Either you wait inside listen_room, or you come back and ask with afterId. A tight poll loop is the wrong answer to both.
|
|
2473
|
+
|
|
2474
|
+
FIRST CALL: read once with no cursor to get the recent history, remember pagination.nextAfterId, and use that from then on.`,
|
|
2475
|
+
inputSchema: {
|
|
2476
|
+
type: 'object',
|
|
2477
|
+
additionalProperties: false,
|
|
2478
|
+
properties: {
|
|
2479
|
+
sessionId: { type: 'integer', description: 'Work session id (session conversation).' },
|
|
2480
|
+
roomType: { type: 'string', enum: ['project', 'request', 'card'] },
|
|
2481
|
+
roomId: { type: 'integer', description: 'Project id, request number, or card GLOBAL id.' },
|
|
2482
|
+
afterId: {
|
|
2483
|
+
type: 'integer',
|
|
2484
|
+
description: 'Last message id you already read. Omit for the first page.',
|
|
2485
|
+
},
|
|
2486
|
+
limit: { type: 'integer', description: 'Page size, default 50, max 200.' },
|
|
2487
|
+
},
|
|
2488
|
+
},
|
|
2489
|
+
handler: (args) => {
|
|
2490
|
+
const query = { afterId: args.afterId, limit: args.limit };
|
|
2491
|
+
if (args.sessionId) {
|
|
2492
|
+
// Iki hedef birden verilirse hangisinin kastedildigi belirsizdir;
|
|
2493
|
+
// sessizce birini secmek yanlis konusmayi okutur.
|
|
2494
|
+
if (args.roomType || args.roomId) {
|
|
2495
|
+
throw new Error('Give either sessionId or roomType+roomId, not both');
|
|
2496
|
+
}
|
|
2497
|
+
return systaFetch('GET', `/work-sessions/${encodeURIComponent(args.sessionId)}/messages`, {
|
|
2498
|
+
query,
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
if (!args.roomType || !args.roomId) {
|
|
2502
|
+
throw new Error('Give either sessionId, or both roomType and roomId');
|
|
2503
|
+
}
|
|
2504
|
+
return systaFetch(
|
|
2505
|
+
'GET',
|
|
2506
|
+
`/rooms/${encodeURIComponent(args.roomType)}/${encodeURIComponent(args.roomId)}/messages`,
|
|
2507
|
+
{ query },
|
|
2508
|
+
);
|
|
2509
|
+
},
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
name: 'listen_room',
|
|
2513
|
+
description: `Wait for live activity in a room instead of polling. Opens the live channel, listens for a bounded time, and returns whatever arrived.
|
|
2514
|
+
|
|
2515
|
+
THIS IS A BOUNDED WAIT, NOT A SUBSCRIPTION. It returns after "seconds" (default 30, max 120), or earlier once maxEvents is reached. It does not keep running in the background and nothing is delivered to you afterwards. stoppedBy tells you why it returned: timeout | maxEvents | streamEnded. To keep waiting, call it again.
|
|
2516
|
+
|
|
2517
|
+
WHEN TO USE IT. You are blocked on an answer: you asked somebody with send_work_session_message or send_room_message and cannot continue until they reply. Or you are waiting for a colleague to release a file. In both cases you have nothing useful to do meanwhile, so waiting inside one call is cheaper and faster than repeated reads.
|
|
2518
|
+
|
|
2519
|
+
WHEN NOT TO USE IT. You have work to do. Then do the work and check in afterwards with read_messages(afterId). Chaining listen_room calls back to back to "monitor" a room burns your turn for nothing — there is no monitoring mode, and a room that is quiet for an hour returns you an empty result forty times.
|
|
2520
|
+
|
|
2521
|
+
NOTHING IS LOST EITHER WAY. Events carry the same message shape as read_messages, so there is nothing new to parse. If the stream drops mid-wait, resume with read_messages(afterId = last id you saw) and continue from exactly there. Heartbeats are transport-level keepalives and never appear as events, so an empty result genuinely means "nobody said anything".
|
|
2522
|
+
|
|
2523
|
+
Requires "work-session.view-active". You only reach rooms your access scope already allows.`,
|
|
2524
|
+
inputSchema: {
|
|
2525
|
+
type: 'object',
|
|
2526
|
+
additionalProperties: false,
|
|
2527
|
+
required: ['roomType', 'roomId'],
|
|
2528
|
+
properties: {
|
|
2529
|
+
roomType: { type: 'string', enum: ['project', 'request', 'card'] },
|
|
2530
|
+
roomId: { type: 'integer', description: 'Project id, request number, or card GLOBAL id.' },
|
|
2531
|
+
seconds: {
|
|
2532
|
+
type: 'integer',
|
|
2533
|
+
description: 'How long to wait before returning, default 30, max 120.',
|
|
2534
|
+
},
|
|
2535
|
+
maxEvents: {
|
|
2536
|
+
type: 'integer',
|
|
2537
|
+
description: 'Return early once this many events arrived, default 50.',
|
|
2538
|
+
},
|
|
2539
|
+
},
|
|
2540
|
+
},
|
|
2541
|
+
handler: (args) =>
|
|
2542
|
+
systaStream(
|
|
2543
|
+
`/rooms/${encodeURIComponent(args.roomType)}/${encodeURIComponent(args.roomId)}/stream`,
|
|
2544
|
+
{
|
|
2545
|
+
seconds: Math.min(Number(args.seconds) || 30, 120),
|
|
2546
|
+
maxEvents: Math.min(Number(args.maxEvents) || 50, 200),
|
|
2547
|
+
},
|
|
2548
|
+
),
|
|
2549
|
+
},
|
|
1903
2550
|
{
|
|
1904
2551
|
name: 'list_allowed_transitions',
|
|
1905
2552
|
description:
|
|
@@ -1954,11 +2601,15 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
1954
2601
|
`/requests/${encodeURIComponent(args.requestNumber)}/allowed-transitions`,
|
|
1955
2602
|
);
|
|
1956
2603
|
const allowed = allowedRes?.data?.data?.allowedTransitions || [];
|
|
2604
|
+
// Gecis satiri `toStatusKey` tasir (canli: `{fromStatusId, toStatusId,
|
|
2605
|
+
// toStatusName, toStatusKey, toStatusColor}`); `statusKey` HIC yoktu, bu
|
|
2606
|
+
// dal hicbir zaman eslesmiyor ve yavas cozumleme yoluna dusuyordu (BF-56).
|
|
1957
2607
|
const hit = allowed.find(
|
|
1958
|
-
(t) => String(t.
|
|
2608
|
+
(t) => String(t.toStatusKey || '').toLowerCase() === String(args.statusKey).toLowerCase(),
|
|
1959
2609
|
);
|
|
1960
2610
|
if (hit) {
|
|
1961
|
-
|
|
2611
|
+
// Gecis satirinin hedefi `toStatusId`dir; `id`/`statusId` yoktu (BF-56).
|
|
2612
|
+
statusId = hit.toStatusId ?? hit.id ?? hit.statusId;
|
|
1962
2613
|
}
|
|
1963
2614
|
if (statusId == null) {
|
|
1964
2615
|
// Yedek: TALEBIN KENDI formuna scope'lu statu listesi (global liste
|
|
@@ -1977,7 +2628,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
1977
2628
|
);
|
|
1978
2629
|
}
|
|
1979
2630
|
const res = await systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
1980
|
-
body: { statusId },
|
|
2631
|
+
body: toNestedWriteBody('request', { statusId }),
|
|
1981
2632
|
});
|
|
1982
2633
|
if (args.comment && String(args.comment).trim()) {
|
|
1983
2634
|
const doc = {
|
|
@@ -2044,7 +2695,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
2044
2695
|
body[k] = v;
|
|
2045
2696
|
}
|
|
2046
2697
|
}
|
|
2047
|
-
return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
2698
|
+
return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
2699
|
+
body: toNestedWriteBody('request', body),
|
|
2700
|
+
});
|
|
2048
2701
|
},
|
|
2049
2702
|
},
|
|
2050
2703
|
{
|
|
@@ -2072,7 +2725,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
2072
2725
|
if (Array.isArray(args.assignedTeams)) {
|
|
2073
2726
|
body.assignedTeams = args.assignedTeams;
|
|
2074
2727
|
}
|
|
2075
|
-
return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
2728
|
+
return systaFetch('PUT', `/requests/${encodeURIComponent(args.requestNumber)}`, {
|
|
2729
|
+
body: toNestedWriteBody('request', body),
|
|
2730
|
+
});
|
|
2076
2731
|
},
|
|
2077
2732
|
},
|
|
2078
2733
|
{
|
|
@@ -2315,7 +2970,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
2315
2970
|
if (args.requestNote) {
|
|
2316
2971
|
body.requestNote = args.requestNote;
|
|
2317
2972
|
}
|
|
2318
|
-
return systaFetch('POST', '/approvals/batch', { body });
|
|
2973
|
+
return systaFetch('POST', '/approvals/batch', { body: toNestedWriteBody('approval', body) });
|
|
2319
2974
|
},
|
|
2320
2975
|
},
|
|
2321
2976
|
{
|
|
@@ -2395,7 +3050,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
2395
3050
|
body[k] = v;
|
|
2396
3051
|
}
|
|
2397
3052
|
}
|
|
2398
|
-
return systaFetch('POST', '/projects', { body });
|
|
3053
|
+
return systaFetch('POST', '/projects', { body: toNestedWriteBody('project', body) });
|
|
2399
3054
|
},
|
|
2400
3055
|
},
|
|
2401
3056
|
{
|
|
@@ -2464,7 +3119,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
2464
3119
|
body[k] = v;
|
|
2465
3120
|
}
|
|
2466
3121
|
}
|
|
2467
|
-
return systaFetch('PUT', `/projects/${encodeURIComponent(args.id)}`, {
|
|
3122
|
+
return systaFetch('PUT', `/projects/${encodeURIComponent(args.id)}`, {
|
|
3123
|
+
body: toNestedWriteBody('project', body),
|
|
3124
|
+
});
|
|
2468
3125
|
},
|
|
2469
3126
|
},
|
|
2470
3127
|
{
|
|
@@ -3006,7 +3663,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3006
3663
|
body[k] = v;
|
|
3007
3664
|
}
|
|
3008
3665
|
}
|
|
3009
|
-
return systaFetch('POST', '/releases/lists', { body });
|
|
3666
|
+
return systaFetch('POST', '/releases/lists', { body: toNestedWriteBody('release', body) });
|
|
3010
3667
|
},
|
|
3011
3668
|
},
|
|
3012
3669
|
{
|
|
@@ -3047,7 +3704,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3047
3704
|
},
|
|
3048
3705
|
handler: (args) => {
|
|
3049
3706
|
if (Array.isArray(args.items) && args.items.length) {
|
|
3050
|
-
return systaFetch('POST', '/releases/items', {
|
|
3707
|
+
return systaFetch('POST', '/releases/items', {
|
|
3708
|
+
body: args.items.map((oge) => toNestedWriteBody('release', oge)),
|
|
3709
|
+
});
|
|
3051
3710
|
}
|
|
3052
3711
|
const body = {};
|
|
3053
3712
|
for (const [k, v] of Object.entries(args)) {
|
|
@@ -3055,7 +3714,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3055
3714
|
body[k] = v;
|
|
3056
3715
|
}
|
|
3057
3716
|
}
|
|
3058
|
-
return systaFetch('POST', '/releases/items', { body });
|
|
3717
|
+
return systaFetch('POST', '/releases/items', { body: toNestedWriteBody('release', body) });
|
|
3059
3718
|
},
|
|
3060
3719
|
},
|
|
3061
3720
|
{
|
|
@@ -3307,7 +3966,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3307
3966
|
{
|
|
3308
3967
|
name: 'systa_api_call',
|
|
3309
3968
|
description:
|
|
3310
|
-
'Generic SysTa REST API call. Use for any endpoint not covered by a specific tool (create/update/comment, etc.). The API key scope is enforced server-side: out-of-scope calls return 403. path is relative to the API base (e.g. "/requests/42/comments"). You may paste a path verbatim from list_capabilities/describe_endpoint even though those include the "/api" prefix — a leading "/api" is auto-stripped (the base URL already ends in /api), so "/api/requests" and "/requests" both work.',
|
|
3969
|
+
'Generic SysTa REST API call. Use for any endpoint not covered by a specific tool (create/update/comment, etc.). The API key scope is enforced server-side: out-of-scope calls return 403. path is relative to the API base (e.g. "/requests/42/comments"). WRITE BODIES ARE NESTED (POST/PUT/PATCH on entities): send identity references as groups exactly as describe_endpoint(fields) shows them -- { status: { id: 9 }, company: { id: 12 }, priority: { key: "high" }, project: null } -- NOT flat statusId/companyId; flat keys are a deprecated transitional form and will be rejected. Query/filter keys on GET stay FLAT (statusId, companyId). Named tools (create_request, update_request, change_request_status, create_project...) do this conversion for you. You may paste a path verbatim from list_capabilities/describe_endpoint even though those include the "/api" prefix — a leading "/api" is auto-stripped (the base URL already ends in /api), so "/api/requests" and "/requests" both work.',
|
|
3311
3970
|
inputSchema: {
|
|
3312
3971
|
type: 'object',
|
|
3313
3972
|
additionalProperties: false,
|
|
@@ -3460,7 +4119,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3460
4119
|
'shown), each with its HTTP methods, safety class (read_only/idempotent/mutating/' +
|
|
3461
4120
|
'destructive) and required permission. Also returns SysTa overview, TR glossary and your ' +
|
|
3462
4121
|
'user (who-am-i). Call this FIRST. For a broad key the full catalog can be large: pass ' +
|
|
3463
|
-
'summary=true to get only orientation + module names with endpoint counts (
|
|
4122
|
+
'summary=true to get only orientation + module names with endpoint counts (~57KB vs ~483KB compact), then ' +
|
|
3464
4123
|
`pass module="<name>" to drill into one module. No args = full compact catalog.${MODULE_GATE_NOTE}`,
|
|
3465
4124
|
inputSchema: {
|
|
3466
4125
|
type: 'object',
|
|
@@ -3659,7 +4318,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
3659
4318
|
body[k] = args[k];
|
|
3660
4319
|
}
|
|
3661
4320
|
}
|
|
3662
|
-
return systaFetch('POST', '/effort/entries', { body });
|
|
4321
|
+
return systaFetch('POST', '/effort/entries', { body: toNestedWriteBody('effort', body) });
|
|
3663
4322
|
},
|
|
3664
4323
|
},
|
|
3665
4324
|
{
|
|
@@ -4013,7 +4672,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
|
|
|
4013
4672
|
},
|
|
4014
4673
|
});
|
|
4015
4674
|
}
|
|
4016
|
-
return systaFetch('PATCH', `/effort/entries/${encodeURIComponent(args.entryId)}`, {
|
|
4675
|
+
return systaFetch('PATCH', `/effort/entries/${encodeURIComponent(args.entryId)}`, {
|
|
4676
|
+
body: toNestedWriteBody('effort', body),
|
|
4677
|
+
});
|
|
4017
4678
|
},
|
|
4018
4679
|
},
|
|
4019
4680
|
{
|
|
@@ -4774,4 +5435,8 @@ module.exports = {
|
|
|
4774
5435
|
sanitizeFileName,
|
|
4775
5436
|
mimeForFileName,
|
|
4776
5437
|
resolveSavePath,
|
|
5438
|
+
// Nested yazma govdesi (Faz 21 / BF-92) — spec-sync testi icin export edilir
|
|
5439
|
+
toNestedWriteBody,
|
|
5440
|
+
YAZMA_YOLLARI,
|
|
5441
|
+
YAZMA_GRUPLARI,
|
|
4777
5442
|
};
|