Kendi santralinizin MCP sunucusu
İşletme sahipleri ve entegratörler için: santralinizi Claude, ChatGPT, Cursor veya VS Code gibi bir yapay zekâ asistanına bağlayın; aramaları günlük dille sorun, müşterileri geri arayın ve verdiğiniz yetkiler kapsamında ayarları değiştirin. MCP, hangi işlemlerin hangi parametrelerle kullanılabildiğini asistana bildirir; uygun işlemi asistan seçer. Anahtarlar, yetkiler ve OAuth, Kendi santralinizin API ve MCP'si genel bakışında açıklanır.
Bu makalede: adres, anahtarla bağlantı, anahtarsız bağlantı, araçlar, diyaloglar, elle JSON-RPC, hatalar.
Sunucu adresi#
MCP sunucusunun adresi https://api.cenaly.ge/pbx-api/mcp şeklindedir (streamable HTTP, JSON-RPC 2.0, oturumsuz). Hem anahtarla bağlantı hem OAuth bağlayıcıları için hesap başına tek adres vardır. Asistan şubeyi location parametresiyle seçer; şube listesini list_locations aracı verir.
Kendi santrali olan şubenin ayrıca santral sunucusu üzerinde doğrudan bir MCP adresi vardır: https://sip-<şube kimliği>.cenaly.ge/pbx-api/mcp (Telefon → API ve MCP → Nasıl bağlanır penceresinde ve /v1/me yanıtındaki direct.<şube>.mcp alanında; mcpReady:true olduğunda hazırdır). Yalnızca cpx_… anahtarlarını kabul eder ve santralin kendisinin yürüttüğü 13 aracı sunar: günlük, kayıtlar, tıkla-ara, yönlendirme, «rahatsız etmeyin», durum bilgisi, olaylar ve açık/kapalı. Kurallar, yapılandırma, webhook'lar, metrikler ve dökümler bulut MCP'si tarafından sunulur (doğrudan adreste adıyla çağrılan böyle bir araç cloudUrl ile served_by_cloud döndürür): bulut adresini aynı anahtarla ikinci sunucu olarak bağlayın. Yukarıdaki adres tüm şubeler ve OAuth için çalışmaya devam eder.
Anahtarla bağlantı#
Hesap sahibi Telefon sistemi → API ve MCP → Anahtarlar → Anahtar oluştur yolunu açar; yetkileri, şubeleri ve süreyi seçer. cpx_… anahtarını kaydedin: hazır bağlantı örnekleriyle birlikte yalnızca bir kez gösterilir. Başlangıç için “Yapay zekâ asistanı” yetki grubu uygundur; şubeyi kapatma yetkisi daynight:write ayrı bir kutuyla eklenir.
Claude Code#
claude mcp add --transport http telephony https://api.cenaly.ge/pbx-api/mcp --header "Authorization: Bearer cpx_…"
claude mcp list
Claude Code'da /mcp komutu bağlantının durumunu gösterir. Komut biçimi Claude Code belgelerinde yer alır.
Cursor#
Sunucuyu .cursor/mcp.json dosyasına ekleyin; gerçek anahtar içeren dosyayı git'e kaydetmeyin.
{
"mcpServers": {
"telephony": {
"url": "https://api.cenaly.ge/pbx-api/mcp",
"headers": {
"Authorization": "Bearer cpx_…"
}
}
}
}
VS Code#
.vscode/mcp.json dosyası mcpServers yerine servers anahtarını kullanır. Sunucuyu MCP: List Servers komutuyla başlatın. Biçim VS Code belgelerinde açıklanır.
{
"servers": {
"telephony": {
"type": "http",
"url": "https://api.cenaly.ge/pbx-api/mcp",
"headers": {
"Authorization": "Bearer cpx_…"
}
}
}
}
Claude Desktop#
Yerel claude_desktop_config.json, anahtarlı uzak sunucuya mcp-remote köprüsü üzerinden bağlanır: npx içeren Node.js gerekir ve köprü bilgisayarınızda çalışır. Dosyayı Claude Desktop geliştirici ayarlarından açın, yapılandırmayı yapıştırın ve uygulamayı tamamen yeniden başlatın.
{
"mcpServers": {
"telephony": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.cenaly.ge/pbx-api/mcp",
"--header",
"Authorization:${PBX_AUTH}"
],
"env": {
"PBX_AUTH": "Bearer cpx_…"
}
}
}
}
Dosya anahtarı içerir; dosyaya erişimi kısıtlayın. Claude Desktop köprü ve anahtar olmadan OAuth bağlayıcısıyla bağlanır (sonraki bölüm).
Anahtarsız bağlantı: OAuth#
Bulut asistanları aynı https://api.cenaly.ge/pbx-api/mcp adresine OAuth ile bağlanır: anahtar oluşturmak gerekmez; hesap sahibi kendi hesabına giriş yapıp erişime izin verir.
| İstemci | Nereden eklenir? |
|---|---|
| claude.ai, Claude Desktop | Ayarlar → Bağlayıcılar → Özel bağlayıcı ekle → sunucu URL'si; OAuth istemcisi sorulursa otomatik kayıt |
| ChatGPT | Özel MCP sunucusu (custom MCP server) → sunucu URL'si → OAuth kimlik doğrulaması |
| Perplexity | Hesap ayarları → Bağlayıcılar → özel uzak bağlayıcı → URL ve OAuth |
| Cursor, VS Code | Aynı URL'yi kullanan, anahtar başlığı olmayan uzak (HTTP) sunucu; giriş penceresi tarayıcıda açılır |
Menü öğeleri istemcinin sürümüne ve o hizmetteki hesabınızın planına bağlıdır; güncel yollar istemcilerin kendi belgelerinde yer alır: Claude, ChatGPT, VS Code.
Ardından asistan Cenaly giriş penceresini açar: hesap sahibi erişimin hangi adrese verildiğini görür, yetkileri ve şubeleri işaretler, “İzin ver” düğmesine basar. Bağlantı “Anahtarlar” bölümünde OAuth etiketiyle görünür; yetkiler aynı yerden değiştirilir ve erişim iptal edilir. İzin penceresinin içeriği ve kendi bağlayıcılarınız için OAuth'un işleyişi, Kendi santralinizin API ve MCP'si genel bakışında açıklanır.
Araç kataloğu#
İstemci yalnızca anahtarının veya bağlantısının izin verdiği araçları görür. “Değişiklik yapar” ayar değişikliği, test gönderme veya arama başlatma anlamına gelir; durum okumak, yetki write veya dial adını taşısa bile hiçbir şeyi değiştirmez.
| Araç | İşlevi | Yetki | Değişiklik yapar | Örnek ifade |
|---|---|---|---|---|
list_locations |
Şubeler ve santral bilgileri | — | Hayır | Hangi şubelere erişebilirim? |
list_calls |
Filtrelenebilir arama günlüğü | calls:read |
Hayır | Bugün kim ulaşamadı? |
get_call |
Arama ve yönlendirme yolu | calls:read |
Hayır | Bu arama neden 101'e gitti? |
get_call_recording |
Kayda geçici bağlantı | recordings:read |
Hayır | Bu aramanın kaydını ver |
get_call_transcript |
Konuşma dökümü ve özeti | recordings:read |
Hayır | Dökümü göster |
call_number |
Çalışanın telefonu üzerinden arama | calls:dial |
Evet | Müşteriyi 101'den ara |
get_dial_status |
Tıkla aramanın ilerleyişi | calls:dial |
Hayır | Arama başladı mı? |
list_extensions |
Çalışanlar, gruplar, odalar | extensions:read |
Hayır | 101 numarasını kim kullanıyor? |
set_forwarding |
Kişisel yönlendirme | extensions:write |
Evet | 102'nin aramalarını 101'e yönlendir |
set_do_not_disturb |
Rahatsız etmeyin durumunu açma veya kapatma | extensions:write |
Evet | 102'yi sabaha kadar rahatsız etmeyin durumuna al |
get_presence |
Şu anda kim müsait? | presence:read |
Hayır | Şu anda kim müsait? |
get_call_metrics |
Dönem analizi | reports:read |
Hayır | Eylül ile ağustosu karşılaştır |
list_routes |
Hatlar, gelen ve giden arama kuralları | routes:read |
Hayır | Gelen aramalar nereye gidiyor? |
list_route_history |
Kural sürümleri | routes:read |
Hayır | Gelen arama kurallarında ne değişti? |
save_inbound_rule |
Gelen arama kuralı oluşturma veya değiştirme | routes:write |
Evet | Kuralın adını “Nöbetçi” yap |
delete_inbound_rule |
Gelen arama kuralını silme | routes:write |
Evet | Bu kuralı sil |
move_inbound_rule |
Gelen arama kuralının sırasını değiştirme | routes:write |
Evet | Bunu ilk sıraya koy |
save_outbound_rule |
Giden arama kuralı oluşturma veya değiştirme | routes:write |
Evet | Bu kalıbı ikinci hattan geçir |
delete_outbound_rule |
Giden arama kuralını silme | routes:write |
Evet | Giden arama kuralını sil |
move_outbound_rule |
Giden arama kuralının sırasını değiştirme | routes:write |
Evet | Bunu ikinci sıraya taşı |
set_outbound_calls |
Giden aramalar ve varsayılan hat | routes:write |
Evet | Harici giden aramaları kapat |
restore_route_version |
Kuralları bir sürüme geri alma | routes:write |
Evet | Dünkü kuralları geri getir |
export_config |
Şifresiz ayar tabloları | config:export |
Hayır | Ayarları dışa aktar |
import_config |
Tabloları önizleme ve uygulama | config:import |
apply ile evet |
Önce dosyanın getireceği değişiklikleri göster |
get_call_events |
Arama olayları akışı | calls:read |
Hayır | Bundan sonra hangi olaylar geldi? |
list_webhooks |
Olay abonelikleri | webhooks:write |
Hayır | Hangi webhook'lar açık? |
save_webhook |
Abonelik oluşturma veya değiştirme | webhooks:write |
Evet | Arama bitişlerini CRM'e gönder |
delete_webhook |
Abonelik silme | webhooks:write |
Evet | Eski webhook'u sil |
rotate_webhook_secret |
İmza sırrını değiştirme | webhooks:write |
Evet | Yeni bir sır oluştur |
test_webhook |
webhook.ping gönderme |
webhooks:write |
Evet | Teslimatı test et |
list_webhook_deliveries |
Teslimat denemeleri günlüğü | webhooks:write |
Hayır | CRM neden olayları alamıyor? |
get_open_closed |
Şubenin açık olup olmadığı | routes:read |
Hayır | Şube elle mi kapatıldı? |
set_open_closed |
Şubeyi açma veya kapatma | daynight:write |
Evet | Yarın 09:00'a kadar kapat |
Araç parametreleri ve yanıtları REST yöntemleriyle aynıdır; ayrıntılar Kendi santralinizin REST API'si makalesindedir.
Örnek diyaloglar#
“Bugün aramaları kim kaçırdı?” Asistan şubeyi ve saat dilimini netleştirir; list_calls aracını status:"missed", since = yerel günün başlangıcı ve until = sonraki günün başlangıcı ile çağırır, nextCursor üzerinden tüm sayfaları okur; belirli bir aramayı incelemek için get_call kullanır. Cevapsız arama her zaman tek bir çalışana ait değildir: birden fazla telefon aynı anda çalmış olabilir.
“101'e yönlendirmeyi aç.” Asistan hangi dahiliden, hangi koşulda yönlendirileceğini sorar. “102'nin tüm aramaları” yanıtından sonra şu çağrıyı yapar:
{
"name": "set_forwarding",
"arguments": {
"location": "loc1",
"extension": "102",
"forward": {
"always": {
"kind": "extension",
"extension": "101"
}
}
}
}
forward:null yönlendirmeyi kaldırır. Harici numara {"kind":"number","number":"+995555123456"} biçiminde verilir; bu yönlendirme dakika harcar.
“Şubeyi sabaha kadar kapat.” Asistan açılış tarihini ve saatini netleştirir, get_open_closed ile durumu kontrol eder ve onaydan sonra closed:true ve örneğin 2026-10-25T09:00:00+04:00 değerindeki until ile set_open_closed çağırır. daynight:write yetkisi ve *28 koduyla kapatmanın açık olması gerekir. until olmadan şube sabaha kadar değil gün sonuna kadar kapalı kalır; süre en fazla 30 gündür. closed:false şubeyi açar.
Elle JSON-RPC#
Oturumsuz, JSON içeren POST isteği. Desteklenen protokol sürümleri: 2025-06-18, 2025-03-26, 2024-11-05. Yanıt örnekleri kod temel alınarak kurgusal verilerle hazırlanmıştır.
MCP='https://api.cenaly.ge/pbx-api/mcp'
KEY='cpx_…'
curl -sS "$MCP" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"example","version":"1.0"}}}'
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "cenaly-pbx",
"title": "Cenaly PBX",
"version": "1.0.0"
},
"instructions": "Cenaly phone system (PBX) of the key owner. Start with list_locations if unsure which location to use. For \"who called today\" use list_calls with since = start of today in the location time zone. Phone numbers are personal data: show them only to the account owner."
}
}
Ardından hazır olma bildirimi (gövdesiz 202 yanıtı) ve katalog:
curl -sS "$MCP" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -sS "$MCP" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
Yalnızca calls:read yetkisi olan anahtar için yanıt (açıklamaları ve şemaları sunucu gönderir):
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "list_locations",
"description": "Locations (business points) this key can see, with their phone system summary and `direct` — the address where the phone system serves this API itself (prefer it when ready).",
"inputSchema": {
"type": "object",
"properties": {},
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false
}
},
{
"name": "list_calls",
"description": "Phone system call log, newest first: who called whom, status (answered / missed / ai / other), duration. Use since/until (ISO time) for \"today\", status=\"missed\" for missed calls.",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Location id (from list_locations). Optional when the key has one location."
},
"since": {
"type": "string",
"description": "ISO date-time, inclusive"
},
"until": {
"type": "string",
"description": "ISO date-time, exclusive"
},
"direction": {
"type": "string",
"enum": [
"inbound",
"outbound",
"internal",
"transfer"
]
},
"status": {
"type": "string",
"description": "Status group answered|missed|ai|other or an exact status"
},
"number": {
"type": "string",
"description": "Part of the phone number (digits)"
},
"extension": {
"type": "string"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100
},
"cursor": {
"type": "string"
}
},
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false
}
},
{
"name": "get_call",
"description": "One call with its routing trace: which rule matched, whose phone rang, who answered.",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Location id (from list_locations). Optional when the key has one location."
},
"id": {
"type": "string"
}
},
"required": [
"id"
],
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false
}
},
{
"name": "get_call_events",
"description": "Live call events of the last 7 days (up to 2000 per location), oldest first: call.started, call.ringing, call.answered, call.transferred, call.ended (who, which line, who answered, result). Poll with since = the \"next\" value of the previous answer to get only new events (also pages through the history: up to 500 per answer, \"more\" = there is a next page). For push delivery set up a webhook (save_webhook).",
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "Location id (from list_locations). Optional when the key has one location."
},
"since": {
"type": "string",
"description": "Event id from a previous answer (\"next\") or ISO time; only later events are returned"
},
"types": {
"type": "array",
"items": {
"type": "string",
"enum": [
"call.started",
"call.ringing",
"call.answered",
"call.transferred",
"call.ended"
]
}
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 500
}
},
"additionalProperties": false
},
"annotations": {
"readOnlyHint": true,
"openWorldHint": false
}
}
]
}
}
Araç çağrısı:
curl -sS "$MCP" -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_call_events","arguments":{"location":"loc1","limit":10}}}'
Olay yoksa yanıt:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"location\":\"loc1\",\"events\":[],\"more\":false}"
}
],
"structuredContent": {
"location": "loc1",
"events": [],
"more": false
},
"isError": false
}
}
Sunucu araç hatasını HTTP 200 ile döndürür: result.isError değerini kontrol edin; neden content[].text içindeki JSON'dur, örneğin {"error":"scope_required","scope":"calls:read"}. Bilinmeyen araç JSON-RPC -32602, bilinmeyen yöntem -32601, bozuk JSON ise -32700 hatasıdır.
Hatalar ve güvenlik#
| Belirti | Ne yapmalı? |
|---|---|
| HTTP 401 | Başlığı, anahtarın süresini ve iptal edilip edilmediğini kontrol edin; OAuth için belirteci yenileyin veya yeniden bağlanın |
scope_required veya gereken araç yok |
Anahtarın “Yetkiler” bölümüne yetki ekleyip istemcideki araç listesini yenileyin |
location_required |
Önce list_locations çağırın, ardından location gönderin |
location_not_found |
Şube yok veya anahtar başka şubelerle sınırlı |
429 rate_limited |
Retry-After süresince bekleyin; limitler REST ve MCP için ortaktır |
| MCP adresine GET çalışmıyor | Bu adres web sayfası değil, POST JSON-RPC içindir |
| Şubeyi kapatırken 409 | *28 koduyla kapatmayı açın ve until tarihini kontrol edin |
Limitler: dakikada 120, günde 20.000 istek; tıkla arama için ayrıca dakikada 10, günde 300. Ağ hatasından sonra call_number çağrısını kontrol etmeden tekrarlamayın: önce get_dial_status ve günlüğe bakın.
routes:write, config:export, config:import, webhooks:write, daynight:write yetkileri yalnızca ayrı kutularla verilir ve “Tümü” grubuna bile dahil değildir. Anahtar hesap sahibi adına işlem yapar: şubelerini ve süresini sınırlayın; anahtarı adrese veya asistanla konuşma metnine yapıştırmayın. Numaralar ve kayıtlar müşterilerinizin kişisel verileridir. Asistan kuralları değiştirmeden veya arama yapmadan önce ne yapacağını kontrol edin. Ayarları içe aktarırken önce apply olmadan, ardından önizlemedeki baseRev ile ilerleyin.