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.
- package/README.md +1 -1
- package/SKILL.md +28 -4
- package/package.json +1 -1
- 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
|
|
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.
|
|
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 (
|
|
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ı: `{
|
|
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.
|
|
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,
|
|
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)`
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
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 ~
|
|
457
|
-
* buffers; this summary is
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
'
|
|
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(
|
|
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),
|
|
3236
|
-
'
|
|
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: '
|
|
3579
|
+
description: 'HTTP method; required so multi-method paths are never selected ambiguously',
|
|
3252
3580
|
},
|
|
3253
3581
|
},
|
|
3254
3582
|
},
|
|
3255
|
-
handler:
|
|
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', '
|
|
4492
|
+
const res = await systaFetch('GET', capabilityProjectionPath('compact'));
|
|
4195
4493
|
if (!res || !res.ok) {
|
|
4196
4494
|
return null;
|
|
4197
4495
|
}
|