დაიწყეთ უფასოდ

საკუთარი სატელეფონო სისტემის MCP-სერვერი

სატელეფონო სისტემის დაკავშირება Claude-თან, ChatGPT-სთან, Cursor-თან და VS Code-თან: გასაღებით ან OAuth-ით გასაღების გარეშე, 33 ხელსაწყოს კატალოგი, დიალოგების მაგალითები, JSON-RPC ხელით, შეცდომები და უსაფრთხოება

10 min read ადმინ პანელის დემო
On this page 12

საკუთარი 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 გამოიყენეთ.

იხილეთ ასევე#