Create, modify, and manage MCP tools for the Naki mahjong AI assistant. Use when adding new MCP tools, modifying existing tools, or fixing tool-related issues...
Base directory: {baseDir}
目前雀魂頁面是 Unity WebGL。遊戲狀態來自 Naki 的 Swift Liqi protocol state,遊戲/大廳/房間動作由 Swift 組 Liqi REQUEST。executeJavaScript 只適合唯讀 page/Unity canvas/Naki injection probe;不得拿來讀遊戲物件、送遊戲動作或模擬座標點擊。
新增或修改工具時必須維持三個邊界:
tools/list 由 MCPToolRegistry 自動生成,是名稱、描述與 schema 的 runtime truth。command/Services/MCP/
├── MCPTool.swift MCPKit re-export、NakiMCPTool、NakiUnsupported
├── MCPContext.swift NakiMCPContext + DefaultNakiMCPContext
├── MCPToolRegistry.swift registration order、tools/list definitions、execution
├── MCPHandler.swift MCP JSON-RPC handler
└── Tools/
├── SystemTools.swift status/help/logs
├── BotTools.swift bot snapshot/control + Liqi chi/pon
├── GameTools.swift protocol state + Liqi game actions
├── UITools.swift one read-only-capable JavaScript surface: execute_js
├── LobbyTools.swift lobby requests + Swift anti-idle scheduler
├── RoomTools.swift friend-room protocol flow
├── EmojiTools.swift Liqi broadcast send/capture
└── HighlightTools.swift six explicit compatibility failure stubs
command/Services/Bridge/
├── LiqiActionSender.swift LiqiRequestSpec/Builder, send outcome/result
├── LiqiOperationStore.swift current server-offered operation snapshots
└── LiqiResponseStore.swift responses and captured broadcasts
State and action flow:
WebSocket frames
→ Liqi parser / Majsoul bridge
→ Swift snapshot + operation/response stores
→ read tools
authorized MCP action
→ LiqiRequestBuilder / NakiGameAction
→ NakiMCPContext.sendLiqi
→ LiqiActionSender
→ correct gateway
→ RESPONSE and/or authoritative state transition
This is the 2026-08-01 source snapshot. Before runtime work, call live tools/list; do not assume this count remains current after code changes.
| Category | Count | Current names |
|---|---|---|
| System | 4 | get_status, get_help, get_logs, clear_logs |
| Bot | 7 | bot_status, bot_trigger, bot_ops, bot_deep, bot_chi, bot_pon, bot_sync |
| Game | 6 | game_state, game_hand, game_ops, game_discard, game_action, game_action_verify |
| JavaScript | 1 | execute_js |
| Lobby | 8 | lobby_status, lobby_match_modes, lobby_start_match, lobby_cancel_match, lobby_account_info, lobby_server_time, lobby_heartbeat, lobby_login_beat |
| Anti-idle | 1 | lobby_anti_idle |
| Friend room | 7 | room_create, room_add_robot, room_start, room_info, room_join, room_leave, room_quick_test |
| Emoji | 2 | game_emoji, game_emoji_listen |
The six highlight stubs (highlight_tile, reset_tile_color, highlight_status,
highlight_settings, show_recommendations, hide_highlight) were removed on 2026-08-02;
calling them returns Unknown tool. Naki's built-in WebGL recommendation highlighter is a
separate Swift-to-JavaScript path (WebViewModel.syncGameHighlight()), and there is no MCP
contract for manual highlighting. Do not re-add a stub that cannot do what its name says.
Define these facts first:
| Question | Required decision |
|---|---|
| What is the source? | Server RESPONSE, Swift protocol snapshot, operation store, response store, bot state, local logs, or read-only page probe |
| Is it a query or mutation? | Identify every account/game/file/log/connection side effect |
| Does a current tool already cover it? | Prefer extending a coherent tool over adding an alias |
| Which gateway/method applies? | Derive from current Liqi schema and existing builders; never guess from UI behavior |
| How is success proven? | Return/throw, server RESPONSE, operation sequence movement, or another explicit authoritative signal |
| What happens without runtime context? | Fail clearly; never fabricate an empty success |
Check git status before editing and preserve concurrent/user changes.
Add the tool to the category that owns its source and side effects. A tool that sends a lobby request belongs with lobby protocol tools; it does not become a UI tool merely because the result is visible on screen.
MCPToolstruct MyQueryTool: MCPTool {
static let name = "my_query"
static let description = "Read a clearly identified live Naki state source; no external mutation"
static let inputSchema = MCPInputSchema(
properties: [
"limit": .integer("Maximum number of records")
],
required: []
)
private let context: MCPContext
init(context: MCPContext) {
self.context = context
}
func execute(arguments: [String: Any]) async throws -> Any {
guard let nakiContext = context as? NakiMCPContext else {
throw MCPToolError.notAvailable("Naki context")
}
guard let snapshot = nakiContext.getGameSnapshot() else {
throw MCPToolError.notAvailable("game protocol snapshot")
}
return [
"success": true,
"source": "swift-protocol-layer",
"snapshot": snapshot
]
}
}
Use a unique snake_case name. Descriptions must say whether the tool is read-only, what it reads, and whether it sends or mutates anything.
static let inputSchema = MCPInputSchema.empty
static let inputSchema = MCPInputSchema(
properties: [
"text": .string("Meaning and accepted format"),
"count": .integer("Bounds and default"),
"ratio": .number("Bounds and default"),
"enabled": .boolean("Omitted means query-only")
],
required: ["text"]
)
NakiMCPContext currently exposes:
let bot = nakiContext.getBotStatus()
nakiContext.triggerAutoPlay()
let game = nakiContext.getGameSnapshot()
let outcome = await nakiContext.sendLiqi(spec, awaitResponseMs: 1500)
let antiIdle = nakiContext.setAntiIdle(enabled: nil, intervalSeconds: nil)
Base MCPContext also provides server port, logs and executeJavaScript. Use JavaScript only for read-only page/Unity/injection diagnostics, and include return when a value is needed:
let script = "return JSON.stringify({url:location.href,canvas:!!document.getElementById('unity-canvas')})"
let result = try await context.executeJavaScript(script)
Do not create a game-state or action tool around JavaScript.
For a request tool:
LiqiRequestBuilder; game action parsing belongs in NakiGameAction when applicable.LiqiOperationStore before an action whose legality/type/combination depends on the server-provided oplist.nakiContext.sendLiqi(spec, awaitResponseMs:).LiqiToolResult.dictionary(outcome, spec:extra:) so method, payload, msgId, send status and RESPONSE semantics stay consistent.let spec = LiqiRequestBuilder.fetchServerTime()
let outcome = await nakiContext.sendLiqi(spec, awaitResponseMs: 1500)
return LiqiToolResult.dictionary(outcome, spec: spec)
sent.success == true proves only that bytes were accepted by an open WebSocket. It is not server acceptance. Preserve response, serverAccepted, timeout and verification fields rather than flattening them into an unconditional success.
Never manually assemble raw WebSocket JavaScript in an MCP action tool; routing, message IDs, encoding and response correlation belong to the shared Liqi sender.
Use MCPToolError for invalid input or missing execution context:
throw MCPToolError.missingParameter("name")
throw MCPToolError.invalidParameter("name", expected: "documented format")
throw MCPToolError.notAvailable("required runtime source")
For a retained compatibility contract with no current implementation, use the shared failure shape:
return NakiUnsupported.result(
reason: "specific missing current surface",
alternative: "current supported route"
)
Failure must contain success: false as a Bool plus machine-readable error and human-readable reason. Never place a failure dictionary inside a truthy success field.
Add the type to the correct ordered section of MCPToolRegistry.registerBuiltInTools():
register(MyQueryTool.self)
tools/list definitions are generated from the registry; do not maintain a separate JSON list. If the registered set changes, update all explicit counts/category docs/help/tests in the same change and verify the live registry after launching the new build.
At minimum, cover the paths relevant to the tool:
NakiMCPContext or unconfigured callbacks.Project commands:
xcodebuild build -project Naki.xcodeproj -scheme Naki
xcodebuild test -project Naki.xcodeproj -scheme Naki -only-testing:NakiTests
Inspect actual command output, test results and git diff; an exit code alone is not enough when the environment is dirty.
Runtime verification is a separate gate after the new build is running:
/naki-mcp-proxy
→ live tools/list
→ confirm the new/current schema
→ call the live tool
→ inspect the returned source, send, RESPONSE and verification fields
→ re-query relevant state when the tool mutates anything
return.tools/list and an actual tool call, or clearly marked unverified.git status/diff contains only authorized changes.command/Services/MCP/MCPTool.swiftcommand/Services/MCP/MCPContext.swiftcommand/Services/MCP/MCPToolRegistry.swiftcommand/Services/MCP/Tools/command/Services/Bridge/LiqiActionSender.swiftcommand/Services/Bridge/LiqiOperationStore.swiftcommand/Services/Bridge/LiqiResponseStore.swift.claude/skills/naki-mcp-proxy/SKILL.mddocs/mcp-server-guide.mddocs/majsoul-unity-protocol.md