systa-mcp 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +28 -4
  3. package/package.json +1 -1
  4. package/server.js +355 -57
package/README.md CHANGED
@@ -117,7 +117,7 @@ tam katalog `list_capabilities` ile keşfedilir.
117
117
  | `upload_file_to` | Yerel dosyaları multipart olarak ek diye yükler (talep/proje/görev kartı/orphan) — `file.upload` |
118
118
  | `list_capabilities` | **Bu anahtar ne yapabilir?** — scope-filtreli modül/endpoint kataloğu (yetkisiz endpoint görünmez); her metot için safety class + gereken izin. Ayrıca SysTa platform özeti (overview), TR glossary ve her modülün açıklamasını içerir |
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
- | `describe_endpoint` | Tek endpoint detayı: alan tipleri (fields), güvenlik sınıfı, açıklama — çağrı gövdesini kurmadan önce |
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
122
  `systa_api_call` ile, özel bir aracı olmayan herhangi bir endpoint çağrılabilir
123
123
  (örn. `POST /requests`, `POST /requests/42/comments`). Anahtarın scope'u dışındaki
package/SKILL.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 1.4.0
2
+ version: 1.5.1
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`. |
@@ -169,9 +169,33 @@ veya `describe_endpoint` ile gövdeyi netleştir.
169
169
  You don't memorize 600+ endpoints. Discover on demand.
170
170
 
171
171
  - **`list_capabilities`** — call when you need to find _which_ module/intent serves the user's verb, or when the user asks "ne yapabilirsin / what can you do". Returns scope-filtered modules (this key's reachable surface), each with a short summary, category, safety class and required permission. Use it to pick the right tool before acting; never claim a capability the catalog doesn't list for this key.
172
- - **`describe_endpoint`** — call when you've picked a tool and need its exact required fields, body shape, enums and defaults before submitting. Prefer this over guessing a body. It returns the field schema (`fieldKey`, type, required, enum, default) so you can pre-fill dynamic required fields and avoid `VALIDATION_FAILED`.
172
+ - **`describe_endpoint`** — call with both `path` and `method` when you've picked a tool and need its exact required fields, body shape, enums and defaults before submitting. Prefer this over guessing a body. It returns the field schema plus the verified HTTP Response Contract v2 (status, transport, content type, envelope and data/body schema); use returned IDs, handles and pagination fields for the next step instead of guessing them.
173
+ - **`describe_module`** — call after you've mapped the user's intent to a module. Returns the module's purpose, its key concepts, its endpoints, and **`entryPoints`**: the calls that need **no id up front**. A chain always starts at an entry point — calling an id-bearing endpoint first just returns 404/400.
173
174
  - **Rule of thumb:** ambiguous intent → `list_capabilities` first; chosen intent but unsure of body → `describe_endpoint`; clear intent + known body → act directly. For REST fallback, the equivalents are `GET /api/api-keys/me/capabilities` (preview of reachable endpoints) and the per-endpoint field schema returned by the MCP `describe_endpoint`.
174
175
 
176
+ ### Çağrı sırası (prerequisites)
177
+
178
+ A compact listing marks a method with `hasPrerequisites: true` when it needs an id you must obtain **first**. `describe_endpoint` returns the detail:
179
+
180
+ ```json
181
+ "prerequisites": [
182
+ {
183
+ "parameter": "requestId",
184
+ "collection": "/api/requests",
185
+ "producedBy": [
186
+ { "path": "/api/requests", "method": "GET", "field": "id", "evidence": "response-field" }
187
+ ]
188
+ }
189
+ ]
190
+ ```
191
+
192
+ - `source` is `"path"` when the id goes into the URL, `"body"` when it is a **required body field** (e.g. `POST /api/approvals` requires `requestId` in the body — it never appears in the path).
193
+ - `producedBy` lists the endpoints that **produce** that id; the first candidate carries the strongest evidence.
194
+ - `evidence: "response-field"` — the producer's response contract really contains `field`; chain it directly.
195
+ - `evidence: "collection-path"` — the link comes from the REST path structure (the producer's response schema is dynamic); call the producer and read the id off the returned record.
196
+
197
+ So the flow is: **user intent → `list_capabilities` (module) → `describe_module` (`entryPoints`) → `describe_endpoint` (fields + `prerequisites`) → walk the chain top-down → perform the action.** Never invent an id; always take it from a producer endpoint.
198
+
175
199
  ## Çekirdek iş akışları (core workflows)
176
200
 
177
201
  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.
@@ -185,7 +209,7 @@ Each maps a P0 user verb to a tool/endpoint, with the opinionated defaults from
185
209
 
186
210
  ### Üzerimdeki işler / talepleri listele (what's on my plate) — `list_requests` → `GET /api/requests`
187
211
 
188
- - "taleplerim / on me" → `assignedTo=<my userId>`. "açık/open" → `openOnly:true`. "geciken/overdue" → `overdue:true` (deadline geçmiş + non-final auto-composed) veya `slaBreached:true` (SLA ihlali). "SLA durumu" → `slaStatus:['breached','active']`. **"dosyalı talepler / eki olan" → `hasAttachments:true`** (top-level talep eki). Deadline'a göre → `deadlineFrom`/`deadlineTo` (YYYY-MM-DD) ya da `deadlineWithinDays:N` (önümüzdeki N gün). Search kapsamını genişlet: `searchIn:['assignee','creator','comments',...]`. **Response boyutu:** `fields` varsayılan `'summary'` (satır başına ~1KB); `'detail'` (description/effort/counts eklenir) veya `'full'` (customFields/stakeHolders dahil, çok büyük). Default `limit=20-50`, sort `created_at desc`. Yanıt zarfı: `{items, total, limit, offset}` — sayfa sayısı `= ceil(total/limit)`. Summarize as a short list (numara + başlık + durum), not raw rows.
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). **`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.requests[]` + `data.pagination{total, limit, offset, page, totalPages}` — dizinin anahtarı `requests`'tir (`items` DEĞİL) ve ikisi de `data` altındadır. Summarize as a short list (numara + başlık + durum), not raw rows.
189
213
 
190
214
  ### Talep detayı (show a request) — `get_request` → `GET /api/requests/:requestNumber`
191
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systa-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "MCP stdio server for SysTa (Talep Takip Sistemi) — lets AI agents (Claude, Codex) use the SysTa REST API via a scoped API key. Zero npm dependencies (native Node).",
5
5
  "type": "commonjs",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -44,16 +44,33 @@ 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, birkac KB; tam katalog yuzlerce KB olabilir), sonra',
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',
51
51
  ' icin user.id BURADAN gelir. Kullanicinin dogal dilini (talep/gorev/efor/pano/durum/atanan...)',
52
52
  ' bunlarla dogru modul ve endpoint ile eslestir.',
53
- ' 2) `describe_module(module)` (opsiyonel) bir modulun (orn. request/kanban/plan) amacini,',
54
- ' kavramlarini ve cagirabilecegin endpointleri topluca gorursun.',
55
- ' 3) `describe_endpoint(path, method)` cagir o endpoint için alan tiplerini (fields),',
56
- ' guvenlik sinifini ve aciklamayi al; sonra cagri govdesini buna gore kur.',
53
+ ' 2) `describe_module(module)` — modulun amacini, kavramlarini, cagirabilecegin endpointleri VE',
54
+ ' `entryPoints`i verir. `entryPoints` ONCEDEN BIR KIMLIK GEREKTIRMEYEN cagrilardir:',
55
+ ' zincir HER ZAMAN buradan baslar. Kimlik isteyen bir ucu once denemek 404/400 uretir.',
56
+ ' 3) `describe_endpoint(path, method)` cagir alan tiplerini (fields), guvenlik sinifini,',
57
+ ' tam HTTP Response Contract v2 bilgisini ve `prerequisites` alanini al.',
58
+ '',
59
+ 'CAGRI SIRASI (prerequisites) — ONEMLI:',
60
+ ' Kompakt listede bir metodun `hasPrerequisites: true` olmasi, o ucun ONCE baska bir cagriyla',
61
+ ' elde edilmesi gereken bir kimlige ihtiyac duydugunu soyler. Ayrintiyi `describe_endpoint`',
62
+ ' verir:',
63
+ ' prerequisites: [{ parameter, collection, producedBy: [{ path, method, field?, evidence }] }]',
64
+ ' - `parameter` : gereken kimlik (orn. requestId).',
65
+ " - `source` : `path` -> URL'de doldurulur. `body` -> ZORUNLU govde alanidir ve",
66
+ " path'te HIC gorunmez (orn. POST /api/approvals govdesinde requestId).",
67
+ ' - `producedBy` : o kimligi URETEN uclar. Ilk aday en guclu kanitlidir.',
68
+ ' - `evidence` : `response-field` -> uretici ucun yanit semasinda `field` gercekten var,',
69
+ ' dogrudan zincirle. `collection-path` -> baglanti REST yapisindan kurulmus;',
70
+ ' uretici ucu cagir ve donen kayittan kimligi oku.',
71
+ ' Yani akis sudur: kullanicinin niyeti -> `list_capabilities` ile MODUL -> `describe_module`',
72
+ ' ile `entryPoints` -> gereken ucta `describe_endpoint` -> `prerequisites` zincirini yukaridan',
73
+ ' asagi cagir -> asil islemi yap. Kimlik UYDURMA; her zaman uretici uctan al.',
57
74
  '',
58
75
  'GUVENLIK SINIFI (safetyClass): read_only (guvenli okuma) | idempotent (tekrarlanabilir)',
59
76
  ' | mutating (durum degistirir) | destructive (geri-alinamaz — dikkatli ol, gerekirse onayla).',
@@ -407,6 +424,292 @@ const systaUploadMultipart = async (apiPath, method, filePaths) => {
407
424
  const unwrapCapabilities = (res) =>
408
425
  res && res.data && res.data.data ? res.data.data : res && res.data;
409
426
 
427
+ const capabilityProjectionPath = (projection, filters = {}) => {
428
+ const query = new URLSearchParams({ projection });
429
+ for (const [key, value] of Object.entries(filters)) {
430
+ if (value !== undefined && value !== null && value !== '') {
431
+ query.set(key, String(value));
432
+ }
433
+ }
434
+ return `/api-keys/me/capabilities?${query.toString()}`;
435
+ };
436
+
437
+ const RESPONSE_TRANSPORTS = new Set([
438
+ 'json',
439
+ 'binary',
440
+ 'text',
441
+ 'sse',
442
+ 'redirect',
443
+ 'empty',
444
+ 'webhook_ack',
445
+ ]);
446
+ const RESPONSE_ENVELOPES = new Set([
447
+ 'systa_success_v1',
448
+ 'systa_error_v1',
449
+ 'raw_json',
450
+ 'legacy_error',
451
+ 'none',
452
+ ]);
453
+ const RESPONSE_SCHEMA_TYPES = new Set([
454
+ 'null',
455
+ 'boolean',
456
+ 'object',
457
+ 'array',
458
+ 'number',
459
+ 'integer',
460
+ 'string',
461
+ ]);
462
+ const RESPONSE_BINARY_DELIVERY_MODES = new Set(['buffer', 'stream']);
463
+ const isPlainObject = (value) =>
464
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value);
465
+
466
+ const validSchemaVariants = (schema) => {
467
+ const variants = schema.oneOf || schema.anyOf;
468
+ if (variants === undefined) {
469
+ return { valid: true, variants: null };
470
+ }
471
+ return {
472
+ valid:
473
+ Array.isArray(variants) &&
474
+ variants.length > 0 &&
475
+ (!schema.oneOf || typeof schema.branchReason === 'string') &&
476
+ variants.every(isValidResponseJsonSchema),
477
+ variants,
478
+ };
479
+ };
480
+
481
+ const validSchemaTypes = (schema) => {
482
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
483
+ return {
484
+ valid: types.length > 0 && types.every((type) => RESPONSE_SCHEMA_TYPES.has(type)),
485
+ types,
486
+ };
487
+ };
488
+
489
+ const validObjectResponseSchema = (schema) => {
490
+ const properties = schema.properties;
491
+ const hasProperties = isPlainObject(properties) && Object.keys(properties).length > 0;
492
+ const dynamic =
493
+ (schema.additionalProperties === true || isPlainObject(schema.additionalProperties)) &&
494
+ typeof schema.dynamicReason === 'string';
495
+ const requiredValid =
496
+ schema.required === undefined ||
497
+ (Array.isArray(schema.required) &&
498
+ schema.required.every((key) => typeof key === 'string' && key in properties));
499
+ const childrenValid =
500
+ !hasProperties || Object.values(properties).every(isValidResponseJsonSchema);
501
+ const additionalValid =
502
+ !isPlainObject(schema.additionalProperties) ||
503
+ isValidResponseJsonSchema(schema.additionalProperties);
504
+ return (hasProperties || dynamic) && requiredValid && childrenValid && additionalValid;
505
+ };
506
+
507
+ function isValidResponseJsonSchema(schema) {
508
+ if (!isPlainObject(schema)) {
509
+ return false;
510
+ }
511
+ const variantResult = validSchemaVariants(schema);
512
+ if (!variantResult.valid) {
513
+ return false;
514
+ }
515
+ if (schema.type === undefined) {
516
+ return Boolean(
517
+ variantResult.variants || schema.$ref !== undefined || schema.const !== undefined,
518
+ );
519
+ }
520
+ const typeResult = validSchemaTypes(schema);
521
+ if (!typeResult.valid) {
522
+ return false;
523
+ }
524
+ if (typeResult.types.includes('array') && !isValidResponseJsonSchema(schema.items)) {
525
+ return false;
526
+ }
527
+ return !typeResult.types.includes('object') || validObjectResponseSchema(schema);
528
+ }
529
+
530
+ const hasValidResponseBasics = (response) =>
531
+ isPlainObject(response) &&
532
+ Number.isInteger(response.status) &&
533
+ response.status >= 100 &&
534
+ response.status <= 599 &&
535
+ RESPONSE_TRANSPORTS.has(response.transport) &&
536
+ RESPONSE_ENVELOPES.has(response.envelope);
537
+
538
+ const hasValidResponseContentType = (response) => {
539
+ const bodyless = response.transport === 'redirect' || response.transport === 'empty';
540
+ return bodyless
541
+ ? response.contentType === null && response.envelope === 'none'
542
+ : typeof response.contentType === 'string' && Boolean(response.contentType.trim());
543
+ };
544
+
545
+ const hasValidDynamicContentType = (response) => {
546
+ const hasDynamicMetadata =
547
+ response.dynamicContentType !== undefined ||
548
+ response.contentTypePattern !== undefined ||
549
+ response.contentTypeSource !== undefined;
550
+ if (response.contentType !== '*/*') {
551
+ return !hasDynamicMetadata;
552
+ }
553
+ if (
554
+ response.transport !== 'binary' ||
555
+ response.dynamicContentType !== true ||
556
+ typeof response.contentTypePattern !== 'string' ||
557
+ !response.contentTypePattern.trim() ||
558
+ typeof response.contentTypeSource !== 'string' ||
559
+ !response.contentTypeSource.trim()
560
+ ) {
561
+ return false;
562
+ }
563
+ try {
564
+ new RegExp(response.contentTypePattern);
565
+ return true;
566
+ } catch {
567
+ return false;
568
+ }
569
+ };
570
+
571
+ const hasValidJsonResponseSchema = (response) => {
572
+ if (response.transport !== 'json' && response.transport !== 'webhook_ack') {
573
+ return true;
574
+ }
575
+ const schema =
576
+ response.envelope === 'systa_success_v1' ? response.dataSchema : response.bodySchema;
577
+ return isValidResponseJsonSchema(schema);
578
+ };
579
+
580
+ const hasValidBinaryMetadata = (response) => {
581
+ if (response.transport !== 'binary' || response.headers === undefined) {
582
+ return true;
583
+ }
584
+ return (
585
+ isPlainObject(response.headers) &&
586
+ (response.headers.contentDisposition === undefined ||
587
+ typeof response.headers.contentDisposition === 'string')
588
+ );
589
+ };
590
+
591
+ const hasValidBinaryDeliveryModes = (response) => {
592
+ if (response.deliveryModes === undefined) {
593
+ return true;
594
+ }
595
+ return (
596
+ response.transport === 'binary' &&
597
+ Array.isArray(response.deliveryModes) &&
598
+ response.deliveryModes.length > 0 &&
599
+ new Set(response.deliveryModes).size === response.deliveryModes.length &&
600
+ response.deliveryModes.every((mode) => RESPONSE_BINARY_DELIVERY_MODES.has(mode))
601
+ );
602
+ };
603
+
604
+ const hasValidRedirectMetadata = (response) =>
605
+ response.transport !== 'redirect' ||
606
+ (typeof response.locationSource === 'string' && Boolean(response.locationSource.trim()));
607
+
608
+ const hasValidSseEvents = (response) =>
609
+ response.transport !== 'sse' ||
610
+ (Array.isArray(response.events) &&
611
+ response.events.length > 0 &&
612
+ response.events.every(
613
+ (event) =>
614
+ isPlainObject(event) &&
615
+ typeof event.name === 'string' &&
616
+ Boolean(event.name.trim()) &&
617
+ isValidResponseJsonSchema(event.dataSchema),
618
+ ));
619
+
620
+ const isValidResponseVariant = (response) =>
621
+ hasValidResponseBasics(response) &&
622
+ hasValidResponseContentType(response) &&
623
+ hasValidDynamicContentType(response) &&
624
+ hasValidJsonResponseSchema(response) &&
625
+ hasValidBinaryMetadata(response) &&
626
+ hasValidBinaryDeliveryModes(response) &&
627
+ hasValidRedirectMetadata(response) &&
628
+ hasValidSseEvents(response);
629
+
630
+ const isResponseContractV2 = (contract) => {
631
+ if (
632
+ !isPlainObject(contract) ||
633
+ contract.contractVersion !== 2 ||
634
+ !Array.isArray(contract.responses) ||
635
+ contract.responses.length === 0 ||
636
+ !contract.responses.every(isValidResponseVariant)
637
+ ) {
638
+ return false;
639
+ }
640
+ if (contract.errors === undefined) {
641
+ return true;
642
+ }
643
+ if (!isPlainObject(contract.errors) || !RESPONSE_ENVELOPES.has(contract.errors.envelope)) {
644
+ return false;
645
+ }
646
+ return (
647
+ contract.errors.statuses === undefined ||
648
+ (Array.isArray(contract.errors.statuses) &&
649
+ contract.errors.statuses.every(
650
+ (status) => Number.isInteger(status) && status >= 400 && status <= 599,
651
+ ))
652
+ );
653
+ };
654
+
655
+ const findCapabilityMethod = (caps, targetPath, targetMethod) => {
656
+ for (const module of caps.modules || []) {
657
+ const endpoint = (module.endpoints || []).find((item) => item.path === targetPath);
658
+ const method =
659
+ endpoint && (endpoint.methods || []).find((item) => item.method === targetMethod);
660
+ if (endpoint && method) {
661
+ return { module, endpoint, method };
662
+ }
663
+ }
664
+ return null;
665
+ };
666
+
667
+ const responseContractProblem = (responseSchema, targetMethod, targetPath) => {
668
+ if (responseSchema && responseSchema.contractVersion !== 2) {
669
+ return `Unsupported response contract version for ${targetMethod} ${targetPath}`;
670
+ }
671
+ if (!isResponseContractV2(responseSchema)) {
672
+ return `Missing or invalid Response Contract v2 for ${targetMethod} ${targetPath}`;
673
+ }
674
+ return null;
675
+ };
676
+
677
+ const describeCapabilityEndpoint = async (args) => {
678
+ const targetPath = String(args.path || '');
679
+ const targetMethod = String(args.method || '').toUpperCase();
680
+ const res = await systaFetch(
681
+ 'GET',
682
+ capabilityProjectionPath('endpoint', { path: targetPath, method: targetMethod }),
683
+ );
684
+ if (!res.ok) {
685
+ return res;
686
+ }
687
+ const found = findCapabilityMethod(unwrapCapabilities(res), targetPath, targetMethod);
688
+ if (!found) {
689
+ return {
690
+ ok: false,
691
+ error: `Endpoint not found or not permitted for this key: ${targetMethod} ${targetPath}`,
692
+ };
693
+ }
694
+ const contractProblem = responseContractProblem(
695
+ found.method.responseSchema,
696
+ targetMethod,
697
+ targetPath,
698
+ );
699
+ if (contractProblem) {
700
+ return { ok: false, error: contractProblem };
701
+ }
702
+ const { responseSchema, ...methodDetail } = found.method;
703
+ return {
704
+ module: found.module.module,
705
+ path: found.endpoint.path,
706
+ summary: found.endpoint.summary,
707
+ ...methodDetail,
708
+ responseContract: responseSchema,
709
+ responseContractStatus: 'verified',
710
+ };
711
+ };
712
+
410
713
  /**
411
714
  * Compact view of the capability catalog — drops field_schema/detail/scenario so
412
715
  * the per-turn payload stays small (lazy: use describe_endpoint for full detail).
@@ -444,7 +747,8 @@ const shapeCompact = (caps) => ({
444
747
  method: mm.method,
445
748
  safetyClass: mm.safetyClass,
446
749
  requiredPermission: mm.requiredPermission,
447
- hasFields: Boolean(mm.fields),
750
+ hasFields: Boolean(mm.hasFields || mm.fields),
751
+ hasResponseContract: Boolean(mm.hasResponseContract || mm.responseSchema),
448
752
  })),
449
753
  })),
450
754
  })),
@@ -453,8 +757,9 @@ const shapeCompact = (caps) => ({
453
757
  /**
454
758
  * Index-only view: orientation (overview/glossary/user) + module names with
455
759
  * endpoint COUNTS, but NO per-endpoint detail. For a wildcard '*' key the full
456
- * compact catalog is ~250KB (26 modules / 698 endpoints) which overflows agent
457
- * buffers; this summary is a few KB. Drill into one module with module='x'.
760
+ * compact catalog is ~483KB (69 modules / 1663 endpoint-methods, olculdu 2026-08-30)
761
+ * which overflows agent buffers; this summary is ~57KB. Drill into one module with
762
+ * module='x' (~54KB) or straight to describe_endpoint (~22KB).
458
763
  */
459
764
  const shapeSummary = (caps) => ({
460
765
  sessionType: caps.sessionType,
@@ -468,11 +773,16 @@ const shapeSummary = (caps) => ({
468
773
  moduleCount: caps.moduleCount,
469
774
  endpointCount: caps.endpointCount,
470
775
  hint: 'Summary view — endpoints omitted. Call list_capabilities({module:"<name>"}) or describe_module to see a module\'s endpoints.',
471
- modules: (caps.modules || []).map((m) => ({
472
- module: m.module,
473
- ...(m.description ? { description: m.description } : {}),
474
- endpointCount: (m.endpoints || []).length,
475
- })),
776
+ modules: (caps.modules || []).map((m) => {
777
+ const paths = m.endpoints || [];
778
+ const methodCount = paths.reduce((sum, endpoint) => sum + (endpoint.methods || []).length, 0);
779
+ return {
780
+ module: m.module,
781
+ ...(m.description ? { description: m.description } : {}),
782
+ pathCount: m.pathCount ?? paths.length,
783
+ endpointCount: m.endpointCount ?? (methodCount || paths.length),
784
+ };
785
+ }),
476
786
  });
477
787
 
478
788
  const shapeOneModule = (caps, target) => {
@@ -784,7 +1094,7 @@ const MODULE_GATE_NOTE =
784
1094
  const TOOLS = [
785
1095
  {
786
1096
  name: 'list_requests',
787
- 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: {items:[], total, limit, offset} — totalPages = ceil(total/limit). RESPONSE SIZE: fields controls how much each row carries -- default 'summary' returns lightweight rows (~1KB each: id, requestNumber, title, statusName, priority, assignedTo, deadline, isOverdue, slaStatus, commentCount, fileCount). Use fields:'detail' when you need description/effort/counts, or fields:'full' for everything incl. customFields/stakeHolders (LARGE — S-A2 kanit: 44 talep tam moduyla ~300KB).${MASKING_NOTE}${EMPTY_SCOPE_NOTE}`,
1097
+ 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.requests[] + data.pagination{total,limit,offset,page,totalPages} — the array key is 'requests' (NOT 'items') and both live under 'data'. Verified against the endpoint Response Contract v2 (required: [requests, pagination]). RESPONSE SIZE: fields controls how much each row carries -- default 'summary' returns lightweight rows (~1KB each: id, requestNumber, title, statusName, priority, assignedTo, deadline, isOverdue, slaStatus, commentCount, fileCount). Use fields:'detail' when you need description/effort/counts, or fields:'full' for everything incl. customFields/stakeHolders (LARGE — S-A2 kanit: 44 talep tam moduyla ~300KB).${MASKING_NOTE}${EMPTY_SCOPE_NOTE}`,
788
1098
  inputSchema: {
789
1099
  type: 'object',
790
1100
  additionalProperties: false,
@@ -852,7 +1162,8 @@ const TOOLS = [
852
1162
  overdue: {
853
1163
  type: 'boolean',
854
1164
  description:
855
- 'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly.',
1165
+ 'true => geciken/overdue: deadline in the past AND not in a final status. Shorthand for deadlineDaysRemaining[operator]=lt&deadlineDaysRemaining[value]=0 combined with openOnly. ' +
1166
+ 'MUTUALLY EXCLUSIVE with deadlineWithinDays (same backend parameter pair); when both are set, overdue WINS.',
856
1167
  },
857
1168
  slaStatus: {
858
1169
  type: 'array',
@@ -879,7 +1190,10 @@ const TOOLS = [
879
1190
  deadlineWithinDays: {
880
1191
  type: 'integer',
881
1192
  description:
882
- 'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N).',
1193
+ 'Deadline within N days from now (i.e. deadlineDaysRemaining[operator]=lte,value=N). ' +
1194
+ 'MUTUALLY EXCLUSIVE with overdue: both map to the same backend parameter pair, so ' +
1195
+ 'if overdue is also set this one is IGNORED. Pick one — they ask opposite questions ' +
1196
+ '(already late vs. due soon).',
883
1197
  },
884
1198
  fields: {
885
1199
  type: 'string',
@@ -3151,7 +3465,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3151
3465
  'shown), each with its HTTP methods, safety class (read_only/idempotent/mutating/' +
3152
3466
  'destructive) and required permission. Also returns SysTa overview, TR glossary and your ' +
3153
3467
  'user (who-am-i). Call this FIRST. For a broad key the full catalog can be large: pass ' +
3154
- 'summary=true to get only orientation + module names with endpoint counts (a few KB), then ' +
3468
+ 'summary=true to get only orientation + module names with endpoint counts (~57KB vs ~483KB compact), then ' +
3155
3469
  `pass module="<name>" to drill into one module. No args = full compact catalog.${MODULE_GATE_NOTE}`,
3156
3470
  inputSchema: {
3157
3471
  type: 'object',
@@ -3169,7 +3483,13 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3169
3483
  },
3170
3484
  },
3171
3485
  handler: async (args) => {
3172
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
3486
+ let projectionPath = capabilityProjectionPath('compact');
3487
+ if (args && args.module) {
3488
+ projectionPath = capabilityProjectionPath('module', { module: args.module });
3489
+ } else if (args && args.summary) {
3490
+ projectionPath = capabilityProjectionPath('summary');
3491
+ }
3492
+ const res = await systaFetch('GET', projectionPath);
3173
3493
  if (!res.ok) {
3174
3494
  return res;
3175
3495
  }
@@ -3187,7 +3507,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3187
3507
  name: 'describe_module',
3188
3508
  description:
3189
3509
  'Orient on ONE SysTa module: returns its purpose (description, TR/EN), key concepts, ' +
3190
- 'and the endpoints THIS key can call within it (path + summary + method/safety). Use the ' +
3510
+ 'the endpoints THIS key can call within it (path + summary + method/safety), and entryPoints — the calls that need NO id up front, i.e. where a chain starts. Use the ' +
3191
3511
  'module name from list_capabilities (e.g. "request", "kanban", "plan", "sla"). Helps map a ' +
3192
3512
  "user's natural-language intent to the right module before describe_endpoint.",
3193
3513
  inputSchema: {
@@ -3199,7 +3519,10 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3199
3519
  },
3200
3520
  },
3201
3521
  handler: async (args) => {
3202
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
3522
+ const res = await systaFetch(
3523
+ 'GET',
3524
+ capabilityProjectionPath('module', { module: args.module }),
3525
+ );
3203
3526
  if (!res.ok) {
3204
3527
  return res;
3205
3528
  }
@@ -3217,6 +3540,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3217
3540
  module: mod.module,
3218
3541
  description: mod.description,
3219
3542
  concepts: mod.concepts,
3543
+ // Zincirin BASI: kimlik gerektirmeyen uclar. Kullanicinin niyeti bu module
3544
+ // dustuyse ilk cagri bunlardan biridir; donen kimlikler sonraki adimlari besler.
3545
+ entryPoints: mod.entryPoints,
3220
3546
  endpoints: (mod.endpoints || []).map((e) => ({
3221
3547
  path: e.path,
3222
3548
  summary: e.summary,
@@ -3224,6 +3550,8 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3224
3550
  method: mm.method,
3225
3551
  safetyClass: mm.safetyClass,
3226
3552
  requiredPermission: mm.requiredPermission,
3553
+ // true ise once describe_endpoint ile onkosullari oku.
3554
+ hasPrerequisites: mm.hasPrerequisites,
3227
3555
  })),
3228
3556
  })),
3229
3557
  };
@@ -3232,14 +3560,14 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3232
3560
  {
3233
3561
  name: 'describe_endpoint',
3234
3562
  description:
3235
- 'Get full detail for one capability: input field types (fields), response shape ' +
3236
- '(responseSchema the data body inside the {success, data} envelope), safety class, ' +
3237
- 'required permission, and summary/scenario when available. Use the path + method from ' +
3563
+ 'Get full detail for one capability: input field types (fields), full HTTP Response ' +
3564
+ 'Contract v2 (status, transport, content type, envelope and data/body schema), safety class, ' +
3565
+ 'required permission, prerequisites (which id this call needs FIRST and which endpoint produces it), and summary/scenario when available. Use the path + method from ' +
3238
3566
  'list_capabilities. Returns only if the key is permitted to call it.',
3239
3567
  inputSchema: {
3240
3568
  type: 'object',
3241
3569
  additionalProperties: false,
3242
- required: ['path'],
3570
+ required: ['path', 'method'],
3243
3571
  properties: {
3244
3572
  path: {
3245
3573
  type: 'string',
@@ -3248,41 +3576,11 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3248
3576
  method: {
3249
3577
  type: 'string',
3250
3578
  enum: ALLOWED_METHODS,
3251
- description: 'Optional HTTP method to disambiguate',
3579
+ description: 'HTTP method; required so multi-method paths are never selected ambiguously',
3252
3580
  },
3253
3581
  },
3254
3582
  },
3255
- handler: async (args) => {
3256
- const targetPath = String(args.path || '');
3257
- const targetMethod = args.method ? String(args.method).toUpperCase() : '';
3258
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
3259
- if (!res.ok) {
3260
- return res;
3261
- }
3262
- const caps = unwrapCapabilities(res);
3263
- for (const module of caps.modules || []) {
3264
- for (const endpoint of module.endpoints || []) {
3265
- if (endpoint.path !== targetPath) {
3266
- continue;
3267
- }
3268
- const match = (endpoint.methods || []).find(
3269
- (mm) => !targetMethod || mm.method === targetMethod,
3270
- );
3271
- if (match) {
3272
- return {
3273
- module: module.module,
3274
- path: endpoint.path,
3275
- summary: endpoint.summary,
3276
- ...match,
3277
- };
3278
- }
3279
- }
3280
- }
3281
- return {
3282
- ok: false,
3283
- error: `Endpoint not found or not permitted for this key: ${targetMethod || 'ANY'} ${targetPath}`,
3284
- };
3285
- },
3583
+ handler: describeCapabilityEndpoint,
3286
3584
  },
3287
3585
 
3288
3586
  // ══════════════════════════════════════════════════════════════════════════
@@ -4191,7 +4489,7 @@ let allowedPermissionsPromise = null;
4191
4489
  // bir kez cekilir (memoized). Basarisizlik -> null (cagiran fail-open/closed'a karar verir).
4192
4490
  const fetchAllowedPermissions = async () => {
4193
4491
  try {
4194
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
4492
+ const res = await systaFetch('GET', capabilityProjectionPath('compact'));
4195
4493
  if (!res || !res.ok) {
4196
4494
  return null;
4197
4495
  }