საკუთარი PBX-ის MCP-სერვერი
ბიზნესის მფლობელისა და ინტეგრატორისთვის: დაუკავშირეთ PBX AI-ასისტენტს — Claude-ს, ChatGPT-ს, Cursor-ს, VS Code-ს — და ჰკითხეთ ზარებზე ჩვეულებრივი სიტყვებით, გადაურეკეთ კლიენტებს და შეცვალეთ პარამეტრები გაცემული უფლებების ფარგლებში. MCP ასისტენტს ატყობინებს, რომელი მოქმედებებია ხელმისაწვდომი და რა პარამეტრებით; საჭირო მოქმედებას ასისტენტი თავად ირჩევს. გასაღებები, უფლებები და OAuth აღწერილია საკუთარი PBX-ის API-სა და MCP-ის მიმოხილვაში.
სტატიაშია: მისამართი, გასაღებით დაკავშირება, გასაღების გარეშე დაკავშირება, ინსტრუმენტები, დიალოგები, JSON-RPC ხელით, შეცდომები.
სერვერის მისამართი#
MCP სერვერის მისამართია https://api.cenaly.ge/pbx-api/mcp (streamable HTTP, JSON-RPC 2.0, სესიების გარეშე). ანგარიშისთვის ის ერთია როგორც გასაღებით დასაკავშირებლად, ისე OAuth კონექტორებისთვის. ობიექტს ასისტენტი პარამეტრით location ირჩევს — ობიექტების სიას ინსტრუმენტი list_locations იძლევა.
საკუთარი სატელეფონო სადგურის მქონე ობიექტს ამას გარდა აქვს პირდაპირი MCP მისამართი თავად სადგურის სერვერზე: https://sip-<ობიექტის id>.cenaly.ge/pbx-api/mcp (ფანჯარაში ტელეფონია → API და MCP → როგორ მივუერთდეთ და /v1/me-ის ველში direct.<ობიექტი>.mcp; მზადაა, როცა mcpReady:true). ის მხოლოდ cpx_… გასაღებებს იღებს და გვთავაზობს 13 ინსტრუმენტს, რომლებსაც სადგური თავად ასრულებს: ჟურნალი, ჩანაწერები, ზარი ერთი დაწკაპუნებით, გადამისამართება, «ნუ შემაწუხებთ», ხელმისაწვდომობა, მოვლენები და ღია/დაკეტილი. წესებს, კონფიგურაციას, ვებჰუკებს, მეტრიკებსა და ტრანსკრიფციებს ღრუბლის MCP ემსახურება (ასეთი ინსტრუმენტი პირდაპირ მისამართზე აბრუნებს served_by_cloud-ს cloudUrl-ით): ღრუბლის მისამართი მიუერთეთ მეორე სერვერად იმავე გასაღებით. ზემოთ მითითებული მისამართი ყველა ობიექტისთვის და OAuth-ისთვის მუშაობს.
გასაღებით დაკავშირება#
მფლობელი ხსნის ტელეფონია → API და MCP → გასაღებები → გასაღების შექმნა, ირჩევს უფლებებს, ობიექტებსა და ვადას. შეინახეთ გასაღები cpx_…: ის მხოლოდ ერთხელ ჩანს, დაკავშირებისთვის გამზადებულ ფრაგმენტებთან ერთად. დასაწყებად გამოდგება ნაკრები „AI-ასისტენტი“; ობიექტის დახურვის უფლება daynight:write ცალკე მონიშვნით ემატება.
Claude Code#
claude mcp add --transport http telephony https://api.cenaly.ge/pbx-api/mcp --header "Authorization: Bearer cpx_…"
claude mcp list
Claude Code-ში ბრძანება /mcp დაკავშირების მდგომარეობას აჩვენებს. ბრძანების ფორმატი მოცემულია Claude Code-ის დოკუმენტაციაში.
Cursor#
დაამატეთ სერვერი .cursor/mcp.json-ში; რეალური გასაღების მქონე ფაილი git-ში არ შეიტანოთ.
{
"mcpServers": {
"telephony": {
"url": "https://api.cenaly.ge/pbx-api/mcp",
"headers": {
"Authorization": "Bearer cpx_…"
}
}
}
}
VS Code#
ფაილი .vscode/mcp.json იყენებს გასაღებს servers და არა mcpServers. გაუშვით სერვერი ბრძანებით MCP: List Servers. ფორმატი მოცემულია VS Code-ის დოკუმენტაციაში.
{
"servers": {
"telephony": {
"type": "http",
"url": "https://api.cenaly.ge/pbx-api/mcp",
"headers": {
"Authorization": "Bearer cpx_…"
}
}
}
}
Claude Desktop#
ლოკალური claude_desktop_config.json გასაღებიან დისტანციურ სერვერს mcp-remote ხიდით უკავშირდება: საჭიროა Node.js და npx, ხიდი თქვენს კომპიუტერზე ეშვება. გახსენით ფაილი Claude Desktop-ის დეველოპერის პარამეტრებიდან, ჩასვით კონფიგურაცია და აპლიკაცია სრულად გადატვირთეთ.
{
"mcpServers": {
"telephony": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://api.cenaly.ge/pbx-api/mcp",
"--header",
"Authorization:${PBX_AUTH}"
],
"env": {
"PBX_AUTH": "Bearer cpx_…"
}
}
}
}
ფაილი გასაღებს შეიცავს — მასზე წვდომა შეზღუდეთ. ხიდისა და გასაღების გარეშე Claude Desktop OAuth-კონექტორით უკავშირდება (შემდეგი განყოფილება).
დაკავშირება გასაღების გარეშე: OAuth#
ღრუბლოვანი ასისტენტები იმავე მისამართს https://api.cenaly.ge/pbx-api/mcp OAuth-ით უკავშირდება: გასაღების შექმნა საჭირო არ არის, მფლობელი თავის ანგარიშში შედის და წვდომას აძლევს.
| კლიენტი | სად დაამატოთ |
|---|---|
| claude.ai, Claude Desktop | პარამეტრები → კონექტორები → საკუთარი კონექტორის დამატება → სერვერის URL; თუ OAuth-კლიენტზე გკითხავთ — ავტომატური რეგისტრაცია |
| ChatGPT | საკუთარი MCP-სერვერი (custom MCP server) → სერვერის URL → OAuth ავთენტიფიკაცია |
| Perplexity | ანგარიშის პარამეტრები → კონექტორები → საკუთარი დისტანციური კონექტორი → URL და OAuth |
| Cursor, VS Code | დისტანციური (HTTP) სერვერი იმავე URL-ით, გასაღების შემცველი სათაურის გარეშე — შესვლის ფანჯარა ბრაუზერში გაიხსნება |
მენიუს პუნქტები კლიენტის ვერსიასა და მასში თქვენი ანგარიშის ტარიფზეა დამოკიდებული; მიმდინარე გზები თავად კლიენტების დოკუმენტაციაშია: Claude, ChatGPT, VS Code.
შემდეგ ასისტენტი Cenaly-ის შესვლის ფანჯარას გახსნის: მფლობელი ხედავს, რომელ მისამართს ეძლევა წვდომა, მონიშნავს უფლებებსა და ობიექტებს და აჭერს „დაშვებას“. კავშირი „გასაღებებში“ OAuth ნიშნულით ჩნდება — იქვე იცვლება უფლებები და უქმდება წვდომა. რას აჩვენებს თანხმობის ფანჯარა და როგორ მუშაობს OAuth საკუთარი კონექტორებისთვის — მიმოხილვაში საკუთარი სატელეფონო სადგურის API და MCP.
ინსტრუმენტების კატალოგი#
კლიენტი ხედავს მხოლოდ გასაღებით ან კავშირით ნებადართულ ინსტრუმენტებს. „ცვლის“ ნიშნავს პარამეტრების შეცვლას, ტესტის გაგზავნას ან ზარის დაწყებას; მდგომარეობის წაკითხვა არაფერს ცვლის, მაშინაც კი, თუ უფლებას write ან dial ეწოდება.
| ინსტრუმენტი | რას აკეთებს | უფლება | ცვლის | ფრაზის მაგალითი |
|---|---|---|---|---|
list_locations |
ობიექტები და მათი სატელეფონო სადგურების მონაცემები | — | არა | რომელი ობიექტებია ხელმისაწვდომი? |
list_calls |
ზარების ჟურნალი ფილტრებით | calls:read |
არა | ვინ ვერ დაგვიკავშირდა დღეს? |
get_call |
ზარი და მარშრუტიზაციის გზა | calls:read |
არა | რატომ მოხვდა ეს ზარი 101-ზე? |
get_call_recording |
ჩანაწერის დროებითი ბმული | recordings:read |
არა | მომეცი ამ ზარის ჩანაწერი |
get_call_transcript |
საუბრის ტექსტი და შეჯამება | recordings:read |
არა | მაჩვენე საუბრის ტექსტი |
call_number |
ზარი თანამშრომლის ტელეფონით | calls:dial |
დიახ | დაურეკე კლიენტს 101-დან |
get_dial_status |
ერთი დაწკაპუნებით დაწყებული ზარის მიმდინარეობა | calls:dial |
არა | დარეკვა დაიწყო? |
list_extensions |
თანამშრომლები, ჯგუფები, ოთახები | extensions:read |
არა | ვინ მუშაობს ნომერზე 101? |
set_forwarding |
პირადი გადამისამართება | extensions:write |
დიახ | 102-ის ზარები 101-ზე გადაამისამართე |
set_do_not_disturb |
„არ შემაწუხოთ“ რეჟიმის ჩართვა ან გამორთვა | extensions:write |
დიახ | 102 დილამდე არ შეაწუხო |
get_presence |
ვინ არის თავისუფალი ამ მომენტში | presence:read |
არა | ვინ არის ახლა თავისუფალი? |
get_call_metrics |
პერიოდის ანალიტიკა | reports:read |
არა | შეადარე სექტემბერი აგვისტოს |
list_routes |
ხაზები, შემომავალი და გამავალი ზარების წესები | routes:read |
არა | სად მიდის შემომავალი ზარები? |
list_route_history |
წესების ვერსიები | routes:read |
არა | რა შეიცვალა შემომავალი ზარების წესებში? |
save_inbound_rule |
შემომავალი ზარის წესის შექმნა ან შეცვლა | routes:write |
დიახ | წესს „მორიგე“ დაარქვი |
delete_inbound_rule |
შემომავალი ზარის წესის წაშლა | routes:write |
დიახ | წაშალე ეს წესი |
move_inbound_rule |
შემომავალი ზარის წესის გადაადგილება | routes:write |
დიახ | პირველ ადგილზე დააყენე |
save_outbound_rule |
გამავალი ზარის წესის შექმნა ან შეცვლა | routes:write |
დიახ | ეს შაბლონი მეორე ხაზზე გაუშვი |
delete_outbound_rule |
გამავალი ზარის წესის წაშლა | routes:write |
დიახ | წაშალე გამავალი ზარების წესი |
move_outbound_rule |
გამავალი ზარის წესის გადაადგილება | routes:write |
დიახ | მეორე ადგილზე გადაიტანე |
set_outbound_calls |
გამავალი ზარები და ნაგულისხმევი ხაზი | routes:write |
დიახ | გამორთე გარე გამავალი ზარები |
restore_route_version |
წესების წინა ვერსიის აღდგენა | routes:write |
დიახ | აღადგინე გუშინდელი წესები |
export_config |
პარამეტრების ცხრილები პაროლების გარეშე | config:export |
არა | ჩამოტვირთე პარამეტრები |
import_config |
ცხრილების წინასწარი ნახვა და გამოყენება | config:import |
დიახ, apply-ისას |
ჯერ ფაილის ცვლილებები მაჩვენე |
get_call_events |
ზარების მოვლენების ნაკადი | calls:read |
არა | ამის შემდეგ რომელი მოვლენები მოვიდა? |
list_webhooks |
მოვლენების გამოწერები | webhooks:write |
არა | რომელი ვებჰუკებია ჩართული? |
save_webhook |
გამოწერის შექმნა ან შეცვლა | webhooks:write |
დიახ | ზარის დასრულების მოვლენები CRM-ში გაგზავნე |
delete_webhook |
გამოწერის წაშლა | webhooks:write |
დიახ | ძველი ვებჰუკი წაშალე |
rotate_webhook_secret |
ხელმოწერის საიდუმლო გასაღების შეცვლა | webhooks:write |
დიახ | ახალი საიდუმლო გასაღები შექმენი |
test_webhook |
webhook.ping-ის გაგზავნა |
webhooks:write |
დიახ | შეამოწმე მიწოდება |
list_webhook_deliveries |
მიწოდების მცდელობების ჟურნალი | webhooks:write |
არა | რატომ ვერ იღებს CRM მოვლენებს? |
get_open_closed |
ღიაა თუ არა ობიექტი | routes:read |
არა | ობიექტი ხელით არის დახურული? |
set_open_closed |
ობიექტის გახსნა ან დახურვა | daynight:write |
დიახ | დახურე ხვალ 09:00-მდე |
ინსტრუმენტების პარამეტრები და პასუხები REST მეთოდებს ემთხვევა — დეტალები იხილეთ სტატიაში თქვენი სატელეფონო სადგურის REST API.
დიალოგების მაგალითები#
„ვინ გამოტოვა ზარები დღეს?“ ასისტენტი აზუსტებს ობიექტსა და მის დროის სარტყელს, იძახებს list_calls-ს status:"missed"-ით, since = ადგილობრივი დღის დასაწყისი და until = შემდეგი დღის დასაწყისი, კითხულობს ყველა გვერდს nextCursor-ით, ხოლო კონკრეტული ზარის გასაანალიზებლად — get_call-ს. გამოტოვებული ზარი ყოველთვის ერთ თანამშრომელს არ ეკუთვნის: შესაძლოა რამდენიმე ტელეფონი ერთდროულად რეკავდა.
„ჩართე გადამისამართება 101-ზე“. ასისტენტი აზუსტებს, რომელი შიდა ნომრიდან და რა პირობით. პასუხის „102-ის ყველა ზარი“ შემდეგ იძახებს:
{
"name": "set_forwarding",
"arguments": {
"location": "loc1",
"extension": "102",
"forward": {
"always": {
"kind": "extension",
"extension": "101"
}
}
}
}
forward:null გადამისამართებას ხსნის. გარე ნომერი მიეთითება როგორც {"kind":"number","number":"+995555123456"} — ასეთი გადამისამართება წუთებს ხარჯავს.
„დახურე ობიექტი დილამდე“. ასისტენტი აზუსტებს გახსნის თარიღსა და დროს, ამოწმებს get_open_closed-ს და დადასტურების შემდეგ იძახებს set_open_closed-ს closed:true-ითა და until-ით, მაგალითად 2026-10-25T09:00:00+04:00. საჭიროა უფლება daynight:write და ჩართული დახურვა კოდით *28. until-ის გარეშე ობიექტი დახურულია დღის ბოლომდე და არა დილამდე; ვადა — მაქსიმუმ 30 დღე. closed:false ობიექტს ხსნის.
JSON-RPC ხელით#
POST მოთხოვნა JSON-ით, სესიის გარეშე. მხარდაჭერილია პროტოკოლის ვერსიები 2025-06-18, 2025-03-26, 2024-11-05. პასუხების მაგალითები შედგენილია კოდის მიხედვით, გამოგონილი მონაცემებით.
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."
}
}
შემდეგ — მზადყოფნის შეტყობინება (პასუხი 202 სხეულის გარეშე) და კატალოგი:
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"}'
პასუხი გასაღებისთვის, რომელსაც მხოლოდ calls:read უფლება აქვს (აღწერებსა და სქემებს სერვერი აგზავნის):
{
"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
}
}
]
}
}
ინსტრუმენტის გამოძახება:
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}}}'
პასუხი, თუ მოვლენები არ არის:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"location\":\"loc1\",\"events\":[],\"more\":false}"
}
],
"structuredContent": {
"location": "loc1",
"events": [],
"more": false
},
"isError": false
}
}
ინსტრუმენტის შეცდომას სერვერი აბრუნებს HTTP 200-ით: შეამოწმეთ result.isError; მიზეზი მოცემულია JSON-ში, content[].text ველში, მაგალითად {"error":"scope_required","scope":"calls:read"}. უცნობი ინსტრუმენტი — JSON-RPC შეცდომა -32602, უცნობი მეთოდი — -32601, არასწორი JSON — -32700.
შეცდომები და უსაფრთხოება#
| სიმპტომი | რა გააკეთოთ |
|---|---|
| HTTP 401 | შეამოწმეთ სათაური, ვადა და გასაღების გაუქმება; OAuth-ისთვის განაახლეთ ტოკენი ან ხელახლა დაუკავშირდით |
scope_required ან საჭირო ინსტრუმენტი არ არის |
გასაღების „უფლებებში“ დაამატეთ უფლება და კლიენტში ინსტრუმენტების სია განაახლეთ |
location_required |
ჯერ list_locations, შემდეგ კი გადააწოდეთ location |
location_not_found |
წერტილი არ არსებობს ან გასაღები სხვა წერტილებითაა შეზღუდული |
429 rate_limited |
დაელოდეთ Retry-After-ს; ლიმიტები REST-სა და MCP-სთვის საერთოა |
| GET MCP-ის მისამართზე არ მუშაობს | ეს POST JSON-RPC-ის მისამართია და არა ვებ-გვერდი |
| წერტილის დახურვისას 409 | ჩართეთ კოდით *28 დახურვა და შეამოწმეთ until თარიღი |
ლიმიტებია: 120 მოთხოვნა წუთში და 20 000 დღეში; ერთი დაწკაპუნებით ზარები ცალკეა — 10 წუთში და 300 დღეში. ქსელის შეფერხების შემდეგ call_number ბრმად არ გაიმეოროთ: ჯერ შეამოწმეთ get_dial_status და ჟურნალი.
routes:write, config:export, config:import, webhooks:write, daynight:write უფლებები მხოლოდ ცალკე მონიშვნით გაიცემა და „ყველა“ ნაკრებშიც კი არ შედის. გასაღები მფლობელის სახელით მოქმედებს: შეზღუდეთ ის წერტილებითა და ვადით, ნუ ჩასვამთ გასაღებს მისამართში ან ასისტენტთან დიალოგის ტექსტში. ნომრები და ჩანაწერები თქვენი კლიენტების პერსონალური მონაცემებია. სანამ ასისტენტი წესებს შეცვლის ან დარეკავს, შეამოწმეთ, რის გაკეთებას აპირებს. პარამეტრების იმპორტი ჯერ apply-ის გარეშე დაიწყეთ, შემდეგ კი წინასწარი ნახვის baseRev გამოიყენეთ.