اتصال سرورهای MCP به آشا
پروتکل MCP ( Model Context Protocol ) پرطرفدارترین راه دادن «ابزار» به مدلهای زبانی است. درگاه آشا نیازی به endpoint اختصاصی MCP ندارد: endpointِ /v1/chat/completions بدنهٔ درخواست را با همان ساختار به ارائهدهنده میفرستد؛ tools ، tool_choice ، tool_calls و پیامهای role: tool بدون هیچ تغییر سمت سرور کار میکنند.
در این راهنما یک کلاینت MCP کامل میسازیم: به یک سرور MCP وصل میشویم (مثلاً سرور فایلسیستم
رسمی)، ابزارهایش را لیست میکنیم، آنها را به تعریف ابزار فرمت OpenAI تبدیل میکنیم، درخواست را با
پارامتر tools به آشا میفرستیم و نتیجهٔ فراخوانی ابزار را بهصورت پیام
role: tool برای پاسخ نهایی برمیگردانیم. هرجا که در نسخهٔ
OpenRouter baseURL را میبینید، فقط مقدار آن به
https://app.asha-ai.ir/v1/ و کلید به
sk-asha-... تغییر میکند.
پیشنیازها
- یک کلید API آشا (
ASHA_API_KEY=sk-asha-...) در متغیر محیطی - یک مدلِ پشتیبانیکنندهٔ tools از کاتالوگ مدلها — در این مثال
~anthropic/claude-sonnet-latest - Node.js برای اجرای سرور نمونهٔ MCP با
npx - کتابخانههای زبان خودتان (جدول زیر)
| زبان | نصب |
|---|---|
| Python | pip install mcp openai python-dotenv |
| TypeScript | npm i @modelcontextprotocol/sdk openai |
| JavaScript | npm i @modelcontextprotocol/sdk openai |
| Go | go get github.com/mark3labs/mcp-go github.com/sashabaranov/go-openai |
| PHP | composer require openai-php/client php-mcp/client |
کلاینت MCP به چند زبان
همان برنامهٔ یکسان را در پنج زبان و در نهایت با HTTP خام (curl) میبینید. در همهٔ پیادهسازیها، تابع کمکی، تعریف ابزار MCP (فرمت Anthropic) را به تعریف تابع فرمت OpenAI تبدیل میکند.
import asyncio
import json
import os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from openai import OpenAI
MODEL = "~anthropic/claude-sonnet-latest" # any slug from the Asha catalogue
SERVER_PARAMS = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
)
client = OpenAI(
base_url="https://app.asha-ai.ir/v1/",
api_key=os.environ["ASHA_API_KEY"],
)
def to_openai_tool(tool):
"""Convert an MCP tool definition to the OpenAI function format."""
return {
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": tool.inputSchema,
},
}
async def run():
async with stdio_client(SERVER_PARAMS) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = (await session.list_tools()).tools
print("Connected tools:", [t.name for t in tools])
messages = [{"role": "user", "content": "List the files in my folder."}]
function_defs = [to_openai_tool(t) for t in tools]
assistant = client.chat.completions.create(
model=MODEL, messages=messages, tools=function_defs
).choices[0].message
messages.append(assistant.model_dump(exclude_none=True))
while assistant.tool_calls:
for call in assistant.tool_calls:
result = await session.call_tool(
call.function.name,
json.loads(call.function.arguments or "{}"),
)
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result.content[0].text,
})
assistant = client.chat.completions.create(
model=MODEL, messages=messages, tools=function_defs
).choices[0].message
messages.append(assistant.model_dump(exclude_none=True))
print(assistant.content)
asyncio.run(run())
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
const MODEL = "~anthropic/claude-sonnet-latest";
const openai = new OpenAI({
baseURL: "https://app.asha-ai.ir/v1/",
apiKey: process.env.ASHA_API_KEY!,
});
const mcp = new Client({ name: "asha-mcp-client", version: "1.0.0" });
await mcp.connect(new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
}));
const { tools } = await mcp.listTools();
const toOpenAITool = (t: any) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema,
},
});
const messages: any[] = [
{ role: "user", content: "List the files in my folder." },
];
const functionDefs = tools.map(toOpenAITool);
let assistant = (
await openai.chat.completions.create({ model: MODEL, messages, tools: functionDefs })
).choices[0].message;
messages.push(assistant);
while (assistant.tool_calls?.length) {
for (const call of assistant.tool_calls) {
const result = await mcp.callTool({
name: call.function.name,
arguments: JSON.parse(call.function.arguments ?? "{}"),
});
messages.push({
role: "tool",
tool_call_id: call.id,
content: result.content?.[0]?.text ?? "",
});
}
assistant = (
await openai.chat.completions.create({ model: MODEL, messages, tools: functionDefs })
).choices[0].message;
messages.push(assistant);
}
console.log(assistant.content);
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import OpenAI from "openai";
const MODEL = "~anthropic/claude-sonnet-latest";
const openai = new OpenAI({
baseURL: "https://app.asha-ai.ir/v1/",
apiKey: process.env.ASHA_API_KEY,
});
const mcp = new Client({ name: "asha-mcp-client", version: "1.0.0" });
await mcp.connect(new StdioClientTransport({
command: "npx",
args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
}));
const { tools } = await mcp.listTools();
const toOpenAITool = (t) => ({
type: "function",
function: {
name: t.name,
description: t.description,
parameters: t.inputSchema,
},
});
const messages = [{ role: "user", content: "List the files in my folder." }];
const functionDefs = tools.map(toOpenAITool);
let assistant = (
await openai.chat.completions.create({ model: MODEL, messages, tools: functionDefs })
).choices[0].message;
messages.push(assistant);
while (assistant.tool_calls?.length) {
for (const call of assistant.tool_calls) {
const result = await mcp.callTool({
name: call.function.name,
arguments: JSON.parse(call.function.arguments ?? "{}"),
});
messages.push({
role: "tool",
tool_call_id: call.id,
content: result.content?.[0]?.text ?? "",
});
}
assistant = (
await openai.chat.completions.create({ model: MODEL, messages, tools: functionDefs })
).choices[0].message;
messages.push(assistant);
}
console.log(assistant.content);
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
"github.com/mark3labs/mcp-go/client"
"github.com/mark3labs/mcp-go/mcp"
openai "github.com/sashabaranov/go-openai"
)
const model = "~anthropic/claude-sonnet-latest"
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
mcpClient, err := client.NewStdioMCPClient(
"npx", nil,
"-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir",
)
if err != nil {
log.Fatal(err)
}
defer mcpClient.Close()
if _, err := mcpClient.Initialize(ctx, mcp.InitializeRequest{}); err != nil {
log.Fatal(err)
}
listed, err := mcpClient.ListTools(ctx, mcp.ListToolsRequest{})
if err != nil {
log.Fatal(err)
}
for _, t := range listed.Tools {
fmt.Printf("- %s: %sn", t.Name, t.Description)
}
tools := make([]openai.Tool, 0, len(listed.Tools))
for _, t := range listed.Tools {
schemaJSON, _ := json.Marshal(t.InputSchema)
var schema any
_ = json.Unmarshal(schemaJSON, &schema)
tools = append(tools, openai.Tool{
Type: openai.ToolTypeFunction,
Function: &openai.FunctionDefinition{
Name: t.Name,
Description: t.Description,
Parameters: schema,
},
})
}
ai := openai.NewClientWithConfig(openai.DefaultConfig(os.Getenv("ASHA_API_KEY")))
ai.BaseURL = "https://app.asha-ai.ir/v1"
messages := []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleUser, Content: "List the files in my folder."},
}
next := func() (openai.ChatCompletionMessage, error) {
reply, err := ai.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: model,
Messages: messages,
Tools: tools,
ToolChoice: "auto",
})
if err != nil {
return openai.ChatCompletionMessage{}, err
}
assistant := reply.Choices[0].Message
messages = append(messages, assistant)
return assistant, nil
}
assistant, err := next()
if err != nil {
log.Fatal(err)
}
for len(assistant.ToolCalls) > 0 {
for _, call := range assistant.ToolCalls {
args := map[string]any{}
_ = json.Unmarshal([]byte(call.Function.Arguments), &args)
res, err := mcpClient.CallTool(ctx, mcp.CallToolRequest{
Params: mcp.CallToolParams{
Name: call.Function.Name,
Arguments: args,
},
})
if err != nil {
log.Fatal(err)
}
text := ""
for _, c := range res.Content {
if tc, ok := c.(mcp.TextContent); ok {
text += tc.Text
}
}
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleTool,
ToolCallID: call.ID,
Content: text,
})
}
assistant, err = next()
if err != nil {
log.Fatal(err)
}
}
fmt.Println(assistant.Content)
}
<?php
require 'vendor/autoload.php';
use McpClient;
use McpClientTransportStdioTransport;
use McpSchemaContentTextContent;
use OpenAI;
$model = '~anthropic/claude-sonnet-latest';
$asha = OpenAI::factory()
->withApiKey($_ENV['ASHA_API_KEY'])
->withBaseUri('https://app.asha-ai.ir/v1/')
->make();
$mcp = Client::builder()
->setClientInfo('asha-mcp-client', '1.0.0')
->build();
$mcp->connect(new StdioTransport(
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/dir'],
));
$toolsResult = $mcp->listTools();
$tools = array_map(static function (object $tool): array {
return [
'type' => 'function',
'function' => [
'name' => $tool->name,
'description' => $tool->description,
'parameters' => $tool->inputSchema,
],
];
}, $toolsResult->tools);
$messages = [
['role' => 'user', 'content' => 'List the files in my folder.'],
];
$completion = $asha->chat()->create([
'model' => $model,
'messages' => $messages,
'tools' => $tools,
]);
$assistant = $completion->choices[0]->message;
$messages[] = [
'role' => 'assistant',
'content' => $assistant->content,
'tool_calls' => $assistant->toolCalls,
];
while (!empty($assistant->toolCalls)) {
foreach ($assistant->toolCalls as $call) {
$result = $mcp->callTool(
$call->function->name,
json_decode($call->function->arguments ?? '{}', true),
);
$text = '';
foreach ($result->content as $content) {
if ($content instanceof TextContent) {
$text .= $content->text;
}
}
$messages[] = [
'role' => 'tool',
'tool_call_id' => $call->id,
'content' => $text,
];
}
$completion = $asha->chat()->create([
'model' => $model,
'messages' => $messages,
'tools' => $tools,
]);
$assistant = $completion->choices[0]->message;
$messages[] = [
'role' => 'assistant',
'content' => $assistant->content,
'tool_calls' => $assistant->toolCalls,
];
}
echo $assistant->content . PHP_EOL;
$mcp->disconnect();
# ---- Request 1: model asks to call a tool (finish_reason: tool_calls) ----
curl https://app.asha-ai.ir/v1/chat/completions
-H "Authorization: Bearer $ASHA_API_KEY"
-H "Content-Type: application/json"
-d '{
"model": "~anthropic/claude-sonnet-latest",
"messages": [
{ "role": "user", "content": "List the files in my folder." }
],
"tools": [
{
"type": "function",
"function": {
"name": "list_directory",
"description": "List the files in a directory",
"parameters": {
"type": "object",
"properties": { "path": { "type": "string" } }
}
}
}
]
}'
# ---- Request 2: append role:tool result, get the final answer ----
curl https://app.asha-ai.ir/v1/chat/completions
-H "Authorization: Bearer $ASHA_API_KEY"
-H "Content-Type: application/json"
-d '{
"model": "~anthropic/claude-sonnet-latest",
"messages": [
{ "role": "user", "content": "List the files in my folder." },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "list_directory",
"arguments": "{"path":"/home/user"}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "README.mdnsrc/n"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "list_directory",
"description": "List the files in a directory",
"parameters": {
"type": "object",
"properties": { "path": { "type": "string" } }
}
}
}
]
}'
نکتهها: این واقعیت که tools یک مقدارِ ردیابیشده در
supported_parameters است، یعنی برای هر مدل از
صفحهٔ مدلها میتوانید چک کنید که آیا ابزارها را پشتیبانی میکند یا نه.
اگر مدل، ابزار را پشتیبانی نکند، میتوانید درخواست خود را به مدلی بفرستید که پشتیبانی میکند یا از
همان فرآیند «تبدیل ابزار» صرفنظر کنید.
چرا آشا؟
- بدون endpoint جدید — همان سرویس
/v1/chat/completionsکل پیامهای ابزار را بدون تغییر به ارائهدهنده منتقل میکند. - یک کلید برای همهٔ مدلها — فقط
MODELرا عوض کنید؛ کلاینتِ MCP بدون تغییر کار میکند. - صورتحساب به تومان از روی مصرفِ گزارششده — هزینهٔ دقیق هر درخواست و هر فراخوانی ابزار از خروجیِ ارائهدهنده ثبت میشود، نه برآورد.
- مشاهدهٔ چرخههای فراخوانی ابزار — هر رفتوبرگشت
tool_callدر «فعالیت» پنل شما دیده میشود. - کنترل هزینه با کیف پول — سقف کیف پول و کلیدهای قابل ابطال، هزینهٔ اجرای agentها را محدود نگه میدارد.
عیبیابی
- «No API key» یا خطای 401 — متغیر
ASHA_API_KEYرا چک کنید و از اعتبار کلید (و موجودی کیف پول) مطمئن شوید. - مدل از ابزارها پشتیبانی نمیکند —
toolsرا ازsupported_parametersمدل در کاتالوگ بررسی کنید. - npx / Node در دسترس نیست — برای سرورهای نمونهٔ MCP نود مورد نیاز است؛ یا سرور MCP را با روشی دیگر اجرا کنید.
- خروجی ابزار متنی/سریالیزه نیست — اگر محتوای ابزار «resource» یا «image» باشد، آن را قبل از افزودن به پیام
role: toolبه متن تبدیل کنید.