@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.
- package/README.md +94 -8
- package/dist/apps/acik-mcp/src/araclar.d.ts +2 -28
- package/dist/apps/acik-mcp/src/araclar.js +29 -114
- package/dist/apps/acik-mcp/src/bologna.d.ts +291 -0
- package/dist/apps/acik-mcp/src/bologna.js +1693 -0
- package/dist/apps/acik-mcp/src/genel-ayristir.d.ts +92 -0
- package/dist/apps/acik-mcp/src/genel-ayristir.js +489 -0
- package/dist/apps/acik-mcp/src/http.d.ts +31 -3
- package/dist/apps/acik-mcp/src/http.js +90 -17
- package/dist/apps/acik-mcp/src/index.d.ts +7 -0
- package/dist/apps/acik-mcp/src/index.js +7 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.d.ts +94 -0
- package/dist/apps/acik-mcp/src/kaynak-dogrula.js +460 -0
- package/dist/apps/acik-mcp/src/kaynak.d.ts +72 -0
- package/dist/apps/acik-mcp/src/kaynak.js +155 -0
- package/dist/apps/acik-mcp/src/kisisel-veri.d.ts +16 -2
- package/dist/apps/acik-mcp/src/kisisel-veri.js +78 -15
- package/dist/apps/acik-mcp/src/not-tablosu.d.ts +39 -0
- package/dist/apps/acik-mcp/src/not-tablosu.js +268 -0
- package/dist/apps/acik-mcp/src/promptlar.d.ts +48 -0
- package/dist/apps/acik-mcp/src/promptlar.js +323 -0
- package/dist/apps/acik-mcp/src/sunucu.d.ts +12 -4
- package/dist/apps/acik-mcp/src/sunucu.js +108 -24
- package/dist/apps/acik-mcp/src/temel.d.ts +59 -0
- package/dist/apps/acik-mcp/src/temel.js +10 -0
- package/dist/apps/acik-mcp/src/yardim.d.ts +56 -0
- package/dist/apps/acik-mcp/src/yardim.js +381 -0
- package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.d.ts +29 -0
- package/dist/packages/baglayicilar/src/bologna-proliz/ayristir.js +200 -0
- package/dist/packages/cekirdek/src/bologna/taslak.d.ts +19 -3
- package/dist/packages/cekirdek/src/bologna/taslak.js +70 -9
- package/dist/packages/cekirdek/src/turler.d.ts +8 -0
- 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
|
-
/**
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
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 {};
|