@lemonbeijing/lemonclaw-mcp-service 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # LemonClaw MCP Service
2
+
3
+ Local STDIO MCP adapter for the existing LemonClaw CLI and portable Runtime. It does not copy business actions or move authentication/account state: the MCP process runs as the current operating-system user and reuses the same Runtime, token store, account context, documents, downloads, and local invoice files.
4
+
5
+ Requires Node.js 20 or newer. The packaged LemonClaw CLI dependency installs and updates the portable Runtime through its existing launcher lifecycle.
6
+
7
+ ## Run from this repository
8
+
9
+ ```text
10
+ npm install
11
+ node bin/lemonclaw-mcp.js
12
+ ```
13
+
14
+ Example MCP host configuration:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "lemonclaw": {
20
+ "type": "stdio",
21
+ "command": "node",
22
+ "args": ["D:/VSProject/lemonscmskills/mcp-service/bin/lemonclaw-mcp.js"]
23
+ }
24
+ }
25
+ }
26
+ ```
27
+
28
+ After publication, the command can be changed to `npx -y @lemonbeijing/lemonclaw-mcp-service`.
29
+
30
+ ## Tool groups
31
+
32
+ - Authentication: `lemon_auth_status`, `lemon_auth_save_api_key`, `lemon_auth_clear`
33
+ - Account context: `lemon_account_current`, `lemon_account_list`, `lemon_account_switch`, `lemon_account_select`, `lemon_account_clear`
34
+ - ACC/SCM/ERP actions: `lemon_action_search`, `lemon_action_show`, `lemon_action_read_doc`, `lemon_action_run`
35
+ - Independent invoicing: `lemon_invoice_read_doc`, `lemon_invoice_api`, `lemon_invoice_run_helper`
36
+
37
+ `stdout` is reserved for MCP JSON-RPC. CLI and Runtime output is captured and returned as MCP Tool content; service diagnostics use `stderr` only.
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../src/index.js";
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@lemonbeijing/lemonclaw-mcp-service",
3
+ "version": "0.1.1",
4
+ "description": "Local stdio MCP adapter for LemonClaw CLI and Runtime.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "bin": {
11
+ "lemonclaw-mcp": "bin/lemonclaw-mcp.js"
12
+ },
13
+ "scripts": {
14
+ "test": "node --test test/*.test.js",
15
+ "start": "node bin/lemonclaw-mcp.js"
16
+ },
17
+ "dependencies": {
18
+ "@lemonbeijing/lemonclaw-cli": "1.0.5",
19
+ "@modelcontextprotocol/server": "2.0.0",
20
+ "zod": "4.4.3"
21
+ },
22
+ "devDependencies": {
23
+ "@modelcontextprotocol/client": "2.0.0"
24
+ },
25
+ "files": [
26
+ "bin/",
27
+ "src/",
28
+ "README.md"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ }
34
+ }
@@ -0,0 +1,29 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+
4
+ import { runProcess } from "./process-runner.js";
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ export function resolveCliLauncher(env = process.env) {
9
+ const override = String(env.LEMONCLAW_CLI_LAUNCHER || "").trim();
10
+ if (override) return path.resolve(override);
11
+
12
+ const packageJson = require.resolve("@lemonbeijing/lemonclaw-cli/package.json");
13
+ return path.join(path.dirname(packageJson), "dist", "lemonclaw-cli.cjs");
14
+ }
15
+
16
+ export function createCliRunner(options = {}) {
17
+ const launcherPath = options.launcherPath || resolveCliLauncher(options.env || process.env);
18
+ const nodeExecutable = options.nodeExecutable || process.execPath;
19
+ const baseEnv = options.env || process.env;
20
+
21
+ return async function runCli(args, runOptions = {}) {
22
+ return runProcess(nodeExecutable, [launcherPath, ...args], {
23
+ stdin: runOptions.stdin,
24
+ timeoutMs: runOptions.timeoutMs,
25
+ maxOutputBytes: runOptions.maxOutputBytes,
26
+ env: runOptions.env || baseEnv,
27
+ });
28
+ };
29
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
2
+
3
+ import { createLemonClawServer } from "./server.js";
4
+
5
+ serveStdio(() => createLemonClawServer());
@@ -0,0 +1,156 @@
1
+ import { mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { runProcess } from "./process-runner.js";
6
+ import {
7
+ isPathWithin,
8
+ requireFile,
9
+ resolveRuntimeEnvironment,
10
+ } from "./runtime-environment.js";
11
+
12
+ const INVOICE_HELPERS = Object.freeze({
13
+ customer_service_link: "scripts/customer_service_link.py",
14
+ preview_server: "scripts/preview_http_server.py",
15
+ query_customer: "scripts/query_customer.py",
16
+ read_excel_simple: "scripts/read_excel_simple.py",
17
+ read_excel_utf8: "scripts/read_excel_utf8.py",
18
+ render_preview: "scripts/render_invoice_preview_switchable.py",
19
+ validate_json: "scripts/validate_json.py",
20
+ });
21
+
22
+ const API_ARGUMENT_FLAGS = Object.freeze({
23
+ eiscid: "--eiscid",
24
+ filter_word: "--filter-word",
25
+ company_name: "--company-name",
26
+ company_id: "--company-id",
27
+ personal_account: "--personal-account",
28
+ uscic: "--uscic",
29
+ auth_company_id: "--auth-company-id",
30
+ company_detail_id: "--company-detail-id",
31
+ api_key_last_four: "--api-key-last-four",
32
+ ai_tool: "--ai-tool",
33
+ model: "--model",
34
+ feedback_contact: "--feedback-contact",
35
+ feedback_version: "--feedback-version",
36
+ feedback_type: "--feedback-type",
37
+ feedback_files: "--feedback-files",
38
+ feedback_text: "--feedback-text",
39
+ message_id: "--message-id",
40
+ context: "--context",
41
+ upload_file_path: "--upload-file-path",
42
+ });
43
+
44
+ export async function resolveInvoiceRuntime(runCli) {
45
+ const runtime = await resolveRuntimeEnvironment(runCli);
46
+ const invoiceRoot = path.join(runtime.skillRoot, "invoice");
47
+ const invoiceInfo = await stat(invoiceRoot).catch(() => null);
48
+ if (!invoiceInfo?.isDirectory()) {
49
+ throw new Error("Installed Runtime does not contain the invoice workflow.");
50
+ }
51
+
52
+ return {
53
+ ...runtime,
54
+ invoiceRoot,
55
+ };
56
+ }
57
+
58
+ export async function readInvoiceDocument(runCli, relativePath) {
59
+ const runtime = await resolveInvoiceRuntime(runCli);
60
+ const requested = String(relativePath || "").replaceAll("\\", "/");
61
+ if (!requested || path.isAbsolute(requested) || requested.split("/").includes("..")) {
62
+ throw new Error("Invoice document path must be relative to the invoice directory.");
63
+ }
64
+ if (![".md", ".json", ".txt"].includes(path.extname(requested).toLowerCase())) {
65
+ throw new Error("Invoice document must be Markdown, JSON, or text.");
66
+ }
67
+
68
+ const rootReal = await realpath(runtime.invoiceRoot);
69
+ const targetReal = await realpath(path.join(runtime.invoiceRoot, requested));
70
+ if (!isPathWithin(rootReal, targetReal)) {
71
+ throw new Error("Invoice document path escapes the invoice directory.");
72
+ }
73
+ const content = await readFile(targetReal, "utf8");
74
+ return content.replace(/^\uFEFF/, "");
75
+ }
76
+
77
+ function requireValue(args, key, operation) {
78
+ const value = args[key];
79
+ if (value === undefined || value === null || String(value).trim() === "") {
80
+ throw new Error(`${operation} requires ${key}.`);
81
+ }
82
+ }
83
+
84
+ function appendApiArguments(command, args) {
85
+ for (const [key, flag] of Object.entries(API_ARGUMENT_FLAGS)) {
86
+ const value = args[key];
87
+ if (value !== undefined && value !== null && String(value) !== "") {
88
+ command.push(flag, String(value));
89
+ }
90
+ }
91
+ }
92
+
93
+ async function withPayloadPath(args, callback) {
94
+ if (args.payload !== undefined && args.payload_path) {
95
+ throw new Error("Use either payload or payload_path, not both.");
96
+ }
97
+ if (args.payload === undefined) return callback(args.payload_path || null);
98
+
99
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "lemonclaw-mcp-"));
100
+ const payloadPath = path.join(tempRoot, "payload.json");
101
+ try {
102
+ await writeFile(payloadPath, JSON.stringify(args.payload), "utf8");
103
+ return await callback(payloadPath);
104
+ } finally {
105
+ await rm(tempRoot, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ export async function runInvoiceApi(runCli, args) {
110
+ const runtime = await resolveInvoiceRuntime(runCli);
111
+ const script = path.join(runtime.invoiceRoot, "assets", "lemon_acc_api.py");
112
+ await requireFile(script, "Installed Runtime invoice API script is missing.");
113
+
114
+ const operation = String(args.operation || "");
115
+ if (operation === "SkillInvoiceIssue") {
116
+ if (args.user_confirmed !== true) {
117
+ throw new Error("SkillInvoiceIssue requires explicit user confirmation.");
118
+ }
119
+ for (const key of ["eiscid", "api_key_last_four", "ai_tool", "model"]) {
120
+ requireValue(args, key, operation);
121
+ }
122
+ if (args.payload === undefined && !args.payload_path) {
123
+ throw new Error("SkillInvoiceIssue requires payload or payload_path.");
124
+ }
125
+ }
126
+
127
+ return withPayloadPath(args, async (payloadPath) => {
128
+ const command = [script, "--action", operation];
129
+ appendApiArguments(command, args);
130
+ if (payloadPath) command.push("--payload-path", String(payloadPath));
131
+ if (operation === "SkillInvoiceIssue") command.push("--issue-confirm", "1");
132
+ return runProcess(runtime.python, command, {
133
+ cwd: runtime.runtimeRoot,
134
+ env: runtime.env,
135
+ timeoutMs: Number(args.timeout_seconds || 120) * 1000,
136
+ });
137
+ });
138
+ }
139
+
140
+ export async function runInvoiceHelper(runCli, args) {
141
+ const runtime = await resolveInvoiceRuntime(runCli);
142
+ const relativeScript = INVOICE_HELPERS[String(args.operation || "")];
143
+ if (!relativeScript) throw new Error("Unsupported invoice helper operation.");
144
+
145
+ const script = path.join(runtime.invoiceRoot, relativeScript);
146
+ await requireFile(script, "Installed Runtime invoice helper script is missing.");
147
+ const commandArgs = Array.isArray(args.arguments) ? args.arguments.map(String) : [];
148
+ return runProcess(runtime.python, [script, ...commandArgs], {
149
+ cwd: runtime.runtimeRoot,
150
+ env: runtime.env,
151
+ stdin: args.stdin,
152
+ timeoutMs: Number(args.timeout_seconds || 120) * 1000,
153
+ });
154
+ }
155
+
156
+ export const invoiceHelperNames = Object.freeze(Object.keys(INVOICE_HELPERS));
@@ -0,0 +1,86 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ // Stay below the common 10 MiB STDIO message limit after JSON-RPC encoding.
4
+ const DEFAULT_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
5
+
6
+ export function runProcess(command, args, options = {}) {
7
+ const timeoutMs = Math.max(1, Number(options.timeoutMs || 120_000));
8
+ const maxOutputBytes = Math.max(
9
+ 1024,
10
+ Number(options.maxOutputBytes || DEFAULT_MAX_OUTPUT_BYTES),
11
+ );
12
+
13
+ return new Promise((resolve) => {
14
+ const child = spawn(command, args, {
15
+ cwd: options.cwd,
16
+ env: options.env || process.env,
17
+ shell: false,
18
+ stdio: ["pipe", "pipe", "pipe"],
19
+ windowsHide: true,
20
+ });
21
+
22
+ const stdout = [];
23
+ const stderr = [];
24
+ let stdoutBytes = 0;
25
+ let stderrBytes = 0;
26
+ let spawnError;
27
+ let timedOut = false;
28
+ let outputLimitExceeded = false;
29
+ let settled = false;
30
+
31
+ const stop = () => {
32
+ if (!child.killed) {
33
+ child.kill("SIGTERM");
34
+ }
35
+ };
36
+
37
+ const timer = setTimeout(() => {
38
+ timedOut = true;
39
+ stop();
40
+ }, timeoutMs);
41
+
42
+ child.stdout.on("data", (chunk) => {
43
+ stdoutBytes += chunk.length;
44
+ if (stdoutBytes <= maxOutputBytes) {
45
+ stdout.push(chunk);
46
+ } else {
47
+ outputLimitExceeded = true;
48
+ stop();
49
+ }
50
+ });
51
+
52
+ child.stderr.on("data", (chunk) => {
53
+ stderrBytes += chunk.length;
54
+ if (stderrBytes <= maxOutputBytes) {
55
+ stderr.push(chunk);
56
+ } else {
57
+ outputLimitExceeded = true;
58
+ stop();
59
+ }
60
+ });
61
+
62
+ child.once("error", (error) => {
63
+ spawnError = error;
64
+ });
65
+
66
+ child.once("close", (code, signal) => {
67
+ if (settled) return;
68
+ settled = true;
69
+ clearTimeout(timer);
70
+ resolve({
71
+ code: Number.isInteger(code) ? code : 1,
72
+ signal: signal || null,
73
+ stdout: Buffer.concat(stdout).toString("utf8"),
74
+ stderr: Buffer.concat(stderr).toString("utf8"),
75
+ timedOut,
76
+ outputLimitExceeded,
77
+ spawnError,
78
+ });
79
+ });
80
+
81
+ child.stdin.on("error", () => {
82
+ // The child may reject input and exit before stdin is fully written.
83
+ });
84
+ child.stdin.end(options.stdin === undefined ? "" : String(options.stdin));
85
+ });
86
+ }
@@ -0,0 +1,43 @@
1
+ import { runProcess } from "./process-runner.js";
2
+ import { resolveRuntimeEnvironment } from "./runtime-environment.js";
3
+
4
+ const AUTH_PROGRAM = String.raw`
5
+ import json
6
+ import sys
7
+
8
+ operation = sys.argv[1]
9
+ skill_root = sys.argv[2]
10
+ sys.path.insert(0, skill_root)
11
+ from auth import auth as lemon_auth
12
+
13
+ if operation == "status":
14
+ result = lemon_auth.auth_status()
15
+ elif operation == "save_api_key":
16
+ result = lemon_auth.save_api_key(sys.stdin.read())
17
+ elif operation == "clear":
18
+ result = lemon_auth.clear_auth()
19
+ else:
20
+ raise SystemExit("unsupported auth operation")
21
+
22
+ print(json.dumps(result, ensure_ascii=False, separators=(",", ":")))
23
+ `;
24
+
25
+ export async function runRuntimeAuth(runCli, args, options = {}) {
26
+ const resolveRuntime = options.resolveRuntime || resolveRuntimeEnvironment;
27
+ const run = options.run || runProcess;
28
+ const runtime = await resolveRuntime(runCli);
29
+ const operation = String(args.operation || "");
30
+ if (!["status", "save_api_key", "clear"].includes(operation)) {
31
+ throw new Error("Unsupported Runtime authentication operation.");
32
+ }
33
+ if (operation === "save_api_key" && !String(args.api_key || "").trim()) {
34
+ throw new Error("API Key is required.");
35
+ }
36
+
37
+ return run(runtime.python, ["-c", AUTH_PROGRAM, operation, runtime.skillRoot], {
38
+ cwd: runtime.runtimeRoot,
39
+ env: runtime.env,
40
+ stdin: operation === "save_api_key" ? String(args.api_key) : undefined,
41
+ timeoutMs: Math.max(1, Number(args.timeout_seconds || 60)) * 1000,
42
+ });
43
+ }
@@ -0,0 +1,51 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ export function isPathWithin(root, candidate) {
5
+ const resolvedRoot = path.resolve(root);
6
+ const resolvedCandidate = path.resolve(candidate);
7
+ const normalize = (value) => (process.platform === "win32" ? value.toLowerCase() : value);
8
+ return normalize(resolvedCandidate).startsWith(normalize(`${resolvedRoot}${path.sep}`));
9
+ }
10
+
11
+ export async function requireFile(filePath, message) {
12
+ const info = await stat(filePath).catch(() => null);
13
+ if (!info?.isFile()) throw new Error(message);
14
+ }
15
+
16
+ export async function resolveRuntimeEnvironment(runCli) {
17
+ const discovery = await runCli(["skill-root", "--check"], { timeoutMs: 120_000 });
18
+ if (discovery.code !== 0 || discovery.timedOut || discovery.spawnError) {
19
+ const detail = String(discovery.stdout || discovery.stderr || "runtime discovery failed").trim();
20
+ throw new Error(detail.slice(0, 2000));
21
+ }
22
+
23
+ const skillRoot = path.resolve(String(discovery.stdout || "").trim());
24
+ const runtimeRoot = path.resolve(skillRoot, "..", "..", "..");
25
+ const metadataPath = path.join(runtimeRoot, "runtime.json");
26
+ const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
27
+ const pythonRelative = String(metadata.pythonPath || "").replaceAll("/", path.sep);
28
+ const python = path.resolve(runtimeRoot, pythonRelative);
29
+
30
+ if (!isPathWithin(runtimeRoot, python)) {
31
+ throw new Error("Installed Runtime Python path escapes the Runtime directory.");
32
+ }
33
+ await requireFile(python, "Installed Runtime Python executable is missing.");
34
+
35
+ const appRoot = path.join(runtimeRoot, "app");
36
+ return {
37
+ skillRoot,
38
+ runtimeRoot,
39
+ python,
40
+ env: {
41
+ ...process.env,
42
+ PYTHONUTF8: "1",
43
+ PYTHONIOENCODING: "utf-8",
44
+ PYTHONNOUSERSITE: "1",
45
+ PYTHONDONTWRITEBYTECODE: "1",
46
+ PYTHONPATH: appRoot,
47
+ SSL_CERT_FILE: path.join(appRoot, "certifi", "cacert.pem"),
48
+ LEMONCLAW_RUNTIME_ROOT: runtimeRoot,
49
+ },
50
+ };
51
+ }
package/src/server.js ADDED
@@ -0,0 +1,516 @@
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
+ import * as z from "zod/v4";
3
+
4
+ import { createCliRunner } from "./cli-runner.js";
5
+ import {
6
+ invoiceHelperNames,
7
+ readInvoiceDocument,
8
+ runInvoiceApi,
9
+ runInvoiceHelper,
10
+ } from "./invoice-runtime.js";
11
+ import { runRuntimeAuth } from "./runtime-auth.js";
12
+ import { cliToolResult, toolError } from "./tool-result.js";
13
+
14
+ const productSchema = z.enum(["acc", "scm", "erp"]);
15
+ const timeoutSchema = z.number().int().min(1).max(600).default(120);
16
+ const optionalText = z.string().min(1).optional();
17
+
18
+ const readOnlyAnnotations = {
19
+ readOnlyHint: true,
20
+ destructiveHint: false,
21
+ idempotentHint: true,
22
+ openWorldHint: true,
23
+ };
24
+
25
+ function commandTimeout(seconds) {
26
+ return (Number(seconds || 120) + 15) * 1000;
27
+ }
28
+
29
+ function addFlag(command, flag, value) {
30
+ if (value !== undefined && value !== null && String(value) !== "") {
31
+ command.push(flag, String(value));
32
+ }
33
+ }
34
+
35
+ function registerCliTool(server, runCli, name, config, build) {
36
+ server.registerTool(name, config, async (args) => {
37
+ try {
38
+ const invocation = build(args);
39
+ const result = await runCli(invocation.command, {
40
+ stdin: invocation.stdin,
41
+ timeoutMs: invocation.timeoutMs,
42
+ });
43
+ return cliToolResult(result, invocation.resultOptions);
44
+ } catch (error) {
45
+ return toolError(error);
46
+ }
47
+ });
48
+ }
49
+
50
+ export function createLemonClawServer(options = {}) {
51
+ const runCli = options.runCli || createCliRunner(options.cliOptions);
52
+ const server = new McpServer(
53
+ { name: "lemonclaw", version: "0.1.1" },
54
+ {
55
+ instructions:
56
+ "Use standard API Key authentication: check lemon_auth_status and, when unavailable, request the user's API Key and pass it only to lemon_auth_save_api_key. Never use browser or device-flow authentication. For ACC, SCM, and ERP, determine the product and account first, then use lemon_action_search, lemon_action_show, read every document exposed by show with lemon_action_read_doc, and only then call lemon_action_run. Never guess action parameters. Independent invoicing uses only the lemon_invoice_* tools and requires its Runtime documents.",
57
+ },
58
+ );
59
+
60
+ server.registerTool(
61
+ "lemon_auth_status",
62
+ {
63
+ title: "Check LemonClaw API Key authentication",
64
+ description:
65
+ "Check whether the current user's standard LemonClaw API Key can provide a token. This uses the Runtime authentication helper and never returns the API Key or token.",
66
+ inputSchema: z.object({
67
+ timeout_seconds: z.number().int().min(1).max(120).default(60),
68
+ }),
69
+ annotations: readOnlyAnnotations,
70
+ },
71
+ async (args) => {
72
+ try {
73
+ return cliToolResult(await runRuntimeAuth(runCli, { ...args, operation: "status" }));
74
+ } catch (error) {
75
+ return toolError(error);
76
+ }
77
+ },
78
+ );
79
+
80
+ server.registerTool(
81
+ "lemon_auth_save_api_key",
82
+ {
83
+ title: "Save LemonClaw API Key",
84
+ description:
85
+ "Save an API Key with the standard Runtime authentication helper for the current operating-system user. The key is sent to Runtime through stdin, is not placed in process arguments, and is never returned.",
86
+ inputSchema: z.object({
87
+ api_key: z.string().min(1),
88
+ timeout_seconds: z.number().int().min(1).max(120).default(60),
89
+ }),
90
+ annotations: {
91
+ readOnlyHint: false,
92
+ destructiveHint: false,
93
+ idempotentHint: false,
94
+ openWorldHint: false,
95
+ },
96
+ },
97
+ async (args) => {
98
+ try {
99
+ return cliToolResult(
100
+ await runRuntimeAuth(runCli, { ...args, operation: "save_api_key" }),
101
+ );
102
+ } catch (error) {
103
+ return toolError(error);
104
+ }
105
+ },
106
+ );
107
+
108
+ server.registerTool(
109
+ "lemon_auth_clear",
110
+ {
111
+ title: "Clear LemonClaw API Key authentication",
112
+ description:
113
+ "Clear the current user's standard LemonClaw API Key and token cache without clearing account context. Use only when the user explicitly requests it.",
114
+ inputSchema: z.object({
115
+ timeout_seconds: z.number().int().min(1).max(120).default(60),
116
+ }),
117
+ annotations: {
118
+ readOnlyHint: false,
119
+ destructiveHint: true,
120
+ idempotentHint: true,
121
+ openWorldHint: false,
122
+ },
123
+ },
124
+ async (args) => {
125
+ try {
126
+ return cliToolResult(await runRuntimeAuth(runCli, { ...args, operation: "clear" }));
127
+ } catch (error) {
128
+ return toolError(error);
129
+ }
130
+ },
131
+ );
132
+
133
+ registerCliTool(
134
+ server,
135
+ runCli,
136
+ "lemon_account_current",
137
+ {
138
+ title: "Get current Lemon account context",
139
+ description:
140
+ "Read the current product and selected account set from the existing local account context. Call this before ACC, SCM, or ERP business actions.",
141
+ inputSchema: z.object({ timeout_seconds: timeoutSchema }),
142
+ annotations: readOnlyAnnotations,
143
+ },
144
+ (args) => ({
145
+ command: ["account", "current", "--timeout", String(args.timeout_seconds)],
146
+ timeoutMs: commandTimeout(args.timeout_seconds),
147
+ }),
148
+ );
149
+
150
+ registerCliTool(
151
+ server,
152
+ runCli,
153
+ "lemon_account_list",
154
+ {
155
+ title: "List Lemon account sets",
156
+ description:
157
+ "List account-set candidates and cache their returned indexes for a later lemon_account_select call. Use only when switch has no usable candidates or the user asks to list accounts.",
158
+ inputSchema: z.object({
159
+ dry_run: z.boolean().default(false),
160
+ timeout_seconds: timeoutSchema,
161
+ }),
162
+ annotations: readOnlyAnnotations,
163
+ },
164
+ (args) => {
165
+ const command = ["account", "list", "--timeout", String(args.timeout_seconds)];
166
+ if (args.dry_run) command.push("--dry-run");
167
+ return { command, timeoutMs: commandTimeout(args.timeout_seconds) };
168
+ },
169
+ );
170
+
171
+ registerCliTool(
172
+ server,
173
+ runCli,
174
+ "lemon_account_switch",
175
+ {
176
+ title: "Switch Lemon account context",
177
+ description:
178
+ "Match a product or account clue and switch when unique. If candidates are returned, present them to the user and select from that same candidate set; do not refresh it with account list.",
179
+ inputSchema: z.object({
180
+ query: optionalText,
181
+ product: productSchema.optional(),
182
+ asid: optionalText,
183
+ serviceid: optionalText,
184
+ name: optionalText,
185
+ edition: optionalText,
186
+ dry_run: z.boolean().default(false),
187
+ timeout_seconds: timeoutSchema,
188
+ }),
189
+ annotations: {
190
+ readOnlyHint: false,
191
+ destructiveHint: false,
192
+ idempotentHint: false,
193
+ openWorldHint: true,
194
+ },
195
+ },
196
+ (args) => {
197
+ const command = ["account", "switch"];
198
+ if (args.query) command.push(args.query);
199
+ addFlag(command, "--product", args.product);
200
+ addFlag(command, "--asid", args.asid);
201
+ addFlag(command, "--serviceid", args.serviceid);
202
+ addFlag(command, "--name", args.name);
203
+ addFlag(command, "--edition", args.edition);
204
+ if (args.dry_run) command.push("--dry-run");
205
+ command.push("--timeout", String(args.timeout_seconds));
206
+ return { command, timeoutMs: commandTimeout(args.timeout_seconds) };
207
+ },
208
+ );
209
+
210
+ registerCliTool(
211
+ server,
212
+ runCli,
213
+ "lemon_account_select",
214
+ {
215
+ title: "Select a Lemon account set",
216
+ description:
217
+ "Select from the candidates cached by the most recent account switch or list. Prefer the returned index. Never invent an index or silently choose between duplicate names.",
218
+ inputSchema: z
219
+ .object({
220
+ index: z.number().int().positive().optional(),
221
+ product: productSchema.optional(),
222
+ asid: optionalText,
223
+ serviceid: optionalText,
224
+ name: optionalText,
225
+ timeout_seconds: timeoutSchema,
226
+ })
227
+ .refine(
228
+ (value) => value.index !== undefined || value.asid || value.serviceid || value.name,
229
+ "Provide index, asid, serviceid, or name from the latest candidate set.",
230
+ ),
231
+ annotations: {
232
+ readOnlyHint: false,
233
+ destructiveHint: false,
234
+ idempotentHint: false,
235
+ openWorldHint: false,
236
+ },
237
+ },
238
+ (args) => {
239
+ const command = ["account", "select"];
240
+ addFlag(command, "--index", args.index);
241
+ addFlag(command, "--product", args.product);
242
+ addFlag(command, "--asid", args.asid);
243
+ addFlag(command, "--serviceid", args.serviceid);
244
+ addFlag(command, "--name", args.name);
245
+ command.push("--timeout", String(args.timeout_seconds));
246
+ return { command, timeoutMs: commandTimeout(args.timeout_seconds) };
247
+ },
248
+ );
249
+
250
+ registerCliTool(
251
+ server,
252
+ runCli,
253
+ "lemon_account_clear",
254
+ {
255
+ title: "Clear Lemon account context",
256
+ description:
257
+ "Clear saved account context without clearing authentication. Use all_products only when the user explicitly requests clearing every product context.",
258
+ inputSchema: z.object({
259
+ product: productSchema.optional(),
260
+ all_products: z.boolean().default(false),
261
+ timeout_seconds: timeoutSchema,
262
+ }),
263
+ annotations: {
264
+ readOnlyHint: false,
265
+ destructiveHint: true,
266
+ idempotentHint: true,
267
+ openWorldHint: false,
268
+ },
269
+ },
270
+ (args) => {
271
+ const command = ["account", "clear"];
272
+ addFlag(command, "--product", args.product);
273
+ if (args.all_products) command.push("--all");
274
+ command.push("--timeout", String(args.timeout_seconds));
275
+ return { command, timeoutMs: commandTimeout(args.timeout_seconds) };
276
+ },
277
+ );
278
+
279
+ registerCliTool(
280
+ server,
281
+ runCli,
282
+ "lemon_action_search",
283
+ {
284
+ title: "Search Lemon business actions",
285
+ description:
286
+ "Search for ACC, SCM, or ERP action candidates in one explicit product. Search does not select an action, create parameters, or execute business requests.",
287
+ inputSchema: z.object({
288
+ product: productSchema,
289
+ query: z.string().min(1),
290
+ limit: z.number().int().min(1).max(20).default(5),
291
+ }),
292
+ annotations: readOnlyAnnotations,
293
+ },
294
+ (args) => ({
295
+ command: [
296
+ "action",
297
+ "search",
298
+ args.query,
299
+ "--product",
300
+ args.product,
301
+ "--limit",
302
+ String(args.limit),
303
+ ],
304
+ timeoutMs: 30_000,
305
+ }),
306
+ );
307
+
308
+ registerCliTool(
309
+ server,
310
+ runCli,
311
+ "lemon_action_show",
312
+ {
313
+ title: "Show a Lemon business action",
314
+ description:
315
+ "Show one action's public contract and the documents that actually exist. A successful show does not prove the action is the unique semantic match.",
316
+ inputSchema: z.object({
317
+ product: productSchema,
318
+ action: z.string().min(1),
319
+ }),
320
+ annotations: readOnlyAnnotations,
321
+ },
322
+ (args) => ({
323
+ command: ["action", "show", args.product, args.action],
324
+ timeoutMs: 30_000,
325
+ }),
326
+ );
327
+
328
+ registerCliTool(
329
+ server,
330
+ runCli,
331
+ "lemon_action_read_doc",
332
+ {
333
+ title: "Read a Lemon business action document",
334
+ description:
335
+ "Read one registered ACTION, input, or output Markdown document. Read every document exposed by lemon_action_show for the final selected action before run.",
336
+ inputSchema: z.object({
337
+ product: productSchema,
338
+ action: z.string().min(1),
339
+ document: z.enum(["action", "input", "output"]),
340
+ }),
341
+ annotations: readOnlyAnnotations,
342
+ },
343
+ (args) => ({
344
+ command: [
345
+ "action",
346
+ "doc",
347
+ args.product,
348
+ args.action,
349
+ "--doc",
350
+ args.document,
351
+ ],
352
+ timeoutMs: 30_000,
353
+ }),
354
+ );
355
+
356
+ registerCliTool(
357
+ server,
358
+ runCli,
359
+ "lemon_action_run",
360
+ {
361
+ title: "Run a Lemon business action",
362
+ description:
363
+ "Run one already-selected ACC, SCM, or ERP action with its documented nested query/body envelope. This tool never searches, guesses parameters, or executes independent invoicing. Set user_confirmed only after explicit confirmation of a documented write action.",
364
+ inputSchema: z.object({
365
+ product: productSchema,
366
+ action: z.string().min(1),
367
+ payload: z
368
+ .object({
369
+ query: z.record(z.string(), z.unknown()).optional(),
370
+ body: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]).optional(),
371
+ _outputMode: z.enum(["basic", "full"]).optional(),
372
+ _extraDisplayFields: z.array(z.string().min(1)).min(1).optional(),
373
+ })
374
+ .strict(),
375
+ user_confirmed: z.boolean().default(false),
376
+ dry_run: z.boolean().default(false),
377
+ timeout_seconds: timeoutSchema,
378
+ }),
379
+ annotations: {
380
+ readOnlyHint: false,
381
+ destructiveHint: true,
382
+ idempotentHint: false,
383
+ openWorldHint: true,
384
+ },
385
+ },
386
+ (args) => {
387
+ const command = [
388
+ "action",
389
+ "run",
390
+ args.product,
391
+ args.action,
392
+ "--json-stdin",
393
+ "--timeout",
394
+ String(args.timeout_seconds),
395
+ ];
396
+ if (args.user_confirmed) command.push("--user-confirmed");
397
+ if (args.dry_run) command.push("--dry-run");
398
+ return {
399
+ command,
400
+ stdin: JSON.stringify(args.payload),
401
+ timeoutMs: commandTimeout(args.timeout_seconds),
402
+ };
403
+ },
404
+ );
405
+
406
+ server.registerTool(
407
+ "lemon_invoice_read_doc",
408
+ {
409
+ title: "Read Lemon independent-invoicing documentation",
410
+ description:
411
+ "Read a Runtime document relative to the invoice directory. Start with SKILL.md, then read only the immediate, batch, or feedback flow and shared documents it directs you to.",
412
+ inputSchema: z.object({
413
+ path: z.string().min(1).describe("For example SKILL.md or references/immediate/flow.md"),
414
+ }),
415
+ annotations: readOnlyAnnotations,
416
+ },
417
+ async ({ path }) => {
418
+ try {
419
+ const content = await readInvoiceDocument(runCli, path);
420
+ return { content: [{ type: "text", text: content }] };
421
+ } catch (error) {
422
+ return toolError(error);
423
+ }
424
+ },
425
+ );
426
+
427
+ server.registerTool(
428
+ "lemon_invoice_api",
429
+ {
430
+ title: "Call the Lemon independent-invoicing API adapter",
431
+ description:
432
+ "Execute one documented independent-invoicing operation with Runtime Python. Read and follow the invoice documents first. SkillInvoiceIssue is blocked unless user_confirmed is true and required context is supplied.",
433
+ inputSchema: z.object({
434
+ operation: z.enum([
435
+ "CompanyFullList",
436
+ "CompanyDetail",
437
+ "Customer",
438
+ "QixinbaoCompanyList",
439
+ "CheckLoginTaskState",
440
+ "AuthStatus",
441
+ "RemainingBalance",
442
+ "TaxItemBatchResolve",
443
+ "TaxBureauAuthChain",
444
+ "SkillInvoiceIssue",
445
+ "FeedbackSubmit",
446
+ ]),
447
+ eiscid: optionalText,
448
+ filter_word: optionalText,
449
+ company_name: optionalText,
450
+ company_id: optionalText,
451
+ personal_account: optionalText,
452
+ uscic: optionalText,
453
+ auth_company_id: optionalText,
454
+ company_detail_id: optionalText,
455
+ payload: z.record(z.string(), z.unknown()).optional(),
456
+ payload_path: optionalText,
457
+ api_key_last_four: optionalText,
458
+ ai_tool: optionalText,
459
+ model: optionalText,
460
+ feedback_contact: optionalText,
461
+ feedback_version: optionalText,
462
+ feedback_type: optionalText,
463
+ feedback_files: optionalText,
464
+ feedback_text: optionalText,
465
+ message_id: optionalText,
466
+ context: optionalText,
467
+ upload_file_path: optionalText,
468
+ user_confirmed: z.boolean().default(false),
469
+ timeout_seconds: timeoutSchema,
470
+ }),
471
+ annotations: {
472
+ readOnlyHint: false,
473
+ destructiveHint: true,
474
+ idempotentHint: false,
475
+ openWorldHint: true,
476
+ },
477
+ },
478
+ async (args) => {
479
+ try {
480
+ return cliToolResult(await runInvoiceApi(runCli, args));
481
+ } catch (error) {
482
+ return toolError(error);
483
+ }
484
+ },
485
+ );
486
+
487
+ server.registerTool(
488
+ "lemon_invoice_run_helper",
489
+ {
490
+ title: "Run a Lemon independent-invoicing helper",
491
+ description:
492
+ "Run one whitelisted Runtime helper for invoice file parsing, validation, preview generation, preview serving, customer lookup, or customer-service output. Arguments must come from the invoice documents.",
493
+ inputSchema: z.object({
494
+ operation: z.enum(invoiceHelperNames),
495
+ arguments: z.array(z.string()).max(30).default([]),
496
+ stdin: z.string().optional(),
497
+ timeout_seconds: timeoutSchema,
498
+ }),
499
+ annotations: {
500
+ readOnlyHint: false,
501
+ destructiveHint: false,
502
+ idempotentHint: false,
503
+ openWorldHint: true,
504
+ },
505
+ },
506
+ async (args) => {
507
+ try {
508
+ return cliToolResult(await runInvoiceHelper(runCli, args));
509
+ } catch (error) {
510
+ return toolError(error);
511
+ }
512
+ },
513
+ );
514
+
515
+ return server;
516
+ }
@@ -0,0 +1,40 @@
1
+ function publicFailure(result) {
2
+ if (result.timedOut) return "LemonClaw command timed out.";
3
+ if (result.outputLimitExceeded) return "LemonClaw command output exceeded the MCP limit.";
4
+ if (result.spawnError) return `Unable to start LemonClaw: ${result.spawnError.message}`;
5
+ return String(result.stderr || "LemonClaw command failed.").trim().slice(0, 2000);
6
+ }
7
+
8
+ export function cliToolResult(result, options = {}) {
9
+ const infrastructureFailure =
10
+ Boolean(result.spawnError) ||
11
+ Boolean(result.timedOut) ||
12
+ Boolean(result.outputLimitExceeded);
13
+ const text = infrastructureFailure
14
+ ? publicFailure(result)
15
+ : String(result.stdout || "").trim() || publicFailure(result);
16
+ const failed =
17
+ infrastructureFailure || (result.code !== 0 && options.nonzeroIsError !== false);
18
+
19
+ return {
20
+ content: [
21
+ {
22
+ type: "text",
23
+ text,
24
+ },
25
+ ],
26
+ ...(failed ? { isError: true } : {}),
27
+ };
28
+ }
29
+
30
+ export function toolError(error) {
31
+ return {
32
+ content: [
33
+ {
34
+ type: "text",
35
+ text: error instanceof Error ? error.message : String(error),
36
+ },
37
+ ],
38
+ isError: true,
39
+ };
40
+ }