API เซิร์ฟเวอร์ MCP (Model Context Protocol) แบบอ่านอย่างเดียว
ให้ AI ภายนอก (เช่น ผู้ช่วยเขียนโค้ด/แชท) อ่านข้อมูลบัญชีของบริษัทผ่านโปรโตคอล MCP แบบ Streamable HTTP + JSON-RPC 2.0 โดยใช้ token เฉพาะ (smlmcp_ro_...) ที่ผู้ดูแลออกให้ ไม่ใช้ JWT เข้าระบบปกติ. โครงสร้างตาราง token อยู่ที่ sml_mcp_access_tokens / sml_mcp_token_databases / sml_mcp_token_audit — โทเคน MCP แบบอ่านอย่างเดียว; หน้าจอจัดการ token อยู่ที่ หน้าจอผู้ดูแลระบบส่วนกลาง (Admin Console); ภาพรวมที่ ภาพรวมแค็ตตาล็อก API (API overview — วงจรคำขอ middleware รูปแบบ response และดัชนี).
เส้นทาง
| method path | auth | handler |
|---|---|---|
GET, POST /mcp |
Bearer token MCP (ไม่มี AdminAuth) | ServeMCP gateway.go:835 |
GET, POST /api/v1/mcp |
เหมือนข้างบน (ชื่อทางเลือก ผ่าน Next ใช้เส้นนี้) | ServeMCP |
GET /api/v1/mcp/tokens/ |
X-Admin-Api-Key |
ListTokens gateway.go:394 |
POST /api/v1/mcp/tokens/ |
X-Admin-Api-Key |
CreateToken gateway.go:490 |
POST /api/v1/mcp/tokens/{id}/revoke |
X-Admin-Api-Key |
RevokeToken gateway.go:682 |
ลงทะเบียนที่ router.go:360-371 (กลุ่ม tokens ใช้ AdminAuth(adminAPIKey); กลุ่ม /mcp ไม่มี middleware auth ของ router — gateway ตรวจ Bearer เองในตัว handler). Middleware global ที่ครอบทุกเส้น: ดู ภาพรวมแค็ตตาล็อก API (API overview — วงจรคำขอ middleware รูปแบบ response และดัชนี).
1. รูปแบบ token และการยืนยัน
- รูปแบบ:
smlmcp_ro_<base64url(provider)>.<id 32 hex>.<base64url(secret)>(token.go:101);ParseTokenจำกัดความยาว 220 และบังคับ prefixsmlmcp_ro_(token.go:27,112-117). โหมดเดียวที่มีคือreadonly(token.go:25). - รับ token จาก
Authorization: Bearer <token>หรือ headerX-MCP-Token(extractBearerOrHeaderTokengateway.go:822). Authenticate(gateway.go:254): แยก provider + id จาก token → ตรวจ cache ในหน่วยความจำauthCache(30 วินาที) ที่ผูกกับ hash+วันหมดอายุ → ถ้าไม่มีใน cache อ่านsml_mcp_access_tokens+sml_mcp_token_databasesเทียบ SHA-256 hash แบบ constant-time (verifyTokenHash). ทุกความล้มเหลวคืนErrDenied(ไม่บอกเหตุ). ถูกเพิกถอนแล้ว evict cache ทันที.- ชื่อฐานข้อมูลที่ token ใช้ได้ต้องตรง
^[a-z0-9_]{1,63}$(token.go:37); ฐานข้อมูลที่ขึ้นต้นด้วย prefix ของฐานระบบหลัก (ไม่ใช่ฐานบริษัท) ถูกปฏิเสธเสมอทั้งตอนใช้งานและตอนสร้าง token (gateway.go:170, token.go:241;DatabaseSelectiontoken.go:133).
2. POST /mcp — JSON-RPC
- Headers:
Authorization: Bearer <mcpToken>(หรือX-MCP-Token),Content-Type: application/json. - Body: JSON-RPC 2.0 เดี่ยว หรืออาร์เรย์ batch 1..20 รายการ. ขนาดสูงสุด 2 MB (
MaxBytesReadergateway.go:890). เวลารวมสูงสุด 35 วินาที (context timeout gateway.go:880). ตั้งCache-Control: no-store,X-Content-Type-Options: nosniff. - Notification (เมธอด
notifications/*ไม่มีid) → 202 ไม่มี body; batch ที่ไม่มี response เลย → 202. - ข้อผิดพลาดระดับ HTTP (ก่อนเข้า JSON-RPC):
| status | เหตุ | หมายเหตุ |
|---|---|---|
| 401 | ไม่ส่ง token / token ผิด หมดอายุ ถูกเพิกถอน / โหมดไม่ใช่ readonly | header WWW-Authenticate: Bearer realm=... + {"error":"invalid_token","message":"..."} (ข้อความไทย) |
| 405 | GET พร้อม Accept: text/event-stream (ไม่มี text/html), หรือ method อื่นที่ไม่ใช่ GET/POST |
Allow: POST หรือ Allow: POST, GET |
- ข้อผิดพลาดระดับ JSON-RPC (ตอบ HTTP 200 พร้อมอ็อบเจ็กต์
error):-32700body อ่านไม่ได้/ว่าง/JSON ผิด,-32600request ไม่ถูกต้อง หรือ batch ว่าง/เกิน 20 หรือมีข้อความต่อท้ายอีกก้อน,-32602พารามิเตอร์ผิด (เช่น ไม่มีuri, ไม่มีnameของ tool),-32601เมธอดไม่รู้จัก,-32603ข้อผิดพลาดภายใน.
เมธอดที่รองรับ (dispatchRPCRequest gateway.go:943)
| method | ผล | ที่มา |
|---|---|---|
initialize |
คืน protocolVersion (สะท้อนค่าที่ client ขอ), capabilities (tools/resources, listChanged=false), serverInfo, และ instructions ภาษาไทยอธิบายฐานข้อมูลที่ token เข้าถึงได้พร้อมตารางสำคัญ |
gateway.go:955 |
ping |
ตอบว่างเปล่า | :999 |
resources/list |
ทรัพยากร sml://<db>/overview ต่อฐานข้อมูลที่ได้รับอนุญาต |
:1006 |
resources/read |
พารามิเตอร์ uri; เรียก execCompanyOverview ของฐานนั้น |
:1022 |
tools/list |
รายการ tool + inputSchema + annotations (readOnlyHint ฯลฯ) |
:1055 |
tools/call |
params: {name, arguments} → ผลเป็น content:[{type:"text",text:"<JSON บีบอัดบรรทัดเดียว>"}], isError:false; ถ้า tool ล้มเหลวคืน isError:true พร้อมข้อความ error (ไม่ใช่ error ระดับ JSON-RPC) |
:1064-1103 |
ตัวอย่าง:
POST /mcp
Authorization: Bearer <mcpToken>
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"sml_query_readonly","arguments":{"database":"<dbName>","sql":"SELECT code, name_1 FROM ic_inventory","limit":20}}}
GET /mcp (ที่ไม่ได้ขอ SSE) คืน JSON อธิบายตัวเอง: ชื่อ sml-ai-account-mcp, เวอร์ชัน 1.1.0, protocolVersion 2025-03-26, โหมด readonly, วิธียืนยันตัวตน, รายการ optimizations และ tool catalog (gateway.go:845-863).
3. Tools (11 ตัว)
catalog ที่ readOnlyToolsCatalog (gateway.go:1142-1308), การส่งต่อที่ executeReadOnlyTool (case ที่ gateway.go:1484-1498).
| tool | input สำคัญ | ขีดจำกัด/พฤติกรรม |
|---|---|---|
sml_system_workflow_guide |
ไม่มี | คู่มือโครงสร้างตาราง, พจนานุกรม trans_flag, กติกาลง GL, คอลัมน์สถานะเอกสาร, มาตรฐาน UI |
sml_ui_workflow_spec |
screen (ไม่บังคับ) |
สเปก workflow หน้าจอ |
sml_list_databases |
ไม่มี | {mode, providerCode, authorizedDatabases} |
sml_company_overview |
database/databases |
ข้อมูลกิจการ (erp_company_profile), ค่าเริ่มต้นระบบ/ทศนิยม/ปีปฏิทิน (erp_option) และ KPI ภาพรวม |
sml_list_tables |
keyword, limit (100, สูงสุด 300), offset, format |
ค้นตารางพร้อมจำนวนคอลัมน์และจำนวนแถวโดยประมาณ |
sml_describe_table |
table หรือ tables, includeSamples |
โครงสร้างคอลัมน์ (และแถวตัวอย่าง) ทีละตารางหรือหลายตารางในครั้งเดียว |
sml_query_readonly |
sql, limit (100, สูงสุด 500), offset, format, omitEmpty |
SELECT/WITH เดียว; ห่อด้วย LIMIT n+1 OFFSET m เพื่อรู้ has_more |
sml_batch_readonly |
queries[] (สูงสุด 5, แต่ละอัน {key, sql, limit} limit 50 สูงสุด 200) |
รันใน snapshot read-only transaction เดียว |
sml_document_lookup |
docNo, keyword, custCode, transFlag, dateFrom, dateTo, limit (50, สูงสุด 200) |
ค้นเอกสารพร้อมรายละเอียด |
sml_financial_summary |
summaryType ∈ trial_balance, profit_loss, balance_sheet, ar_aging, ap_aging, vat_summary, stock_balance, top_products, cash_bank_flow, autocare_summary; dateFrom, dateTo, limit (100, สูงสุด 500) |
งบ/สรุปสำเร็จรูป |
sml_run_report |
menuKey (บังคับ), mode, dateFrom, dateTo, fromCode, toCode, keyword |
รันรายงานตามเมนู |
พารามิเตอร์ร่วม:
database(ฐานเดียว) หรือdatabases(หลายฐาน) — ห้ามส่งพร้อมกัน; สูงสุด 20 ฐานต่อครั้ง; ซ้ำ/ว่าง/นอก allow-list = error; ไม่ระบุใช้ฐานแรกที่ token อนุญาต (DatabaseSelectiontoken.go:133). หลายฐานรันขนานจำกัดพร้อมกัน 4 (gateway.go:1509-1515).format∈compact(ค่าเริ่มต้น, ตารางแบบ columns+rows),records,markdown(compact.go:9-30).omitEmpty(ค่าเริ่มต้น true) ตัดคอลัมน์ที่ว่าง/ศูนย์ทั้งคอลัมน์.
กติกาความปลอดภัยของ SQL
validateReadOnlySQL(gateway.go:1801) ใช้domainquery.NewStatement(domain/query/query.go:290): อนุญาต SELECT/WITH คำสั่งเดียว ห้าม DML/DDL และ pattern ต้องห้าม.forbiddenReadTables(gateway.go:30) บล็อกการอ่านตารางที่อ่อนไหว (บัญชีผู้ใช้/รหัสผ่าน, ตารางโทเค็น MCP, บัญชี Google และตารางรหัสผ่านของระบบ PostgreSQL).runReadOnlyQueryWithCols(gateway.go:1552): transactionReadOnly+ RepeatableRead +SET LOCAL statement_timeout='15s'.- ผลทั้งหมดถูก cache/ตัดแบบ compact เพื่อประหยัด token ของ AI (compact.go).
4. API จัดการ token (ใช้โดยหน้า Admin เท่านั้น)
ทุกเส้นต้องมี X-Admin-Api-Key: <adminKey> (API คอนโซลผู้ดูแล /api/v1/admin — provider, user, database, main-query, menu-permissions (Admin console API) อธิบาย AdminAuth) และเรียกผ่านชั้น Next ที่สร้างตัวตนผู้ใช้จาก session.
GET /api/v1/mcp/tokens/
- Response 200:
{"ok":true,"data":{"tokens":[...],"databases":[...],"tools":[...],"mode":"readonly"}}(gateway.go:394-). อ่านสูงสุด 200 token ที่ยังไม่ถูกเพิกถอน กรองตามผู้สร้างหรือขอบเขตฐานข้อมูลของผู้เรียก.
POST /api/v1/mcp/tokens/
- Body (จำกัด 32,768 ไบต์):
{provider, user_code|userCode, email, role, user_level, is_demo, name, mode, databaseCodes|database_codes[], days, expiresAt|expires_at}. - กฎ (
ValidateCreateInputtoken.go:224):name1-100 ตัวอักษร,modeต้องเป็นreadonly(ค่าเริ่มต้น), ฐานข้อมูล 1..50 รายการไม่ซ้ำ, วันหมดอายุต้องอยู่ในอนาคตและไม่เกิน 1 ปี + 1 วัน;daysค่าเริ่มต้น 90 สูงสุด 365; หรือส่งexpiresAtเป็น RFC3339. - Response 200:
{"ok":true,"data":{"token":"<แสดงครั้งเดียว>","metadata":{...}}}. บันทึกsml_mcp_access_tokens+sml_mcp_token_databases+sml_mcp_token_auditใน transaction เดียว (เก็บเฉพาะ hash ของ secret). - Errors: 400
READONLY_ONLY(mode ไม่ใช่ readonly), 400DB_OUTSIDE_SCOPE(ขอฐานนอกขอบเขตของผู้ออก), 400 validation, 403 (ErrDenied), 503 (DB ใช้ไม่ได้).
POST /api/v1/mcp/tokens/{id}/revoke
{id}ต้องตรง^[0-9a-f]{32}$(ไม่ตรง/ไม่พบ = 404). Body จำกัด 8,192 ไบต์.- เพิกถอนได้เมื่อเป็นผู้สร้าง,
SUPERADMIN, หรือ level >= 9 (ผู้ใช้ demo แตะได้เฉพาะ token demo) มิฉะนั้น 403. - Response 200:
{"ok":true,"data":{"id":"<id>","revoked":true}}; บันทึก audit actionrevokeและ evict cache.
ตัวอย่างสร้าง token (ผ่าน Go โดยตรง):
POST /api/v1/mcp/tokens/
X-Admin-Api-Key: <adminKey>
Content-Type: application/json
{"provider":"<provider>","user_code":"<userCode>","name":"<tokenName>","mode":"readonly","databaseCodes":["<dbName>"],"days":90}
5. ชั้น Next (BFF)
frontend/src/app/api/mcp/route.ts(GET/POST): ส่งผ่านไป/api/v1/mcpโดยส่งต่อเฉพาะ headerauthorization,x-mcp-token,content-type,accept,origin,mcp-protocol-version— ไม่ส่งต่อ cookie session. POST เกิน 2 MB = 413request_too_large; หมดเวลาต้นทาง 40 วินาที; ต้นทางล้ม = 503mcp_unavailable.frontend/src/app/api/mcp/tokens/route.ts: ตรวจ session (getSession(), ไม่มี = 401NO_SESSION) สร้างตัวตนจาก session แล้วเติมX-Admin-Api-Keyด้วยgetAdminHeaders(). GET = รายการ; POST{action:"revoke", id}= เพิกถอน, POST อื่น = สร้าง (ยอมรับเฉพาะreadonly,name1-100,databaseCodes,daysค่าเริ่มต้น 90).
ผู้เรียกฝั่งหน้าเว็บ
frontend/account/admin/screens/McpTokenScreen.tsx — รายการ :141, สร้าง :238, เพิกถอน :271, ทดสอบเชื่อมต่อด้วย fetch("/api/mcp") :370. ผู้เรียกจริงของ /mcp คือไคลเอนต์ AI ภายนอกที่ถือ token.
ข้อสังเกต
GET|POST /mcpไม่มี AdminAuth/JWT — ป้องกันด้วย token Bearer (ตั้งใจ).- ข้อผิดพลาด JSON-RPC ตอบ HTTP 200 — เครื่องมือเฝ้าระวังที่ดูเฉพาะ status code จะมองไม่เห็น.
- คำอธิบาย
trans_flagในsml_system_workflow_guideระบุ 32 เป็นใบสั่งขาย แต่ dashboard นับ 12 และ 32 เป็นยอดซื้อ (API โมดูลธุรกิจ /api/v1/{dashboard,crm,ai,wms,tms,procure,etax,market,autocare} (Business module endpoints)) — ควรตรวจก่อนใช้ตัวเลขร่วมกัน.
ที่มาในโค้ด
- backend/internal/adapter/inbound/http/router.go:360-371 (เส้นทาง MCP และ tokens)
- backend/internal/adapter/inbound/http/mcp/gateway.go:30 (forbiddenReadTables), :170 (ตัดฐานระบบหลัก), :845-863 (descriptor), :880 (timeout 35s), :890 (2 MB), :254 (Authenticate), :394 (ListTokens), :490 (CreateToken), :682 (RevokeToken), :822 (extractBearerOrHeaderToken), :835-940 (ServeMCP), :943-1105 (dispatchRPCRequest), :1142-1308 (readOnlyToolsCatalog), :1484-1498 (tool dispatch), :1509-1515 (concurrency 4), :1552 (runReadOnlyQueryWithCols), :1801 (validateReadOnlySQL)
- backend/internal/adapter/inbound/http/mcp/token.go:25-37 (mode, prefix, patterns), :101 (รูปแบบ token), :112 (ParseToken), :133 (DatabaseSelection), :224 (ValidateCreateInput)
- backend/internal/adapter/inbound/http/mcp/compact.go:9-30 (format)
- backend/internal/domain/query/query.go:290 (NewStatement)
- frontend/src/app/api/mcp/route.ts; frontend/src/app/api/mcp/tokens/route.ts; frontend/account/admin/screens/McpTokenScreen.tsx:141,238,271,370