اتصال سرورهای 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-... تغییر می‌کند.

پروتکل MCP داری‌حالت (stateful) است و به مدیریت نشست (session) نیاز دارد؛ اما این پیچیدگی کنارِ کلاینت است، نه درگاه آشا. آشا صرفاً یک API سازگار با OpenAI باقی می‌ماند و کل چرخهٔ MCP——لیست ابزار، فراخوانی ابزار، دور دوم پاسخ——در کدِ کلاینت شما مدیریت می‌شود.

پیش‌نیازها

  • یک کلید API آشا (ASHA_API_KEY=sk-asha-...) در متغیر محیطی
  • یک مدلِ پشتیبانی‌کنندهٔ tools از کاتالوگ مدل‌ها — در این مثال ~anthropic/claude-sonnet-latest
  • Node.js برای اجرای سرور نمونهٔ MCP با npx
  • کتابخانه‌های زبان خودتان (جدول زیر)
زباننصب
Pythonpip install mcp openai python-dotenv
TypeScriptnpm i @modelcontextprotocol/sdk openai
JavaScriptnpm i @modelcontextprotocol/sdk openai
Gogo get github.com/mark3labs/mcp-go github.com/sashabaranov/go-openai
PHPcomposer 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 به متن تبدیل کنید.