GraphQL plugin cung cấp một MCP server để AI client có thể khám phá và gọi các công cụ liên quan đến schema, REST swagger, SQL scripts, prompt, resource... MCP server này chạy trên endpoint /graphql/mcp, dùng JSON-RPC 2.0 và chuyển các method chuẩn của MCP thành event nội bộ.

Cơ chế hoạt động

MCP server không yêu cầu mỗi tool phải đăng ký thủ công vào một danh sách tập trung. Thay vào đó, mỗi tool được cài bằng 2 event handler:
  • Tool schema handler: mô tả tool cho tools/list.
  • Tool call handler: xử lý khi MCP client gọi tools/call.
Tên event là contract quan trọng nhất:
<tool_name>_mcp_tool_schema_event
<tool_name>_mcp_tool_event
Ví dụ tool get_web_view_schema sẽ có:
get_web_view_schema_mcp_tool_schema_event
get_web_view_schema_mcp_tool_event
Luồng tổng quát:
sequenceDiagram
    participant Client as MCP Client
    participant Server as GraphQL MCP Server
    participant Event as Event Handler Manager
    participant Schema as Tool Schema Handler
    participant Tool as Tool Call Handler

    Client->>Server: POST /graphql/mcp initialize
    Server->>Event: initialize_mcp_method_event
    Event-->>Server: capabilities, serverInfo
    Server-->>Client: JSON-RPC result

    Client->>Server: POST /graphql/mcp tools/list
    Server->>Event: tools/list_mcp_method_event
    Event->>Schema: scan *_mcp_tool_schema_event
    Schema-->>Event: tool metadata
    Event-->>Server: danh sách tools
    Server-->>Client: JSON-RPC result

    Client->>Server: POST /graphql/mcp tools/call
    Server->>Event: tools/call_mcp_method_event
    Event->>Tool: gọi <tool_name>_mcp_tool_event
    Tool-->>Event: content, structuredContent, isError
    Event-->>Server: tool result
    Server-->>Client: JSON-RPC result

MCP Method và MCP Tool

Trong plugin, cần phân biệt rõ:
  • MCP method dùng cho protocol: initialize, ping, tools/list, tools/call, resources/list, resources/read, prompts/list...
  • MCP tool dùng cho tính năng ứng dụng: lấy schema, lấy swagger, tạo dữ liệu, gọi API quản trị...
Khi thêm tính năng mới cho AI client, hãy tạo MCP tool, không tạo MCP method mới, trừ khi bạn thật sự mở rộng protocol-level behavior.

Endpoint MCP

MCP server expose:
GET /graphql/mcp
POST /graphql/mcp
GET /graphql/mcp trả về thông tin mô tả server, protocol version, endpoint và danh sách method được hỗ trợ.
POST /graphql/mcp nhận JSON-RPC request. Ví dụ:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
Server sẽ tự chuyển method thành event name:
tools/list -> tools/list_mcp_method_event
tools/call -> tools/call_mcp_method_event
initialize -> initialize_mcp_method_event
Với tools/call, server đọc params.name, tìm tool handler tương ứng, rồi truyền params.arguments vào handler.

Cấu trúc một MCP Tool

Một tool hoàn chỉnh gồm 2 class.

Tool Schema Handler

Schema handler trả về metadata để MCP client biết tool tên gì, nhận input nào và trả output dạng nào.
Ví dụ theo pattern của AdminMcpGetWebViewSchemaToolSchemaEventHandler:
@EzySingleton
public class AdminMcpGetWebViewSchemaToolSchemaEventHandler
    extends AbstractEventHandler<Map<String, Object>, Object> {

    @Override
    public Object handleEventData(Map<String, Object> data) {
        return EzyMapBuilder.mapBuilder()
            .put("name", "get_web_view_schema")
            .put("title", "Get Web View Schema")
            .put(
                "description",
                "Returns the download URL for the web view schema. " +
                    "Use curl to save to disk; do not read into context."
            )
            .put("inputSchema", newInputSchema())
            .put("outputSchema", newOutputSchema())
            .toMap();
    }

    private Map<String, Object> newInputSchema() {
        return EzyMapBuilder.mapBuilder()
            .put("type", "object")
            .put("properties", Collections.emptyMap())
            .put("additionalProperties", Boolean.FALSE)
            .toMap();
    }

    private Map<String, Object> newOutputSchema() {
        return EzyMapBuilder.mapBuilder()
            .put("type", "object")
            .put(
                "properties",
                EzyMapBuilder.mapBuilder()
                    .put(
                        "schema",
                        EzyMapBuilder.mapBuilder()
                            .put("type", "string")
                            .put(
                                "description",
                                "JSON view schema content of the web plugin."
                            )
                            .toMap()
                    )
                    .toMap()
            )
            .put("required", Collections.singletonList("schema"))
            .put("additionalProperties", Boolean.FALSE)
            .toMap();
    }

    @Override
    public String getEventName() {
        return "get_web_view_schema_mcp_tool_schema_event";
    }
}
Điểm cần nhớ:
  • name phải đúng với phần <tool_name> trong event name.
  • inputSchema nên là JSON Schema object.
  • Nếu tool không nhận tham số, dùng properties rỗng và additionalProperties = false.
  • outputSchema mô tả structuredContent, không thay thế phần text content.

Tool Call Handler

Tool call handler xử lý logic thật khi MCP client gọi tool.
Ví dụ:
@EzySingleton
public class AdminMcpGetWebViewSchemaToolEventHandler
    extends AbstractEventHandler<Map<String, Object>, Object> {

    private static final String DOWNLOAD_PATH =
        "/graphql/api/v1/schemas/web/view";

    private static final String RESPONSE_TEXT =
        "Web view schema is available for download at: " + DOWNLOAD_PATH + "nn" +
        "IMPORTANT: Do NOT read the file content into context; " +
        "it can be very large and will waste tokens.nn" +
        "Save to the project's .agents/ folder instead:n" +
        "  curl --create-dirs -o .agents/web-view-schema.json <server_base_url>" +
        DOWNLOAD_PATH;

    @Override
    public Object handleEventData(Map<String, Object> arguments) {
        return EzyMapBuilder.mapBuilder()
            .put(
                "content",
                Collections.singletonList(
                    EzyMapBuilder.mapBuilder()
                        .put("type", "text")
                        .put("text", RESPONSE_TEXT)
                        .toMap()
                )
            )
            .put("isError", Boolean.FALSE)
            .toMap();
    }

    @Override
    public String getEventName() {
        return "get_web_view_schema_mcp_tool_event";
    }
}
Tool result nên có dạng:
{
  "content": [
    {
      "type": "text",
      "text": "Result text"
    }
  ],
  "structuredContent": {
    "schema": "{}"
  },
  "isError": false
}
structuredContent nên được thêm khi output có dữ liệu có cấu trúc. Nếu tool chỉ hướng dẫn tải file hoặc trả thông báo text, có thể chỉ cần contentisError.

Ví dụ Tool Có Tham Số

Với tool nhận tham số, khai báo required trong inputSchema.
Ví dụ tool lấy schema của một view theo URI:
private Map<String, Object> newInputSchema() {
    return EzyMapBuilder.mapBuilder()
        .put("type", "object")
        .put(
            "properties",
            EzyMapBuilder.mapBuilder()
                .put(
                    "viewUri",
                    EzyMapBuilder.mapBuilder()
                        .put("type", "string")
                        .put("description", "The URI of the web view.")
                        .toMap()
                )
                .toMap()
        )
        .put("required", Collections.singletonList("viewUri"))
        .put("additionalProperties", Boolean.FALSE)
        .toMap();
}
Trong call handler:
@Override
public Object handleEventData(Map<String, Object> arguments) {
    arguments = arguments != null
        ? arguments
        : Collections.emptyMap();

    String viewUri = (String) arguments.get("viewUri");
    if (viewUri == null || viewUri.isEmpty()) {
        return newToolCallResult("viewUri is required", true);
    }

    String schemaJson = loadSchemaByViewUri(viewUri);
    return newToolCallResult(schemaJson, false);
}

Kiểm Tra Tool

Sau khi thêm 2 handler và restart plugin, gọi tools/list:
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
Kết quả phải có tool mới trong result.tools.
Gọi tool:
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "get_web_view_schema",
    "arguments": {}
  }
}
Với tool có tham số:
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_web_view_schema_by_uri",
    "arguments": {
      "viewUri": "/articles"
    }
  }
}

Lưu Ý Bảo Mật

Endpoint MCP được bảo vệ bằng cơ chế authenticated admin. Khi request đi qua MCP server, admin access token được đưa vào params, và nếu có arguments, token cũng được đưa vào arguments.
Điều này giúp tool handler có thể gọi tiếp các API nội bộ cần quyền admin hoặc quyền quản trị web mà không bắt MCP client tự truyền token trong input schema.
Không nên khai báo access token là field public trong inputSchema.

Checklist Khi Thêm Tool Mới

  • Tạo schema handler với @EzySingleton.
  • Handler extends AbstractEventHandler<Map<String, Object>, Object>.
  • getEventName() của schema handler trả về <tool_name>_mcp_tool_schema_event.
  • Metadata có đủ name, title, description, inputSchema.
  • Tạo call handler với @EzySingleton.
  • getEventName() của call handler trả về <tool_name>_mcp_tool_event.
  • Call handler validate arguments.
  • Tool trả về contentisError.
  • Nếu có dữ liệu máy đọc được, thêm structuredContent.
  • Restart plugin và kiểm tra bằng tools/list, sau đó tools/call.

Kết Luận

Để cài một MCP tool tương thích với GraphQL plugin, chỉ cần tuân thủ contract event name và response shape của MCP tool. Plugin sẽ tự động scan các schema handler cho tools/list và các call handler cho tools/call, nên việc mở rộng tool mới khá gọn: một handler để mô tả, một handler để thực thi.