systa-mcp 1.0.0 → 1.0.7

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 (4) hide show
  1. package/README.md +41 -1
  2. package/SKILL.md +81 -2
  3. package/package.json +1 -1
  4. package/server.js +229 -31
package/README.md CHANGED
@@ -9,7 +9,47 @@ ile SysTa REST API'sini "araç" olarak kullanmasını sağlar.
9
9
  `Authorization: Bearer <key>` + `X-Vendor-Code` ile isteği proxy'ler; anahtarın kapsamı
10
10
  dışındaki çağrılar backend tarafından `403` ile reddedilir.
11
11
 
12
- ## Kurulum
12
+ ## Hızlı Başlangıç (AI İstemci)
13
+
14
+ Harici bir AI istemciyi (Claude Code, MCP uyumlu ajan vb.) SysTa'ya bağlamak için
15
+ aşağıdaki adımları izleyin.
16
+
17
+ ### 1. API Anahtarı Oluşturun
18
+
19
+ SysTa'da **Ayarlar > API Anahtarları** sayfasından scoped API key oluşturun.
20
+ Anahtar yalnızca oluşturma anında gösterilir — güvenli bir yerde saklayın.
21
+
22
+ ### 2. Claude Code'a MCP Sunucusunu Ekleyin
23
+
24
+ ```bash
25
+ claude mcp add systa \
26
+ -e SYSTA_API_KEY=sk_live_ANAHTARINIZ \
27
+ -e SYSTA_API_BASE_URL=https://systa.vizyoneks.com.tr/api \
28
+ -- npx -y systa-mcp
29
+ ```
30
+
31
+ ### 3. Skill'i Yükleyin
32
+
33
+ ```bash
34
+ npx -y systa-mcp --install-skill
35
+ ```
36
+
37
+ ### 4. Doğrulama
38
+
39
+ ```bash
40
+ # Kimlik kontrolü
41
+ curl -H "Authorization: Bearer sk_live_..." https://systa.vizyoneks.com.tr/api/auth/me
42
+
43
+ # Yetenek kataloğu
44
+ curl -H "Authorization: Bearer sk_live_..." https://systa.vizyoneks.com.tr/api/api-keys/me/capabilities
45
+ ```
46
+
47
+ Her iki endpoint de `200` dönüyorsa bağlantı hazırdır. `.env.example` dosyasını
48
+ referans olarak kullanabilirsiniz (`cp .env.example .env` ile kopyalayıp doldurun).
49
+
50
+ ---
51
+
52
+ ## Kurulum (Detaylı)
13
53
 
14
54
  Anahtarı SysTa'da **Ayarlar → API Anahtarları → Yeni Anahtar** ile üretin (scope'ları
15
55
  seçin; anahtar yalnızca o anda gösterilir). Anahtar **self-routing**'tir: vendor bilgisi
package/SKILL.md CHANGED
@@ -48,7 +48,7 @@ Before any SysTa action, confirm connectivity, auth and identity. Do this once p
48
48
  If there is no MCP server, fall back to the REST API with a scoped API key. The key is **never typed into chat** — read it from the environment / a local secret file:
49
49
 
50
50
  ```bash
51
- : "${SYSTA_BASE_URL:?set SYSTA_BASE_URL, e.g. https://<vendor>.systa.example.com}"
51
+ : "${SYSTA_API_BASE_URL:?set SYSTA_API_BASE_URL, e.g. https://systa.example.com/api}"
52
52
  : "${SYSTA_API_KEY:?export a scoped sk_live_... key (do not paste in chat)}"
53
53
  ```
54
54
 
@@ -56,7 +56,7 @@ Before any SysTa action, confirm connectivity, auth and identity. Do this once p
56
56
 
57
57
  ```bash
58
58
  curl -fsS -H "Authorization: Bearer $SYSTA_API_KEY" \
59
- "$SYSTA_BASE_URL/api/auth/me"
59
+ "$SYSTA_API_BASE_URL/auth/me"
60
60
  ```
61
61
 
62
62
  Capture: `userId`, `companyId` (default company), `vendorSchema`, and the permission/scope set. If this returns `401`/`403` or `API key not found`, stop and tell the user the key is missing/expired/over-scoped — do **not** guess credentials.
@@ -87,6 +87,48 @@ Short shared vocabulary. Use these words with the user; never expose raw REST pa
87
87
  | Sürüm | Release | Yazılım sürüm yönetimi: liste → item → onay → Go/No-Go → deployment workflow. |
88
88
  | Vendor şema | Vendor schema | Multi-tenant izolasyon; her vendor ayrı schema (`vendor_xxx`). Public referans alınmaz. |
89
89
 
90
+ ### Anahtar kelime → modül eşlemesi (keyword index)
91
+
92
+ Kullanıcının doğal-dil kelimesini doğru modüle hızlı eşle. Bu, MCP `list_capabilities`
93
+ yanıtındaki `glossary` alias'larının ayna kopyasıdır (MCP yokken de geçerli). Alias'lar
94
+ ASCII'dir — eşleştirmede aksanı yok say. Modülü bulduktan sonra `describe_module(modül)`
95
+ veya `describe_endpoint` ile gövdeyi netleştir.
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 | create_kanban_card / move_kanban_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 | create_leave_request |
109
+ | vendor, sirket, musteri | company | list_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 | get_form_schema |
114
+ | rapor, raporlama, disa aktar, export, excel, csv | report | preview_report / export_report |
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 |
127
+
128
+ > Bu tablo katalogdaki ~44 modülden kullanıcı-yüzeyli olanları kapsar. Listede olmayan
129
+ > bir kelime için `list_capabilities` çağır — anahtarının erişebildiği güncel modül/alias
130
+ > kümesini döndürür (kapsam-filtreli; yetkisiz modül görünmez).
131
+
90
132
  ## UX Rules
91
133
 
92
134
  1. **Be concise; summarize.** No raw IDs, no JSON dumps in chat. Report "Talep #1423 açıldı — 'Login hatası', sana atandı" / "Request #1423 created and assigned to you." Surface a stable handle the user recognizes (talep numarası, başlık), not internal uuids.
@@ -185,3 +227,40 @@ SysTa returns structured errors. Recover deterministically instead of giving up.
185
227
  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.
186
228
  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.
187
229
  8. **Multi-tenant safety.** Never carry an id across vendors. If a resolved id isn't found in the caller's vendor scope, treat it as not-found (step 3), not as a public-schema fallback.
230
+
231
+ ## DX / Keşif Notları (talep 166 — prod'da doğrulanmış)
232
+
233
+ Gerçek AI kullanımında tespit edilen sürtünme noktaları; her biri canlı doğrulandı.
234
+
235
+ ### Cevap boyutu / kompaktlık
236
+
237
+ - **Liste uçları ağırdır.** Geniş/wildcard anahtarda önce `list_capabilities({summary:true})` (oryantasyon, birkaç KB), sonra `list_capabilities({module:"<ad>"})` ile derinleş.
238
+ - **Yalnızca SAYI gerekiyorsa** tüm veriyi çekme: `list_*` araçlarını `limit=1` ile çağır, `pagination.total` oku.
239
+
240
+ ### /api ön-eki
241
+
242
+ - `systa_api_call` base'i zaten `/api` içerir. Katalog yolları `/api/...` gösterse de path `/requests` ya da `/api/requests` olabilir — baştaki tek `/api` otomatik strip edilir.
243
+
244
+ ### Analitik / rapor / agregasyon (ham veri çekip elle hesaplama)
245
+
246
+ - `querybuilder_execute_query` server-side groupBy/aggregations yapar (queryConfig: `fields`/`groupBy`/`aggregations:[{field,function}]`/`having`).
247
+ - HAZIR PIVOT: `reports/preview` (reportType=user/request/sla/organization/project/lifecycle) per-satır pivot/ortalama döner — örn. reportType=user → kullanıcı başına `avgResolutionHours`/`totalResolved`/`slaSuccessRate`.
248
+ - Proje metrikleri: `get_project_analytics` + `get_project_health`; plan: `get_plan_critical_path`.
249
+
250
+ ### Talep açma — `description` = ilk yorum (commentNo 1)
251
+
252
+ - Verdiğin `description` talebin **ilk yorumu** olarak materyalize edilir.
253
+ - **Tablo/zengin içerik** için: `systa_api_call POST /requests` ile `descriptionJson` alanına TipTap doc (tablo dahil) gönder — bu da comment #1 olur. **Ayrıca ayrı yorum EKLEME** (çift yorum olur). İçeriği yalnız ayrı yorumda istiyorsan `description` boş bırak.
254
+
255
+ ### Kanban
256
+
257
+ - Talep listesindeki `taskCount` ile kanban kart sayısı **farklı olabilir** (subtask/arşiv/parent kapsamı). Kesin liste/sayım için `get_request_board` (full board) otoritedir.
258
+ - **Tamamlanma zamanı:** `completedAt`/`completedBy` yalnız full board'da gelir; lightweight `cards/list` bunları İÇERMEZ (yalnız `isCompleted`). Süre/performans analizinde full board kullan.
259
+
260
+ ### Plan modları
261
+
262
+ - `POST/PUT /plan/modes` filtreleme **mod değil her BOARD içindeki** `filterCriteria` ile yapılır. Geçerli anahtarlar: `assignedTo`, `statusId`/`excludeStatusId`, `projectId` (id dizileri), `includeStakeholderRequests` (bool). describe opak gösterir; emin değilsen mevcut bir modu `GET /plan/modes` ile incele.
263
+
264
+ ### Proje alt-ağacı
265
+
266
+ - Kök projenin tüm alt projelerini kapsamak için önce hiyerarşiyi al (`get_project`/`list_projects`), kök + tüm alt-proje id'lerini `projectId` DİZİSİ olarak geç.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systa-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.7",
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
@@ -29,7 +29,7 @@ const path = require('node:path');
29
29
 
30
30
  const PROTOCOL_VERSION = '2024-11-05';
31
31
  const SERVER_NAME = 'systa-mcp';
32
- const SERVER_VERSION = '1.0.0';
32
+ const SERVER_VERSION = '1.0.3';
33
33
 
34
34
  // Server-seviye rehber — her oturumda AI ajanin context'ine girer (kisa tutulur).
35
35
  // Ajana SysTa'yi nasil kesfedip kullanacagini ogretir; detay lazy araclardadir.
@@ -41,9 +41,12 @@ const INSTRUCTIONS = [
41
41
  'kapsam disi araclar listede yer almaz; listede gorundugu halde kapsam disi kalan bir',
42
42
  'cagri yine de sunucu tarafinda 403 doner. Bir isi yapmadan ONCE:',
43
43
  ' 1) `list_capabilities` cagir — anahtarin erisebildigi modul/endpoint katalogunu',
44
- ' (scope-filtreli) gorursun. Yetkisiz endpoint listede GORUNMEZ. Yanit AYRICA SysTa',
45
- ' platform OZETI (overview), TR GLOSSARY ve her modulun ACIKLAMASINI (description/',
46
- ' concepts) icerir kullanicinin dogal dilini (talep/gorev/efor/pano/durum/atanan...)',
44
+ ' (scope-filtreli) gorursun. GENIS/WILDCARD anahtarda ONCE `list_capabilities({summary:true})`',
45
+ ' cagir (oryantasyon + modul indeksi, birkac KB; tam katalog yuzlerce KB olabilir), sonra',
46
+ ' ilgili modul icin `list_capabilities({module:"<ad>"})` ile derinles. Yetkisiz endpoint listede GORUNMEZ. Yanit AYRICA SysTa',
47
+ ' platform OZETI (overview), TR GLOSSARY, her modulun ACIKLAMASINI (description/concepts)',
48
+ ' ve SENIN kullanicini (user) icerir — "ben / bana ata / uzerimdeki" niyetinde assignedTo',
49
+ ' icin user.id BURADAN gelir. Kullanicinin dogal dilini (talep/gorev/efor/pano/durum/atanan...)',
47
50
  ' bunlarla dogru modul ve endpoint ile eslestir.',
48
51
  ' 2) `describe_module(module)` (opsiyonel) — bir modulun (orn. request/kanban/plan) amacini,',
49
52
  ' kavramlarini ve cagirabilecegin endpointleri topluca gorursun.',
@@ -70,7 +73,38 @@ const INSTRUCTIONS = [
70
73
  ' respond_to_approval/list_pending_approvals), proje (create_project/get_project/update_project)',
71
74
  ' ve dosya ekleri (download_request_file / upload_file_to) ozel araclardir — `systa_api_call` JSON-only oldugundan',
72
75
  ' binary tasiyamaz; diger her endpoint icin `systa_api_call` (method+path) kullan — path ve',
73
- ' govdeyi describe_endpoint ciktisindan kur.',
76
+ ' govdeyi describe_endpoint ciktisindan kur (path icin /api on-eki opsiyonel, otomatik normalize edilir).',
77
+ '',
78
+ 'RAPOR / SAYIM / DAGILIM / TOPLAMA niyetinde: list_requests`i elle sayfalayip client-side TOPLAMA YAPMA.',
79
+ ' Sunucu-tarafli rapor araclari VARDIR: querybuilder_ai_generate_query (dogal dil -> sorgu),',
80
+ ' querybuilder_execute_query (server-side groupBy/sayim/filtre), querybuilder_describe_fields,',
81
+ ' querybuilder_saved_query, querybuilder_schedule_report. Proje metrikleri icin get_project_analytics',
82
+ ' (burndown/velocity/cumulative-flow...) ve get_project_health (saglik/risk/sla). Plan icin',
83
+ ' get_plan_critical_path. Bunlar katalogda da var — once list_capabilities ile dogrula.',
84
+ ' HAZIR PIVOT: reports/preview (reportType=user|request|sla|organization|project|lifecycle)',
85
+ ' sunucu-tarafli pivot/ortalama dondurur — orn. reportType=user kullanici basina',
86
+ ' avgResolutionHours/totalResolved/slaSuccessRate verir; "ortalama cozum suresi / kim kaç /',
87
+ ' dağılım" gibi sorularda ham veri cekip elle hesaplama, bu pivotu kullan.',
88
+ ' Yalnizca SAYI gerekiyorsa (kac kayit) tum veriyi cekme: list_* araclarini limit=1 ile',
89
+ ' cagir ve donen pagination.total degerini oku.',
90
+ '',
91
+ 'KANBAN (gorev panosu): bir talebin taskCount`u (talep listesinde) ile kanban kart sayisi',
92
+ ' FARKLI olabilir — taskCount, subtask/arsiv/parent kartlari farkli kapsar. Kesin kart',
93
+ ' listesi/sayimi ve TAMAMLANMA bilgisi icin get_request_board (full /kanban) otoritedir:',
94
+ ' kartta completedAt + completedBy + isCompleted + completionPercentage gelir. Lightweight',
95
+ ' cards/list hafiftir ama completedAt/completedBy ICERMEZ (yalniz isCompleted) — tamamlanma',
96
+ ' suresi / performans analizi icin get_request_board kullan, her karti tek tek cekme.',
97
+ '',
98
+ 'PLAN modlari (POST/PUT /plan/modes): filtreleme MOD seviyesinde degil, her BOARD icindeki',
99
+ ' filterCriteria ile yapilir (mode.boards[].filterCriteria). describe_endpoint bunu opak',
100
+ ' (unknown/additionalProperties) gosterir; gecerli anahtarlar request filtre semasidir:',
101
+ ' assignedTo (user id dizisi), statusId / excludeStatusId (durum id dizisi), projectId',
102
+ ' (proje id dizisi), includeStakeholderRequests (bool). Emin degilsen mevcut bir modu',
103
+ ' GET /plan/modes ile inceleyip formati ornek al — bastan tahmin etme.',
104
+ '',
105
+ 'PROJE alt-agaci: bir kok projenin TUM alt projelerini kapsamak icin once hiyerarsiyi al',
106
+ ' (get_project / list_projects ile parent-child), sonra kok + tum alt-proje id`lerini',
107
+ ' projectId DIZISI olarak gec (tek kok id alt agaci otomatik kapsamaz).',
74
108
  ].join('\n');
75
109
 
76
110
  const BASE_URL = (process.env.SYSTA_API_BASE_URL || '').replace(/\/+$/, '');
@@ -79,6 +113,17 @@ const VENDOR_CODE = process.env.SYSTA_VENDOR_CODE || '';
79
113
 
80
114
  const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
81
115
 
116
+ // list_capabilities/describe_endpoint advertise route patterns WITH the "/api"
117
+ // prefix (e.g. "/api/requests"), but BASE_URL already ends in /api — so a path
118
+ // copied verbatim would double-prefix into /api/api -> ROUTE_NOT_FOUND. Strip a
119
+ // single leading "/api" segment so both "/api/requests" and "/requests" work.
120
+ const stripApiPrefix = (p) => {
121
+ if (typeof p !== 'string') {
122
+ return p;
123
+ }
124
+ return /^\/api(\/|$)/.test(p) ? p.slice(4) || '/' : p;
125
+ };
126
+
82
127
  // Diagnostics MUST go to stderr — stdout is the JSON-RPC channel.
83
128
  const logErr = (message) => process.stderr.write(`[${SERVER_NAME}] ${message}\n`);
84
129
 
@@ -334,12 +379,51 @@ const shapeCompact = (caps) => ({
334
379
  })),
335
380
  });
336
381
 
382
+ /**
383
+ * Index-only view: orientation (overview/glossary/user) + module names with
384
+ * endpoint COUNTS, but NO per-endpoint detail. For a wildcard '*' key the full
385
+ * compact catalog is ~250KB (26 modules / 698 endpoints) which overflows agent
386
+ * buffers; this summary is a few KB. Drill into one module with module='x'.
387
+ */
388
+ const shapeSummary = (caps) => ({
389
+ sessionType: caps.sessionType,
390
+ ...(caps.user ? { user: caps.user } : {}),
391
+ scopes: caps.scopes,
392
+ isSuperAdmin: caps.isSuperAdmin,
393
+ ...(caps.overview ? { overview: caps.overview } : {}),
394
+ ...(caps.glossary ? { glossary: caps.glossary } : {}),
395
+ moduleCount: caps.moduleCount,
396
+ endpointCount: caps.endpointCount,
397
+ hint: 'Summary view — endpoints omitted. Call list_capabilities({module:"<name>"}) or describe_module to see a module\'s endpoints.',
398
+ modules: (caps.modules || []).map((m) => ({
399
+ module: m.module,
400
+ ...(m.description ? { description: m.description } : {}),
401
+ endpointCount: (m.endpoints || []).length,
402
+ })),
403
+ });
404
+
405
+ const shapeOneModule = (caps, target) => {
406
+ const t = String(target).toLowerCase();
407
+ const full = shapeCompact(caps);
408
+ const mod = (full.modules || []).find((m) => String(m.module).toLowerCase() === t);
409
+ return {
410
+ ...full,
411
+ modules: mod ? [mod] : [],
412
+ ...(mod
413
+ ? {}
414
+ : {
415
+ note: `Module "${target}" not found or not in this key scope.`,
416
+ availableModules: (full.modules || []).map((m) => m.module),
417
+ }),
418
+ };
419
+ };
420
+
337
421
  // ── Tool definitions ─────────────────────────────────────────────────────────
338
422
  const TOOLS = [
339
423
  {
340
424
  name: 'list_requests',
341
425
  description:
342
- 'List/search SysTa requests (talepler) with pagination and rich filters: status, assignee, company, category, department, priority, free-text. "uzerimdeki isler / my open work" => assignedTo=<me> (resolve via list_capabilities user.id) + non-final statuses. Returns only requests the API key scope permits. For array/range filters beyond these, use systa_api_call GET /requests.',
426
+ 'List/search SysTa requests (talepler) with pagination and rich filters: status, assignee, company, category, department, priority, free-text. "uzerimdeki acik isler / my open work" => assignedTo=<me> (resolve via list_capabilities user.id) + openOnly:true (one call — excludes final/"Tamamlandi" statuses automatically). Multiple statuses: statusId can be an array [1,3,5]. Returns only requests the API key scope permits. For filters beyond these, use systa_api_call GET /requests (this endpoint rejects unknown params with a 400 naming the valid set).',
343
427
  inputSchema: {
344
428
  type: 'object',
345
429
  additionalProperties: false,
@@ -352,7 +436,16 @@ const TOOLS = [
352
436
  },
353
437
  offset: { type: 'integer', minimum: 0, description: 'Pagination offset' },
354
438
  search: { type: 'string', description: 'Free-text search' },
355
- statusId: { type: 'integer', description: 'Filter by status id' },
439
+ statusId: {
440
+ type: ['integer', 'array'],
441
+ items: { type: 'integer' },
442
+ description: 'Filter by a status id, or an array of status ids for multiple',
443
+ },
444
+ openOnly: {
445
+ type: 'boolean',
446
+ description:
447
+ 'true => only open (non-final) requests — auto-resolves to the non-final status ids (excludes "Tamamlandi/kapatildi")',
448
+ },
356
449
  assignedTo: {
357
450
  type: 'integer',
358
451
  description: 'Filter by assignee user id (use own id for "my work")',
@@ -363,31 +456,55 @@ const TOOLS = [
363
456
  priority: { type: 'string', description: 'Filter by priority key/level' },
364
457
  },
365
458
  },
366
- handler: (args) =>
367
- systaFetch('GET', '/requests', {
459
+ handler: async (args) => {
460
+ let statusId = args.statusId;
461
+ // openOnly: resolve the NON-final status ids and filter to them. GET /requests
462
+ // accepts statusId as an array (validator normalizes a comma list); it does NOT
463
+ // support excludeStatusIds, so we INCLUDE the open ones rather than exclude final.
464
+ if (args.openOnly) {
465
+ const st = await systaFetch('GET', '/statuses').catch(() => null);
466
+ const rows = st && st.ok ? (st.data && (st.data.data || st.data)) || [] : [];
467
+ const openIds = (Array.isArray(rows) ? rows : [])
468
+ .filter((s) => s && !s.isFinal)
469
+ .map((s) => s.id);
470
+ if (openIds.length > 0) {
471
+ statusId = openIds;
472
+ }
473
+ }
474
+ return systaFetch('GET', '/requests', {
368
475
  query: {
369
476
  limit: args.limit ?? 20,
370
477
  offset: args.offset,
371
478
  search: args.search,
372
- statusId: args.statusId,
479
+ statusId,
373
480
  assignedTo: args.assignedTo,
374
481
  companyId: args.companyId,
375
482
  categoryId: args.categoryId,
376
483
  departmentId: args.departmentId,
377
484
  priority: args.priority,
378
485
  },
379
- }),
486
+ });
487
+ },
380
488
  },
381
489
  {
382
490
  name: 'get_request',
383
- description: 'Get a single SysTa request (talep) by its numeric id, including its details.',
491
+ description:
492
+ 'Get a single SysTa request (talep) by its request number, including its details. Param name is "requestNumber" to match the other request tools (add_request_comment/change_request_status/assign_request/update_request); "id" is accepted as an alias.',
384
493
  inputSchema: {
385
494
  type: 'object',
386
495
  additionalProperties: false,
387
- required: ['id'],
388
- properties: { id: { type: 'integer', description: 'Request id' } },
496
+ properties: {
497
+ requestNumber: { type: 'integer', description: 'Request number (the id shown in lists)' },
498
+ id: { type: 'integer', description: 'Alias for requestNumber (back-compat)' },
499
+ },
500
+ },
501
+ handler: async (args) => {
502
+ const rn = args.requestNumber ?? args.id;
503
+ if (rn === undefined || rn === null) {
504
+ throw new Error('requestNumber (or id) is required');
505
+ }
506
+ return systaFetch('GET', `/requests/${encodeURIComponent(rn)}`);
389
507
  },
390
- handler: (args) => systaFetch('GET', `/requests/${encodeURIComponent(args.id)}`),
391
508
  },
392
509
  {
393
510
  name: 'list_projects',
@@ -411,7 +528,7 @@ const TOOLS = [
411
528
  {
412
529
  name: 'create_request',
413
530
  description:
414
- '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: title/companyId/statusId are the baseline (+assignedTo for assignment), but this vendor\'s ACTIVE REQUEST FORM may require more system fields (e.g. categoryId, organizationId). 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.',
531
+ '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: title/companyId/statusId are the baseline (+assignedTo for assignment), but this vendor\'s ACTIVE REQUEST FORM may require more system fields (e.g. categoryId, organizationId). 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.',
415
532
  inputSchema: {
416
533
  type: 'object',
417
534
  additionalProperties: false,
@@ -534,11 +651,17 @@ const TOOLS = [
534
651
  } catch {
535
652
  // non-fatal — baseline fallback below
536
653
  }
537
- if (requiredFields.length === 0) {
538
- requiredFields = ['title', 'companyId', 'statusId', 'assignedTo'].map((k) => ({
539
- fieldKey: k,
540
- isSystem: true,
541
- }));
654
+ // create_request ALSO requires API-layer invariants that are NOT part of
655
+ // the active form schema (title/companyId/statusId/assignedTo). The form
656
+ // only describes custom/system form fields, so always merge these in —
657
+ // otherwise the helper omits e.g. assignedTo and the first create_request
658
+ // still 400s on a missing field (the exact roundtrip this tool prevents).
659
+ const BASELINE_REQUIRED = ['title', 'companyId', 'statusId', 'assignedTo'];
660
+ const have = new Set(requiredFields.map((f) => f.fieldKey));
661
+ for (const k of BASELINE_REQUIRED) {
662
+ if (!have.has(k)) {
663
+ requiredFields.push({ fieldKey: k, isSystem: true });
664
+ }
542
665
  }
543
666
  const need = new Set(requiredFields.map((f) => f.fieldKey));
544
667
 
@@ -579,7 +702,7 @@ const TOOLS = [
579
702
  companyId,
580
703
  requiredFields,
581
704
  options,
582
- note: 'Fill create_request with these required fields; resolve each id from options[fieldKey]. statusId.isFinal=true => a tamamla/kapat status. A missing option list = out of this key scope; ask the user or use systa_api_call.',
705
+ note: 'Fill create_request with ALL these required fields; resolve each id from options[fieldKey]. assignedTo is the assignee user id — for "bana ata / assign to me" use your own user.id from list_capabilities (who-am-i). statusId.isFinal=true => a tamamla/kapat status. A missing option list = out of this key scope; ask the user or use systa_api_call.',
583
706
  },
584
707
  };
585
708
  },
@@ -1687,7 +1810,7 @@ const TOOLS = [
1687
1810
  {
1688
1811
  name: 'systa_api_call',
1689
1812
  description:
1690
- '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").',
1813
+ '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.',
1691
1814
  inputSchema: {
1692
1815
  type: 'object',
1693
1816
  additionalProperties: false,
@@ -1711,7 +1834,7 @@ const TOOLS = [
1711
1834
  if (typeof args.path !== 'string' || !args.path.startsWith('/')) {
1712
1835
  throw new Error('path must start with "/"');
1713
1836
  }
1714
- return systaFetch(method, args.path, { query: args.query, body: args.body });
1837
+ return systaFetch(method, stripApiPrefix(args.path), { query: args.query, body: args.body });
1715
1838
  },
1716
1839
  },
1717
1840
  {
@@ -1798,7 +1921,7 @@ const TOOLS = [
1798
1921
  if (filePaths.length < 1 || filePaths.length > 50) {
1799
1922
  throw new Error('filePaths must contain between 1 and 50 local file paths');
1800
1923
  }
1801
- return systaUploadMultipart(args.path, method, filePaths);
1924
+ return systaUploadMultipart(stripApiPrefix(args.path), method, filePaths);
1802
1925
  },
1803
1926
  },
1804
1927
  {
@@ -1807,15 +1930,38 @@ const TOOLS = [
1807
1930
  'Discover what THIS API key can actually do. Returns a scope-filtered catalog of ' +
1808
1931
  'modules and endpoints the key is permitted to call (out-of-scope endpoints are not ' +
1809
1932
  'shown), each with its HTTP methods, safety class (read_only/idempotent/mutating/' +
1810
- 'destructive) and required permission. Call this FIRST to learn available capabilities, ' +
1811
- 'then describe_endpoint for parameter details. Compact view no field schemas.',
1812
- inputSchema: { type: 'object', additionalProperties: false, properties: {} },
1813
- handler: async () => {
1933
+ 'destructive) and required permission. Also returns SysTa overview, TR glossary and your ' +
1934
+ 'user (who-am-i). Call this FIRST. For a broad key the full catalog can be large: pass ' +
1935
+ 'summary=true to get only orientation + module names with endpoint counts (a few KB), then ' +
1936
+ 'pass module="<name>" to drill into one module. No args = full compact catalog.',
1937
+ inputSchema: {
1938
+ type: 'object',
1939
+ additionalProperties: false,
1940
+ properties: {
1941
+ summary: {
1942
+ type: 'boolean',
1943
+ description: 'true => orientation + module index only (no per-endpoint detail)',
1944
+ },
1945
+ module: {
1946
+ type: 'string',
1947
+ description:
1948
+ 'return only this module\'s endpoints (e.g. "request", "project", "query-builder")',
1949
+ },
1950
+ },
1951
+ },
1952
+ handler: async (args) => {
1814
1953
  const res = await systaFetch('GET', '/api-keys/me/capabilities');
1815
1954
  if (!res.ok) {
1816
1955
  return res;
1817
1956
  }
1818
- return shapeCompact(unwrapCapabilities(res));
1957
+ const caps = unwrapCapabilities(res);
1958
+ if (args && args.module) {
1959
+ return shapeOneModule(caps, args.module);
1960
+ }
1961
+ if (args && args.summary) {
1962
+ return shapeSummary(caps);
1963
+ }
1964
+ return shapeCompact(caps);
1819
1965
  },
1820
1966
  },
1821
1967
  {
@@ -2037,14 +2183,63 @@ const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
2037
2183
  const reply = (id, result) => ({ jsonrpc: '2.0', id, result });
2038
2184
  const errorReply = (id, code, message) => ({ jsonrpc: '2.0', id, error: { code, message } });
2039
2185
 
2186
+ /**
2187
+ * Validate tool-call arguments against the tool's declared inputSchema BEFORE
2188
+ * dispatch. The server advertises additionalProperties:false but did not enforce
2189
+ * it, so a hallucinated arg (e.g. isCompleted on list_requests) was silently
2190
+ * dropped and produced a confidently-wrong result. Reject unknown args + missing
2191
+ * required fields with a clear, actionable message so the agent self-corrects.
2192
+ * @returns {string|null} error message, or null if valid
2193
+ */
2194
+ const validateArgs = (tool, args) => {
2195
+ const schema = tool && tool.inputSchema;
2196
+ if (!schema || schema.type !== 'object') {
2197
+ return null;
2198
+ }
2199
+ const props = schema.properties || {};
2200
+ const a = args && typeof args === 'object' ? args : {};
2201
+ if (schema.additionalProperties === false) {
2202
+ const unknown = Object.keys(a).filter((k) => !(k in props));
2203
+ if (unknown.length > 0) {
2204
+ const valid = Object.keys(props).join(', ') || '(none)';
2205
+ return `Unknown argument(s): ${unknown.join(', ')}. This tool accepts only: ${valid}. For filters beyond these use systa_api_call (check describe_endpoint for the valid set).`;
2206
+ }
2207
+ }
2208
+ const required = Array.isArray(schema.required) ? schema.required : [];
2209
+ const missing = required.filter((k) => a[k] === undefined || a[k] === null);
2210
+ if (missing.length > 0) {
2211
+ return `Missing required argument(s): ${missing.join(', ')}.`;
2212
+ }
2213
+ return null;
2214
+ };
2215
+
2040
2216
  const handleToolCall = async (id, params) => {
2041
2217
  const name = params && params.name;
2042
2218
  const tool = TOOL_MAP.get(name);
2043
2219
  if (!tool) {
2044
2220
  return errorReply(id, -32602, `Unknown tool: ${name}`);
2045
2221
  }
2222
+ const args = (params && params.arguments) || {};
2223
+ // Enforce the declared inputSchema (additionalProperties:false + required) so
2224
+ // unknown/missing args surface a clear error instead of being silently dropped.
2225
+ const argError = validateArgs(tool, args);
2226
+ if (argError) {
2227
+ return reply(id, {
2228
+ content: [
2229
+ {
2230
+ type: 'text',
2231
+ text: JSON.stringify(
2232
+ { ok: false, error: 'INVALID_ARGUMENTS', message: argError },
2233
+ null,
2234
+ 2,
2235
+ ),
2236
+ },
2237
+ ],
2238
+ isError: true,
2239
+ });
2240
+ }
2046
2241
  try {
2047
- const result = await tool.handler((params && params.arguments) || {});
2242
+ const result = await tool.handler(args);
2048
2243
  const isError = result && result.ok === false;
2049
2244
  return reply(id, {
2050
2245
  content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
@@ -2211,6 +2406,7 @@ module.exports = {
2211
2406
  TOOL_MAP,
2212
2407
  TOOL_REQUIRED_PERMISSION,
2213
2408
  visibleTools,
2409
+ validateArgs,
2214
2410
  installSkill,
2215
2411
  runCli,
2216
2412
  systaFetch,
@@ -2218,6 +2414,8 @@ module.exports = {
2218
2414
  systaUploadMultipart,
2219
2415
  handleMessage,
2220
2416
  shapeCompact,
2417
+ shapeSummary,
2418
+ stripApiPrefix,
2221
2419
  unwrapCapabilities,
2222
2420
  parseContentDispositionFilename,
2223
2421
  sanitizeFileName,