systa-mcp 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +26 -2
  3. package/package.json +1 -1
  4. package/server.js +343 -50
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.0
3
3
  name: systa
4
4
  description: |
5
5
  Drive SysTa (Talep Takip Sistemi / SysTa) — a
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "systa-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.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
@@ -50,10 +50,27 @@ const INSTRUCTIONS = [
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
  })),
@@ -468,11 +772,16 @@ const shapeSummary = (caps) => ({
468
772
  moduleCount: caps.moduleCount,
469
773
  endpointCount: caps.endpointCount,
470
774
  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
- })),
775
+ modules: (caps.modules || []).map((m) => {
776
+ const paths = m.endpoints || [];
777
+ const methodCount = paths.reduce((sum, endpoint) => sum + (endpoint.methods || []).length, 0);
778
+ return {
779
+ module: m.module,
780
+ ...(m.description ? { description: m.description } : {}),
781
+ pathCount: m.pathCount ?? paths.length,
782
+ endpointCount: m.endpointCount ?? (methodCount || paths.length),
783
+ };
784
+ }),
476
785
  });
477
786
 
478
787
  const shapeOneModule = (caps, target) => {
@@ -3169,7 +3478,13 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3169
3478
  },
3170
3479
  },
3171
3480
  handler: async (args) => {
3172
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
3481
+ let projectionPath = capabilityProjectionPath('compact');
3482
+ if (args && args.module) {
3483
+ projectionPath = capabilityProjectionPath('module', { module: args.module });
3484
+ } else if (args && args.summary) {
3485
+ projectionPath = capabilityProjectionPath('summary');
3486
+ }
3487
+ const res = await systaFetch('GET', projectionPath);
3173
3488
  if (!res.ok) {
3174
3489
  return res;
3175
3490
  }
@@ -3187,7 +3502,7 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3187
3502
  name: 'describe_module',
3188
3503
  description:
3189
3504
  '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 ' +
3505
+ '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
3506
  'module name from list_capabilities (e.g. "request", "kanban", "plan", "sla"). Helps map a ' +
3192
3507
  "user's natural-language intent to the right module before describe_endpoint.",
3193
3508
  inputSchema: {
@@ -3199,7 +3514,10 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3199
3514
  },
3200
3515
  },
3201
3516
  handler: async (args) => {
3202
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
3517
+ const res = await systaFetch(
3518
+ 'GET',
3519
+ capabilityProjectionPath('module', { module: args.module }),
3520
+ );
3203
3521
  if (!res.ok) {
3204
3522
  return res;
3205
3523
  }
@@ -3217,6 +3535,9 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3217
3535
  module: mod.module,
3218
3536
  description: mod.description,
3219
3537
  concepts: mod.concepts,
3538
+ // Zincirin BASI: kimlik gerektirmeyen uclar. Kullanicinin niyeti bu module
3539
+ // dustuyse ilk cagri bunlardan biridir; donen kimlikler sonraki adimlari besler.
3540
+ entryPoints: mod.entryPoints,
3220
3541
  endpoints: (mod.endpoints || []).map((e) => ({
3221
3542
  path: e.path,
3222
3543
  summary: e.summary,
@@ -3224,6 +3545,8 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3224
3545
  method: mm.method,
3225
3546
  safetyClass: mm.safetyClass,
3226
3547
  requiredPermission: mm.requiredPermission,
3548
+ // true ise once describe_endpoint ile onkosullari oku.
3549
+ hasPrerequisites: mm.hasPrerequisites,
3227
3550
  })),
3228
3551
  })),
3229
3552
  };
@@ -3232,14 +3555,14 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3232
3555
  {
3233
3556
  name: 'describe_endpoint',
3234
3557
  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 ' +
3558
+ 'Get full detail for one capability: input field types (fields), full HTTP Response ' +
3559
+ 'Contract v2 (status, transport, content type, envelope and data/body schema), safety class, ' +
3560
+ '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
3561
  'list_capabilities. Returns only if the key is permitted to call it.',
3239
3562
  inputSchema: {
3240
3563
  type: 'object',
3241
3564
  additionalProperties: false,
3242
- required: ['path'],
3565
+ required: ['path', 'method'],
3243
3566
  properties: {
3244
3567
  path: {
3245
3568
  type: 'string',
@@ -3248,41 +3571,11 @@ The number is the request number for "#", and the card's GLOBAL id for "##" (NOT
3248
3571
  method: {
3249
3572
  type: 'string',
3250
3573
  enum: ALLOWED_METHODS,
3251
- description: 'Optional HTTP method to disambiguate',
3574
+ description: 'HTTP method; required so multi-method paths are never selected ambiguously',
3252
3575
  },
3253
3576
  },
3254
3577
  },
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
- },
3578
+ handler: describeCapabilityEndpoint,
3286
3579
  },
3287
3580
 
3288
3581
  // ══════════════════════════════════════════════════════════════════════════
@@ -4191,7 +4484,7 @@ let allowedPermissionsPromise = null;
4191
4484
  // bir kez cekilir (memoized). Basarisizlik -> null (cagiran fail-open/closed'a karar verir).
4192
4485
  const fetchAllowedPermissions = async () => {
4193
4486
  try {
4194
- const res = await systaFetch('GET', '/api-keys/me/capabilities');
4487
+ const res = await systaFetch('GET', capabilityProjectionPath('compact'));
4195
4488
  if (!res || !res.ok) {
4196
4489
  return null;
4197
4490
  }