@akredit/acik-mcp 0.1.0 → 0.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 (33) hide show
  1. package/README.md +94 -8
  2. package/dist/apps/acik-mcp/src/araclar.d.ts +2 -28
  3. package/dist/apps/acik-mcp/src/araclar.js +29 -114
  4. package/dist/apps/acik-mcp/src/bologna.d.ts +291 -0
  5. package/dist/apps/acik-mcp/src/bologna.js +1693 -0
  6. package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +92 -0
  7. package/dist/apps/acik-mcp/src/genel-ayristir.js +489 -0
  8. package/dist/apps/acik-mcp/src/http.d.ts +31 -3
  9. package/dist/apps/acik-mcp/src/http.js +90 -17
  10. package/dist/apps/acik-mcp/src/index.d.ts +7 -0
  11. package/dist/apps/acik-mcp/src/index.js +7 -0
  12. package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +94 -0
  13. package/dist/apps/acik-mcp/src/kaynak-dogrula.js +460 -0
  14. package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
  15. package/dist/apps/acik-mcp/src/kaynak.js +155 -0
  16. package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +16 -2
  17. package/dist/apps/acik-mcp/src/kisisel-veri.js +78 -15
  18. package/dist/apps/acik-mcp/src/not-tablosu.d.ts +39 -0
  19. package/dist/apps/acik-mcp/src/not-tablosu.js +268 -0
  20. package/dist/apps/acik-mcp/src/promptlar.d.ts +48 -0
  21. package/dist/apps/acik-mcp/src/promptlar.js +323 -0
  22. package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
  23. package/dist/apps/acik-mcp/src/sunucu.js +108 -24
  24. package/dist/apps/acik-mcp/src/temel.d.ts +59 -0
  25. package/dist/apps/acik-mcp/src/temel.js +10 -0
  26. package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
  27. package/dist/apps/acik-mcp/src/yardim.js +381 -0
  28. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
  29. package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
  30. package/dist/packages/cekirdek/src/bologna/taslak.d.ts +19 -3
  31. package/dist/packages/cekirdek/src/bologna/taslak.js +70 -9
  32. package/dist/packages/cekirdek/src/turler.d.ts +8 -0
  33. package/package.json +5 -3
@@ -6,6 +6,26 @@
6
6
  // Sınırlar: gövde en çok 512 KB; adres başına kova (varsayılan 60 istek, saniyede 1 dolar).
7
7
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
8
8
  import { acikSunucu } from "./sunucu.js";
9
+ /**
10
+ * İstemci adresi. Sıra: cf-connecting-ip (Cloudflare yazar, istemci değiştiremez), x-real-ip (ters vekil yazar),
11
+ * x-forwarded-for'un sondan `guvenilenVekil`. öğesi. İlk öğe asla kullanılmaz: istemci onu istediği gibi yazabilir.
12
+ * Not: cf-connecting-ip'ye ancak köken sunucuya yalnız Cloudflare üzerinden erişiliyorsa güvenilebilir.
13
+ */
14
+ export function istemciAdresi(basliklar, guvenilenVekil = 1) {
15
+ const cf = basliklar.get("cf-connecting-ip")?.trim();
16
+ if (cf)
17
+ return cf.slice(0, 64);
18
+ const gercek = basliklar.get("x-real-ip")?.trim();
19
+ if (gercek)
20
+ return gercek.slice(0, 64);
21
+ const liste = (basliklar.get("x-forwarded-for") ?? "")
22
+ .split(",")
23
+ .map((x) => x.trim())
24
+ .filter(Boolean);
25
+ const n = Math.max(1, Math.floor(guvenilenVekil));
26
+ const adres = liste.length >= n ? liste[liste.length - n] : undefined;
27
+ return adres ? adres.slice(0, 64) : "bilinmeyen";
28
+ }
9
29
  export const CORS_BASLIKLARI = {
10
30
  "access-control-allow-origin": "*",
11
31
  "access-control-allow-methods": "POST, GET, OPTIONS",
@@ -18,7 +38,11 @@ const json = (govde, durum, ek = {}) => new Response(JSON.stringify(govde), {
18
38
  headers: { "content-type": "application/json; charset=utf-8", ...CORS_BASLIKLARI, ...ek },
19
39
  });
20
40
  const rpcHata = (durum, code, message, ek = {}) => json({ jsonrpc: "2.0", id: null, error: { code, message } }, durum, ek);
21
- /** Süreç içi kova. Adres sayısı sınırlıdır; dolan tablo en eski kayıtlardan boşaltılır. */
41
+ /**
42
+ * Süreç içi kova. Adres sayısı sınırlıdır. Tablo dolunca önce süresi geçmiş (jetonu yeniden dolmuş, yani yeni
43
+ * gelen bir adresle aynı durumdaki) kayıtlar silinir; yine doluysa yeni adres reddedilir. Böylece çok sayıda sahte
44
+ * adresle eski kayıtları itip gerçek adreslerin sayacı sıfırlanamaz.
45
+ */
22
46
  export class AdresKovasi {
23
47
  kovalar = new Map();
24
48
  kapasite;
@@ -32,11 +56,10 @@ export class AdresKovasi {
32
56
  al(adres, simdiMs) {
33
57
  let k = this.kovalar.get(adres);
34
58
  if (!k) {
35
- if (this.kovalar.size >= this.enCokAdres) {
36
- const ilk = this.kovalar.keys().next().value;
37
- if (ilk !== undefined)
38
- this.kovalar.delete(ilk);
39
- }
59
+ if (this.kovalar.size >= this.enCokAdres)
60
+ this.temizle(simdiMs);
61
+ if (this.kovalar.size >= this.enCokAdres)
62
+ return { izin: false, bekle: Math.ceil(1 / this.saniyede) };
40
63
  k = { jeton: this.kapasite, son: simdiMs };
41
64
  this.kovalar.set(adres, k);
42
65
  }
@@ -47,8 +70,46 @@ export class AdresKovasi {
47
70
  k.jeton -= 1;
48
71
  return { izin: true, bekle: 0 };
49
72
  }
73
+ /** Jetonu yeniden kapasiteye dolmuş kayıtları siler. */
74
+ temizle(simdiMs) {
75
+ for (const [a, k] of this.kovalar)
76
+ if (k.jeton + ((simdiMs - k.son) / 1000) * this.saniyede >= this.kapasite)
77
+ this.kovalar.delete(a);
78
+ }
79
+ get boyut() {
80
+ return this.kovalar.size;
81
+ }
82
+ }
83
+ /** Gövdeyi bayt sayarak okur; sınır aşılınca akışı iptal eder. content-length olmasa da (chunked) bellek sınırlı kalır. */
84
+ export async function govdeOku(istek, enBuyuk) {
85
+ const uzunluk = Number(istek.headers.get("content-length") ?? "0");
86
+ if (uzunluk > enBuyuk)
87
+ return { tur: "buyuk" };
88
+ if (!istek.body)
89
+ return { tur: "tamam", tampon: new Uint8Array(0) };
90
+ const okuyucu = istek.body.getReader();
91
+ const parcalar = [];
92
+ let toplam = 0;
93
+ for (;;) {
94
+ const { done, value } = await okuyucu.read();
95
+ if (done)
96
+ break;
97
+ toplam += value.byteLength;
98
+ if (toplam > enBuyuk) {
99
+ await okuyucu.cancel().catch(() => { });
100
+ return { tur: "buyuk" };
101
+ }
102
+ parcalar.push(value);
103
+ }
104
+ const tampon = new Uint8Array(toplam);
105
+ let i = 0;
106
+ for (const p of parcalar) {
107
+ tampon.set(p, i);
108
+ i += p.byteLength;
109
+ }
110
+ return { tur: "tamam", tampon };
50
111
  }
51
- /** Gövdeden günlüğe yazılacak tek bilgi: yöntem ve (tools/call ise) araç adı. İçerik okunmaz. */
112
+ /** Gövdeden günlüğe yazılacak tek bilgi: yöntem ve (tools/call ya da prompts/get ise) araç ya da istem adı. İçerik okunmaz. */
52
113
  function yontemVeArac(govde) {
53
114
  const ilk = Array.isArray(govde) ? govde[0] : govde;
54
115
  if (!ilk || typeof ilk !== "object")
@@ -58,7 +119,9 @@ function yontemVeArac(govde) {
58
119
  const ad = ilk.params?.name;
59
120
  return {
60
121
  yontem,
61
- arac: yontem === "tools/call" && typeof ad === "string" ? ad.replace(/[^a-z_]/gu, "").slice(0, 40) : null,
122
+ arac: (yontem === "tools/call" || yontem === "prompts/get") && typeof ad === "string"
123
+ ? ad.replace(/[^a-z_]/gu, "").slice(0, 40)
124
+ : null,
62
125
  };
63
126
  }
64
127
  export function acikMcpIsleyici(s = {}) {
@@ -66,7 +129,8 @@ export function acikMcpIsleyici(s = {}) {
66
129
  const enBuyuk = s.enBuyukGovde ?? 512 * 1024;
67
130
  const kova = new AdresKovasi(s.hiz?.kapasite ?? 60, s.hiz?.saniyede ?? 1);
68
131
  const gunluk = s.gunluk ?? ((k) => console.info(JSON.stringify({ olay: "acik_mcp", ...k })));
69
- const adresBasligi = s.adresBasligi ?? "x-forwarded-for";
132
+ const guvenilenVekil = s.guvenilenVekil ?? 1;
133
+ const kaynakKovasi = new AdresKovasi(s.kaynakHizi?.kapasite ?? 5, s.kaynakHizi?.saniyede ?? 5 / 60);
70
134
  return async (istek) => {
71
135
  const bas = performance.now();
72
136
  let kayit = { yontem: istek.method, arac: null, boyut: 0 };
@@ -87,20 +151,17 @@ export function acikMcpIsleyici(s = {}) {
87
151
  return bitir(json({
88
152
  hata: "Bu adres bir MCP sunucusudur. Yapay zekâ istemcinize bağlayıcı olarak ekleyin: https://akredit.org/mcp-kullan",
89
153
  }, 405, { allow: "POST, OPTIONS" }));
90
- const adres = (istek.headers.get(adresBasligi) ?? "").split(",")[0]?.trim() || "bilinmeyen";
154
+ const adres = istemciAdresi(istek.headers, guvenilenVekil);
91
155
  const h = kova.al(adres, simdi().getTime());
92
156
  if (!h.izin)
93
157
  return bitir(rpcHata(429, -32000, "Çok fazla istek. Biraz bekleyip yeniden deneyin.", { "retry-after": String(h.bekle) }));
94
- const uzunluk = Number(istek.headers.get("content-length") ?? "0");
95
- if (uzunluk > enBuyuk)
96
- return bitir(rpcHata(413, -32600, "İstek çok büyük. Veriyi bölerek gönderin."));
97
158
  let metin;
98
159
  try {
99
- const tampon = await istek.arrayBuffer();
100
- kayit.boyut = tampon.byteLength;
101
- if (tampon.byteLength > enBuyuk)
160
+ const g = await govdeOku(istek, enBuyuk);
161
+ if (g.tur === "buyuk")
102
162
  return bitir(rpcHata(413, -32600, "İstek çok büyük. Veriyi bölerek gönderin."));
103
- metin = new TextDecoder().decode(tampon);
163
+ kayit.boyut = g.tampon.byteLength;
164
+ metin = new TextDecoder().decode(g.tampon);
104
165
  }
105
166
  catch {
106
167
  return bitir(rpcHata(400, -32700, "İstek okunamadı."));
@@ -113,6 +174,18 @@ export function acikMcpIsleyici(s = {}) {
113
174
  return bitir(rpcHata(400, -32700, "İleti JSON değil."));
114
175
  }
115
176
  kayit = { ...kayit, ...yontemVeArac(govde) };
177
+ // Dış katalog sorgusu yapan araç için adres başına ayrı, sıkı kota (toplu istekte her çağrı sayılır).
178
+ const kaynakCagrisi = (Array.isArray(govde) ? govde : [govde]).filter((x) => x &&
179
+ typeof x === "object" &&
180
+ x.method === "tools/call" &&
181
+ x.params?.name === "kaynak_dogrula").length;
182
+ for (let i = 0; i < kaynakCagrisi; i++) {
183
+ const k = kaynakKovasi.al(adres, simdi().getTime());
184
+ if (!k.izin)
185
+ return bitir(rpcHata(429, -32000, "Kaynak doğrulama için kısa sürede çok fazla istek geldi. Bir dakika bekleyip yeniden deneyin.", {
186
+ "retry-after": String(k.bekle),
187
+ }));
188
+ }
116
189
  const sunucu = acikSunucu({ simdi });
117
190
  const tasima = new WebStandardStreamableHTTPServerTransport({
118
191
  sessionIdGenerator: undefined,
@@ -1,4 +1,11 @@
1
1
  export * from "./araclar.ts";
2
+ export * from "./bologna.ts";
3
+ export * from "./genel-ayristir.ts";
2
4
  export * from "./http.ts";
5
+ export * from "./kaynak.ts";
6
+ export * from "./kaynak-dogrula.ts";
3
7
  export * from "./kisisel-veri.ts";
8
+ export * from "./not-tablosu.ts";
9
+ export * from "./promptlar.ts";
4
10
  export * from "./sunucu.ts";
11
+ export * from "./yardim.ts";
@@ -1,6 +1,13 @@
1
1
  // @akredit/acik-mcp: herkese açık, hesapsız, salt okuma MCP sunucusu (İP-49).
2
2
  // Kapsam: plan/ip/IP-49-acik-mcp.md
3
3
  export * from "./araclar.js";
4
+ export * from "./bologna.js";
5
+ export * from "./genel-ayristir.js";
4
6
  export * from "./http.js";
7
+ export * from "./kaynak.js";
8
+ export * from "./kaynak-dogrula.js";
5
9
  export * from "./kisisel-veri.js";
10
+ export * from "./not-tablosu.js";
11
+ export * from "./promptlar.js";
6
12
  export * from "./sunucu.js";
13
+ export * from "./yardim.js";
@@ -0,0 +1,94 @@
1
+ import { z } from "zod";
2
+ export declare const IZINLI_HOSTLAR: readonly ["api.crossref.org", "openlibrary.org", "www.googleapis.com"];
3
+ export declare const ZAMAN_ASIMI_MS = 8000;
4
+ export declare const EN_BUYUK_YANIT = 1000000;
5
+ /** Çağrı başına en çok kaynak. */
6
+ export declare const EN_COK_KAYNAK = 5;
7
+ /** Süreç genelinde aynı anda en çok dış istek. */
8
+ export declare const ESZAMANLI_DIS_ISTEK = 4;
9
+ /** Süreç genelindeki dış istek semaforu. Sınır doluysa sıra bekler. */
10
+ export declare function sinirli<T>(is: () => Promise<T>): Promise<T>;
11
+ /** Katalog kaydının imzası: künye ve bağlantı üzerinden HMAC-SHA256 (ilk 32 onaltılık hane). */
12
+ export declare function kaynakImzasi(kunye: string, baglanti: string): string;
13
+ /** İmza bu sürecin kaynak_dogrula çıktısına mı ait? */
14
+ export declare function kaynakImzasiDogru(kunye: string, baglanti: string, imza: string): boolean;
15
+ /** "https://doi.org/10.x/y", "doi:10.x/y" ya da "10.x/y" → "10.x/y". Geçersizse null. */
16
+ export declare function doiTemizle(d: string): string | null;
17
+ /** ISBN-10 ya da ISBN-13; tire ve boşluk atılır, denetim basamağı sınanır. Geçersizse null. */
18
+ export declare function isbnTemizle(i: string): string | null;
19
+ /** Karşılaştırma için sadeleştirme: küçük harf, Türkçe harfler düz, noktalama yok. */
20
+ export declare function sadele(s: string): string[];
21
+ /**
22
+ * Sorgu başlığının sözcüklerinin en az %80'i bulunan başlıkta var mı, ve bulunanın en az %60'ı sorguda var mı.
23
+ * Kısa (en çok iki sözcüklü) başlıklarda ("Veri Yapıları", "Genel Kimya") sözcük kümeleri birebir aynı olmalı.
24
+ */
25
+ export declare function baslikEslesir(sorgu: string, bulunan: string): boolean;
26
+ /**
27
+ * Sorgudaki yazar soyadı (son sözcük ya da virgülden önceki kısım) bulunan yazarlardan birinde TAM belirteç olarak
28
+ * geçiyor mu. Alt dize yetmez: "Yılmaz" sorgusu "Yılmazer" ile eşleşmez.
29
+ */
30
+ export declare function yazarEslesir(sorgu: string, yazarlar: string[]): boolean;
31
+ /** Yanıt gövdesini bayt sayarak okur; sınır aşılırsa akışı iptal edip null döner. */
32
+ export declare function sinirliMetin(y: Response, enBuyuk: number): Promise<string | null>;
33
+ export type KayitTuru = "kitap" | "makale" | "kitap_bolumu" | "diger";
34
+ export interface Dogrulanmis {
35
+ durum: "katalogda_bulundu";
36
+ kunye: string;
37
+ eksikler: string[];
38
+ baglanti: string;
39
+ katalog: "Crossref" | "Open Library" | "Google Books";
40
+ kayitTuru: KayitTuru;
41
+ baslik: string;
42
+ yazarlar: string[];
43
+ yil?: number | string;
44
+ /** Neyle eşleşti: DOI, ISBN ya da başlık + tam soyad (+ yıl, tür). */
45
+ kimlik_eslesmesi: {
46
+ yontem: "doi" | "isbn" | "baslik_yazar";
47
+ denetlenen: string[];
48
+ };
49
+ /** Künyede eksik alan var mı (yayınevi, yıl...). Eksik alan [EKSİK] kalır. */
50
+ bibliyografik_tamlik: "tam" | "eksik";
51
+ /** Katalogda bulunmak, ders kaynağı olarak uygun olmak demek değildir. */
52
+ ders_kaynagi_uygunlugu: "hoca karar verir";
53
+ /** Bologna araçlarında doğrulanmış kaynak kanıtı. */
54
+ imza: string;
55
+ }
56
+ export interface Dogrulanamadi {
57
+ durum: "bulunamadi";
58
+ neden: string;
59
+ /** Hoca eseri kendisi biliyorsa yalnız "hoca_beyani" etiketiyle yazılabilir. */
60
+ hocaBeyani: string;
61
+ }
62
+ export type DogrulamaSonucu = Dogrulanmis | Dogrulanamadi;
63
+ declare const KaynakSorgusu: z.ZodObject<{
64
+ doi: z.ZodOptional<z.ZodString>;
65
+ isbn: z.ZodOptional<z.ZodString>;
66
+ baslik: z.ZodOptional<z.ZodString>;
67
+ yazar: z.ZodOptional<z.ZodString>;
68
+ tur: z.ZodOptional<z.ZodEnum<{
69
+ kitap: "kitap";
70
+ makale: "makale";
71
+ kitap_bolumu: "kitap_bolumu";
72
+ herhangi: "herhangi";
73
+ }>>;
74
+ yil: z.ZodOptional<z.ZodNumber>;
75
+ }, z.core.$strip>;
76
+ type KaynakSorgusuT = z.infer<typeof KaynakSorgusu>;
77
+ /** Tek kaynağı doğrular. DOI önce, sonra ISBN, sonra başlık+yazar. */
78
+ export declare function kaynakDogrula(f: typeof fetch, k: KaynakSorgusuT): Promise<DogrulamaSonucu>;
79
+ export declare const kaynakDogrulaAraci: import("./temel.ts").AcikArac<z.ZodObject<{
80
+ kaynaklar: z.ZodArray<z.ZodObject<{
81
+ doi: z.ZodOptional<z.ZodString>;
82
+ isbn: z.ZodOptional<z.ZodString>;
83
+ baslik: z.ZodOptional<z.ZodString>;
84
+ yazar: z.ZodOptional<z.ZodString>;
85
+ tur: z.ZodOptional<z.ZodEnum<{
86
+ kitap: "kitap";
87
+ makale: "makale";
88
+ kitap_bolumu: "kitap_bolumu";
89
+ herhangi: "herhangi";
90
+ }>>;
91
+ yil: z.ZodOptional<z.ZodNumber>;
92
+ }, z.core.$strip>>;
93
+ }, z.core.$strip>>;
94
+ export {};