2 Commits
Author SHA1 Message Date
Donaldttt 5c2cbac5b1 Merge pull request #10 from Donaldttt/feat/fixed-justconn-endpoint
Release / release (push) Canceled after 0s
feat: fix NocoBase endpoint to justconn.top
2026-08-15 14:04:36 +08:00
nac c40e456d7c feat: fix NocoBase endpoint to justconn.top 2026-08-15 06:03:08 +00:00
12 changed files with 64 additions and 75 deletions
+7 -9
View File
@@ -2,11 +2,12 @@
这是一份供终端型 AI Agent 执行的通用安装协议,不依赖任何特定 Agent 或 MCP 客户端。用户只需要提供本链接、确认本机变更,并在隐藏提示中输入自己的 NocoBase Authorize Key。 这是一份供终端型 AI Agent 执行的通用安装协议,不依赖任何特定 Agent 或 MCP 客户端。用户只需要提供本链接、确认本机变更,并在隐藏提示中输入自己的 NocoBase Authorize Key。
如果你是安装 Agent,请直接执行流程,不要让用户逐条尝试命令。只有在确认系统工具变更、取得 NocoBase URL、要求用户创建 Key,或让用户在隐藏提示中粘贴 Key 时才中断用户。 如果你是安装 Agent,请直接执行流程,不要让用户逐条尝试命令。只有在确认系统工具变更、要求用户创建 Key,或让用户在隐藏提示中粘贴 Key 时才中断用户。
## 完成标准 ## 完成标准
- 下载本文件所在的同一固定 Release,不使用默认分支; - 下载本文件所在的同一固定 Release,不使用默认分支;
- 只连接固定的 NocoBase API `https://justconn.top/api`;
- MCP 在用户本机以 stdio 运行,默认只读; - MCP 在用户本机以 stdio 运行,默认只读;
- 认证属于当前用户自己的个人 Authorize Key 和业务角色; - 认证属于当前用户自己的个人 Authorize Key 和业务角色;
- 当前 MCP 客户端已注册 `nocobase-crm`; - 当前 MCP 客户端已注册 `nocobase-crm`;
@@ -21,16 +22,13 @@
- 不静默安装 Git、Node.js、包管理器或其他系统工具。 - 不静默安装 Git、Node.js、包管理器或其他系统工具。
- 修改 MCP 客户端配置前展示目标文件与新增项,创建备份,并结构化合并;不得覆盖其他 server。 - 修改 MCP 客户端配置前展示目标文件与新增项,创建备份,并结构化合并;不得覆盖其他 server。
- 默认不启用写入、原始数据、后台服务或网络监听。 - 默认不启用写入、原始数据、后台服务或网络监听。
- 不询问或接受其他 NocoBase URL,也不允许通过参数、配置或凭证文件覆盖固定 API 地址。
## 安装流程 ## 安装流程
### 1. 确认输入与 Release 权限 ### 1. 确认本机位置与 Release 权限
取得: 确定当前 Agent/MCP 客户端及其配置路径,以及普通用户应用数据目录。NocoBase API 已固定为 `https://justconn.top/api`,不需要向用户询问地址。
- NocoBase 应用 URL,例如 `https://nocobase.example`;
- 当前 Agent/MCP 客户端及其配置路径;
- 普通用户应用数据目录。
从本文件 URL 解析固定 Release Tag。读取同一 Release 的资产列表,只接受: 从本文件 URL 解析固定 Release Tag。读取同一 Release 的资产列表,只接受:
@@ -79,10 +77,10 @@ npm install --prefix <install-prefix> --omit=dev <verified-release.tgz>
在用户可直接控制的本机终端运行: 在用户可直接控制的本机终端运行:
```bash ```bash
<installed-command> setup --api-base-url <nocobase-url> --json <installed-command> setup --json
``` ```
安装程序会先验证安全 TTY,再显示隐藏 Key 提示;没有安全 TTY 时会在写文件前停止。Key 通过 `roles:check` 验证成功后才保存,并立即执行 `crm_health`。Key 不会出现在返回 JSON、命令参数、MCP 配置或日志中。 安装程序会先验证安全 TTY,再显示固定 API 地址和隐藏 Key 提示;没有安全 TTY 时会在写文件前停止。Key 通过 `https://justconn.top/api/roles:check` 验证成功后才保存,并立即执行 `crm_health`。凭证文件只保存 Key,不保存或接受 API 地址。Key 不会出现在返回 JSON、命令参数、MCP 配置或日志中。
POSIX 凭证目录使用 `0700`,凭证文件使用 `0600`。Windows 使用当前用户应用数据目录。不要复制其他用户的凭证文件;每个操作系统用户都必须单独设置自己的 Key。 POSIX 凭证目录使用 `0700`,凭证文件使用 `0600`。Windows 使用当前用户应用数据目录。不要复制其他用户的凭证文件;每个操作系统用户都必须单独设置自己的 Key。
+4 -4
View File
@@ -7,7 +7,7 @@
正式 Release 是一个已打包的 npm tarball,内部包含 MCP SDK 和运行代码。用户电脑只需要: 正式 Release 是一个已打包的 npm tarball,内部包含 MCP SDK 和运行代码。用户电脑只需要:
- Node.js 20.11 或更高版本(npm 随 Node.js 一起提供); - Node.js 20.11 或更高版本(npm 随 Node.js 一起提供);
- 能访问目标 NocoBase; - 能访问固定的 NocoBase 环境 `https://justconn.top`;
- 用户自己的个人 Authorize Key。 - 用户自己的个人 Authorize Key。
用户不再需要 Git、Python、uv、Python 包或 `nb CLI`。安装也不会全局安装本项目,而是放到用户自己的应用数据目录。 用户不再需要 Git、Python、uv、Python 包或 `nb CLI`。安装也不会全局安装本项目,而是放到用户自己的应用数据目录。
@@ -40,7 +40,7 @@ node dist/index.js --help
开发时初始化个人配置: 开发时初始化个人配置:
```bash ```bash
node dist/index.js setup --api-base-url https://nocobase.example --json node dist/index.js setup --json
``` ```
Authorize Key 只能输入到隐藏提示。不要把 Key 放进参数、环境变量、聊天、`config.json`、MCP 客户端配置或日志。 Authorize Key 只能输入到隐藏提示。不要把 Key 放进参数、环境变量、聊天、`config.json`、MCP 客户端配置或日志。
@@ -48,7 +48,7 @@ Authorize Key 只能输入到隐藏提示。不要把 Key 放进参数、环境
可用命令: 可用命令:
```text ```text
nocobase-crm-mcp setup --api-base-url <url> [--config <path>] [--json] nocobase-crm-mcp setup [--config <path>] [--json]
nocobase-crm-mcp [serve] [--config <path>] [--enable-writes] nocobase-crm-mcp [serve] [--config <path>] [--enable-writes]
nocobase-crm-mcp doctor [--config <path>] [--json] nocobase-crm-mcp doctor [--config <path>] [--json]
nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json] nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
@@ -71,6 +71,6 @@ nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
AI 客户端 → 本机 TypeScript MCP → HTTPS NocoBase API → 当前用户 Key 对应的 ACL AI 客户端 → 本机 TypeScript MCP → HTTPS NocoBase API → 当前用户 Key 对应的 ACL
``` ```
MCP 不缓存共享身份。每个请求都会从当前操作系统用户的私有凭证文件读取 Key,因此撤销或替换 Key 后无需重新生成 MCP 客户端配置。远程地址必须使用 HTTPS(回环地址除外),跨域重定向会在转发 Authorization 前被拒绝,错误内容会脱敏。 MCP 不缓存共享身份。每个请求都会从当前操作系统用户的私有凭证文件读取 Key,因此撤销或替换 Key 后无需重新生成 MCP 客户端配置。API 地址固定为 `https://justconn.top/api`,不接受安装参数或凭证文件覆盖;跨域重定向会在转发 Authorization 前被拒绝,错误内容会脱敏。
内部结构见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md),运维与权限验收见 [`docs/LOCAL_DELIVERY.md`](docs/LOCAL_DELIVERY.md)。 内部结构见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md),运维与权限验收见 [`docs/LOCAL_DELIVERY.md`](docs/LOCAL_DELIVERY.md)。
+2 -2
View File
@@ -9,7 +9,7 @@ MCP client → TypeScript tool boundary → CRM domain service → NocoBaseClien
## 模块边界 ## 模块边界
- `src/index.ts` 是单一可执行入口;`src/cli.ts` 管理 setup、doctor、客户端配置和 stdio 生命周期。 - `src/index.ts` 是单一可执行入口;`src/cli.ts` 管理 setup、doctor、客户端配置和 stdio 生命周期。
- `src/credentials.ts` 负责 URL 安全校验、隐藏 TTY 输入和用户级私有凭证文件。Key 不进入 argv、环境变量、CRM 配置或 MCP 配置。 - `src/credentials.ts` 固定 `https://justconn.top/api`、负责隐藏 TTY 输入和用户级私有凭证文件。凭证文件只保存 Key;Key 不进入 argv、环境变量、CRM 配置或 MCP 配置。
- `src/nocobase-client.ts` 是唯一远程适配器,直接实现 NocoBase resource action 协议、data-modeling 读取、项目附件关系和 ACL 检查。 - `src/nocobase-client.ts` 是唯一远程适配器,直接实现 NocoBase resource action 协议、data-modeling 读取、项目附件关系和 ACL 检查。
- `src/crm-service.ts` 负责 CRM 查询、解析、分页、聚合和可读输出;`src/crm-helpers.ts` 与 `src/filters.ts` 保存纯函数。 - `src/crm-service.ts` 负责 CRM 查询、解析、分页、聚合和可读输出;`src/crm-helpers.ts` 与 `src/filters.ts` 保存纯函数。
- `src/project-service.ts` 只允许固定项目字段的 create/update;`src/tools.ts` 是稳定 MCP 契约和错误/原始数据策略边界。 - `src/project-service.ts` 只允许固定项目字段的 create/update;`src/tools.ts` 是稳定 MCP 契约和错误/原始数据策略边界。
@@ -18,7 +18,7 @@ MCP client → TypeScript tool boundary → CRM domain service → NocoBaseClien
## 身份与网络安全 ## 身份与网络安全
- 每个请求重新读取当前 OS 用户的凭证文件,NocoBase ACL 是最终权限边界。 - 每个请求重新读取当前 OS 用户的凭证文件,NocoBase ACL 是最终权限边界。
- 非回环 HTTP URL 被拒绝;远程 Key 只能发往 HTTPS。 - 生产运行时只连接固定 HTTPS API `https://justconn.top/api`,不接受参数、配置或凭证文件覆盖。
- Authorization 只允许在同源重定向中保留,跨源重定向在发送凭证前失败。 - Authorization 只允许在同源重定向中保留,跨源重定向在发送凭证前失败。
- Token 会从 HTTP 错误中替换为 `<redacted>`。 - Token 会从 HTTP 错误中替换为 `<redacted>`。
- stdio 是唯一默认传输,不创建后台服务或监听端口。 - stdio 是唯一默认传输,不创建后台服务或监听端口。
+2 -2
View File
@@ -5,7 +5,7 @@
每位用户在自己的电脑运行 MCP,并使用绑定本人业务身份的个人 Authorize Key: 每位用户在自己的电脑运行 MCP,并使用绑定本人业务身份的个人 Authorize Key:
```text ```text
本机 AI 客户端 → 本机 TypeScript MCP → 远程 NocoBase API → 本人 ACL 本机 AI 客户端 → 本机 TypeScript MCP → https://justconn.top/api → 本人 ACL
``` ```
项目不保存共享业务账号,也不需要多人共用的远程 MCP 服务。 项目不保存共享业务账号,也不需要多人共用的远程 MCP 服务。
@@ -53,4 +53,4 @@ Agent 下载新的固定 Release tarball并验证 SHA-256,再对同一隔离
- `nocobase_request_timeout`:检查 NocoBase 可用性或缩小查询。 - `nocobase_request_timeout`:检查 NocoBase 可用性或缩小查询。
- `nocobase_request_failed` / `NO_PERMISSIONS`:检查该用户 ACL 和字段/行级权限。 - `nocobase_request_failed` / `NO_PERMISSIONS`:检查该用户 ACL 和字段/行级权限。
- 凭证文件权限错误:POSIX 下修复为 `0600`,父目录为 `0700`,不要放宽为共享可读。 - 凭证文件权限错误:POSIX 下修复为 `0600`,父目录为 `0700`,不要放宽为共享可读。
- 跨源重定向错误:修正 NocoBase API URL或代理配置;不要关闭同源保护。 - 跨源重定向错误:检查 `https://justconn.top/api` 的反向代理配置;不要关闭同源保护。
+5 -1
View File
@@ -2,7 +2,7 @@
"schemaVersion": 2, "schemaVersion": 2,
"id": "com.nocobase.crm-mcp", "id": "com.nocobase.crm-mcp",
"name": "NocoBase CRM MCP", "name": "NocoBase CRM MCP",
"version": "0.3.0", "version": "0.3.1",
"distribution": { "distribution": {
"repository": "https://github.com/Donaldttt/nocobase-mcp", "repository": "https://github.com/Donaldttt/nocobase-mcp",
"releaseRef": "must-match-v<version>", "releaseRef": "must-match-v<version>",
@@ -19,6 +19,10 @@
"nocobaseCli": false, "nocobaseCli": false,
"python": false "python": false
}, },
"nocobase": {
"apiBaseUrl": "https://justconn.top/api",
"userConfigurableEndpoint": false
},
"installation": { "installation": {
"requiredTools": ["node", "npm"], "requiredTools": ["node", "npm"],
"sourceCheckout": false, "sourceCheckout": false,
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@donaldttt/nocobase-crm-mcp", "name": "@donaldttt/nocobase-crm-mcp",
"version": "0.3.0", "version": "0.3.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@donaldttt/nocobase-crm-mcp", "name": "@donaldttt/nocobase-crm-mcp",
"version": "0.3.0", "version": "0.3.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"bin": { "bin": {
"nocobase-crm-mcp": "dist/index.js" "nocobase-crm-mcp": "dist/index.js"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@donaldttt/nocobase-crm-mcp", "name": "@donaldttt/nocobase-crm-mcp",
"version": "0.3.0", "version": "0.3.1",
"description": "Local NocoBase CRM MCP server using each user's personal Authorize Key", "description": "Local NocoBase CRM MCP server using each user's personal Authorize Key",
"type": "module", "type": "module",
"private": true, "private": true,
+13 -12
View File
@@ -6,7 +6,7 @@ import { loadConfig } from "./config.js";
import { import {
credentialsPath, credentialsPath,
loadCredentials, loadCredentials,
normalizeApiBaseUrl, NOCOBASE_API_BASE_URL,
readHidden, readHidden,
saveCredentials, saveCredentials,
} from "./credentials.js"; } from "./credentials.js";
@@ -18,7 +18,6 @@ import { VERSION } from "./version.js";
interface ParsedArgs { interface ParsedArgs {
command: "serve" | "setup" | "doctor" | "client-config" | "help" | "version"; command: "serve" | "setup" | "doctor" | "client-config" | "help" | "version";
apiBaseUrl?: string;
config?: string; config?: string;
enableWrites: boolean; enableWrites: boolean;
json: boolean; json: boolean;
@@ -80,27 +79,29 @@ export function createRuntime(configPath: string): {
} }
async function setup(args: ParsedArgs): Promise<void> { async function setup(args: ParsedArgs): Promise<void> {
if (!args.apiBaseUrl) throw new Error("setup requires --api-base-url <https://nocobase.example>");
if (!process.stdin.isTTY || !process.stderr.isTTY) { if (!process.stdin.isTTY || !process.stderr.isTTY) {
throw new Error("Setup requires a user-controlled local terminal; no files were changed"); throw new Error("Setup requires a user-controlled local terminal; no files were changed");
} }
const apiBaseUrl = normalizeApiBaseUrl(args.apiBaseUrl);
process.stderr.write( process.stderr.write(
"Create a revocable personal Authorize Key in NocoBase for your own business account.\n" + `This MCP connects only to ${NOCOBASE_API_BASE_URL}.\n` +
"Create a revocable personal Authorize Key in NocoBase for your own business account.\n" +
"Do not paste the Key into Agent chat. Enter it only in the hidden prompt below.\n", "Do not paste the Key into Agent chat. Enter it only in the hidden prompt below.\n",
); );
const authorizeKey = (await readHidden("Personal NocoBase Authorize Key: ")).trim(); const authorizeKey = (await readHidden("Personal NocoBase Authorize Key: ")).trim();
if (!authorizeKey) throw new Error("Authorize Key must not be blank; no files were changed"); if (!authorizeKey) throw new Error("Authorize Key must not be blank; no files were changed");
const verificationClient = new NocoBaseClient(() => ({ apiBaseUrl, authorizeKey })); const verificationClient = new NocoBaseClient(() => ({
apiBaseUrl: NOCOBASE_API_BASE_URL,
authorizeKey,
}));
await verificationClient.verifyCredentials(); await verificationClient.verifyCredentials();
const configPath = ensureConfig(args.config); const configPath = ensureConfig(args.config);
saveCredentials({ apiBaseUrl, authorizeKey }); saveCredentials({ authorizeKey });
const runtime = createRuntime(configPath); const runtime = createRuntime(configPath);
const health = await runtime.crm.crmHealth(); const health = await runtime.crm.crmHealth();
printResult( printResult(
{ {
ok: true, ok: true,
apiBaseUrl, apiBaseUrl: NOCOBASE_API_BASE_URL,
credentialsPath: credentialsPath(), credentialsPath: credentialsPath(),
keyExposure: "private-tty-and-0600-local-file-only", keyExposure: "private-tty-and-0600-local-file-only",
configPath, configPath,
@@ -165,11 +166,10 @@ function parseArgs(argv: string[]): ParsedArgs {
else if (flag === "--version" || flag === "-v") parsed.command = "version"; else if (flag === "--version" || flag === "-v") parsed.command = "version";
else if (flag === "--enable-writes") parsed.enableWrites = true; else if (flag === "--enable-writes") parsed.enableWrites = true;
else if (flag === "--json") parsed.json = true; else if (flag === "--json") parsed.json = true;
else if (flag === "--api-base-url" || flag === "--config") { else if (flag === "--config") {
const value = argv[index + 1]; const value = argv[index + 1];
if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`); if (!value || value.startsWith("-")) throw new Error(`${flag} requires a value`);
if (flag === "--api-base-url") parsed.apiBaseUrl = value; parsed.config = value;
else parsed.config = value;
index += 1; index += 1;
} else throw new Error(`Unknown option: ${flag}`); } else throw new Error(`Unknown option: ${flag}`);
index += 1; index += 1;
@@ -185,11 +185,12 @@ function helpText(): string {
return `NocoBase CRM MCP ${VERSION} return `NocoBase CRM MCP ${VERSION}
Usage: Usage:
nocobase-crm-mcp setup --api-base-url <url> [--config <path>] [--json] nocobase-crm-mcp setup [--config <path>] [--json]
nocobase-crm-mcp [serve] [--config <path>] [--enable-writes] nocobase-crm-mcp [serve] [--config <path>] [--enable-writes]
nocobase-crm-mcp doctor [--config <path>] [--json] nocobase-crm-mcp doctor [--config <path>] [--json]
nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json] nocobase-crm-mcp client-config [--config <path>] [--enable-writes] [--json]
The NocoBase API endpoint is fixed to ${NOCOBASE_API_BASE_URL}.
The setup command accepts the personal Authorize Key only through a hidden local TTY prompt. The setup command accepts the personal Authorize Key only through a hidden local TTY prompt.
The default stdio server is read-only. The default stdio server is read-only.
`; `;
+10 -24
View File
@@ -8,21 +8,7 @@ export interface Credentials {
authorizeKey: string; authorizeKey: string;
} }
export function normalizeApiBaseUrl(value: string): string { export const NOCOBASE_API_BASE_URL = "https://justconn.top/api";
const raw = value.trim();
if (!raw) throw new Error("NocoBase URL is required");
const url = new URL(raw);
if (!(["http:", "https:"] as string[]).includes(url.protocol))
throw new Error("NocoBase URL must use HTTP or HTTPS");
if (url.username || url.password || url.search || url.hash) {
throw new Error("NocoBase URL must not contain credentials, a query string, or a fragment");
}
const local = url.hostname === "localhost" || url.hostname === "::1" || url.hostname.startsWith("127.");
if (url.protocol !== "https:" && !local)
throw new Error("A remote Authorize Key may only be sent over HTTPS");
url.pathname = `${url.pathname.replace(/\/+$/, "")}${url.pathname.replace(/\/+$/, "").endsWith("/api") ? "" : "/api"}`;
return url.toString().replace(/\/$/, "");
}
export function credentialsPath(): string { export function credentialsPath(): string {
const override = process.env.NOCOBASE_MCP_CREDENTIALS?.trim(); const override = process.env.NOCOBASE_MCP_CREDENTIALS?.trim();
@@ -38,12 +24,12 @@ export function credentialsPath(): string {
return join(base, "nocobase-crm-mcp", "credentials.json"); return join(base, "nocobase-crm-mcp", "credentials.json");
} }
export function saveCredentials(credentials: Credentials, path = credentialsPath()): void { export function saveCredentials(
const normalized: Credentials = { credentials: Pick<Credentials, "authorizeKey">,
apiBaseUrl: normalizeApiBaseUrl(credentials.apiBaseUrl), path = credentialsPath(),
authorizeKey: credentials.authorizeKey.trim(), ): void {
}; const authorizeKey = credentials.authorizeKey.trim();
if (!normalized.authorizeKey) throw new Error("Authorize Key must not be blank"); if (!authorizeKey) throw new Error("Authorize Key must not be blank");
mkdirSync(dirname(path), { recursive: true, mode: 0o700 }); mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
if (process.platform !== "win32") { if (process.platform !== "win32") {
try { try {
@@ -56,7 +42,7 @@ export function saveCredentials(credentials: Credentials, path = credentialsPath
} }
const descriptor = openSync(path, "w", 0o600); const descriptor = openSync(path, "w", 0o600);
try { try {
writeFileSync(descriptor, `${JSON.stringify(normalized, null, 2)}\n`, "utf8"); writeFileSync(descriptor, `${JSON.stringify({ authorizeKey }, null, 2)}\n`, "utf8");
} finally { } finally {
closeSync(descriptor); closeSync(descriptor);
} }
@@ -69,12 +55,12 @@ export function loadCredentials(path = credentialsPath()): Credentials {
throw new Error(`Credential file must be private (0600); current mode is ${mode.toString(8)}`); throw new Error(`Credential file must be private (0600); current mode is ${mode.toString(8)}`);
} }
const parsed: unknown = JSON.parse(readFileSync(path, "utf8")); const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
if (!isRecord(parsed) || typeof parsed.apiBaseUrl !== "string" || typeof parsed.authorizeKey !== "string") { if (!isRecord(parsed) || typeof parsed.authorizeKey !== "string") {
throw new Error("Credential file is invalid; run setup again"); throw new Error("Credential file is invalid; run setup again");
} }
const authorizeKey = parsed.authorizeKey.trim(); const authorizeKey = parsed.authorizeKey.trim();
if (!authorizeKey) throw new Error("Credential file has no Authorize Key; run setup again"); if (!authorizeKey) throw new Error("Credential file has no Authorize Key; run setup again");
return { apiBaseUrl: normalizeApiBaseUrl(parsed.apiBaseUrl), authorizeKey }; return { apiBaseUrl: NOCOBASE_API_BASE_URL, authorizeKey };
} }
export async function readHidden(prompt: string): Promise<string> { export async function readHidden(prompt: string): Promise<string> {
+1 -1
View File
@@ -423,7 +423,7 @@ async function safeTool(operation: () => Promise<ToolPayload>): Promise<ToolPayl
error: { error: {
code: error.statusCode === 408 ? "nocobase_request_timeout" : "nocobase_request_failed", code: error.statusCode === 408 ? "nocobase_request_timeout" : "nocobase_request_failed",
message: error.message, message: error.message,
hint: "Check the NocoBase URL, ACL, collection mapping, and query parameters.", hint: "Check justconn.top availability, ACL, collection mapping, and query parameters.",
retryable: error.retryable, retryable: error.retryable,
}, },
}; };
+9 -5
View File
@@ -9,7 +9,7 @@ import { VERSION } from "../src/version.js";
test("package, manifest, and runtime versions agree with no runtime npm dependencies", () => { test("package, manifest, and runtime versions agree with no runtime npm dependencies", () => {
const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")); const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const manifest = JSON.parse(readFileSync(new URL("../install-manifest.json", import.meta.url), "utf8")); const manifest = JSON.parse(readFileSync(new URL("../install-manifest.json", import.meta.url), "utf8"));
assert.equal(VERSION, "0.3.0"); assert.equal(VERSION, "0.3.1");
assert.equal(packageJson.version, VERSION); assert.equal(packageJson.version, VERSION);
assert.equal(manifest.version, VERSION); assert.equal(manifest.version, VERSION);
assert.deepEqual(packageJson.dependencies, undefined); assert.deepEqual(packageJson.dependencies, undefined);
@@ -31,10 +31,7 @@ test("setup refuses a non-TTY before creating credential files", async () => {
const previous = process.env.NOCOBASE_MCP_CREDENTIALS; const previous = process.env.NOCOBASE_MCP_CREDENTIALS;
process.env.NOCOBASE_MCP_CREDENTIALS = path; process.env.NOCOBASE_MCP_CREDENTIALS = path;
try { try {
await assert.rejects( await assert.rejects(runCli(["setup", "--json"]), /local terminal/);
runCli(["setup", "--api-base-url", "https://nb.example", "--json"]),
/local terminal/,
);
assert.equal(existsSync(path), false); assert.equal(existsSync(path), false);
assert.equal(existsSync(join(directory, "config.json")), false); assert.equal(existsSync(join(directory, "config.json")), false);
} finally { } finally {
@@ -42,3 +39,10 @@ test("setup refuses a non-TTY before creating credential files", async () => {
else process.env.NOCOBASE_MCP_CREDENTIALS = previous; else process.env.NOCOBASE_MCP_CREDENTIALS = previous;
} }
}); });
test("setup rejects API endpoint overrides", async () => {
await assert.rejects(
runCli(["setup", "--api-base-url", "https://other.example"]),
/Unknown option: --api-base-url/,
);
});
+8 -12
View File
@@ -4,24 +4,20 @@ import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import test from "node:test"; import test from "node:test";
import { DEFAULT_CONFIG, loadConfig } from "../src/config.js"; import { DEFAULT_CONFIG, loadConfig } from "../src/config.js";
import { loadCredentials, normalizeApiBaseUrl, saveCredentials } from "../src/credentials.js"; import { loadCredentials, NOCOBASE_API_BASE_URL, saveCredentials } from "../src/credentials.js";
test("normalizes application URLs to the NocoBase API base", () => { test("stores only the Key and always loads the fixed NocoBase endpoint", () => {
assert.equal(normalizeApiBaseUrl("https://nb.example/app/"), "https://nb.example/app/api");
assert.equal(normalizeApiBaseUrl("http://127.0.0.1:13000/api"), "http://127.0.0.1:13000/api");
assert.throws(() => normalizeApiBaseUrl("http://nb.example"), /HTTPS/i);
assert.throws(() => normalizeApiBaseUrl("https://user:pass@nb.example"), /credentials/i);
});
test("stores and reads credentials from a private file", () => {
const directory = mkdtempSync(join(tmpdir(), "nocobase-mcp-credentials-")); const directory = mkdtempSync(join(tmpdir(), "nocobase-mcp-credentials-"));
const path = join(directory, "private", "credentials.json"); const path = join(directory, "private", "credentials.json");
saveCredentials({ apiBaseUrl: "https://nb.example", authorizeKey: "secret" }, path); saveCredentials({ authorizeKey: "secret" }, path);
assert.deepEqual(loadCredentials(path), { assert.deepEqual(loadCredentials(path), {
apiBaseUrl: "https://nb.example/api", apiBaseUrl: NOCOBASE_API_BASE_URL,
authorizeKey: "secret", authorizeKey: "secret",
}); });
assert.match(readFileSync(path, "utf8"), /secret/); assert.deepEqual(JSON.parse(readFileSync(path, "utf8")), { authorizeKey: "secret" });
writeFileSync(path, JSON.stringify({ apiBaseUrl: "https://legacy.example/api", authorizeKey: "secret" }));
assert.equal(loadCredentials(path).apiBaseUrl, NOCOBASE_API_BASE_URL);
if (process.platform !== "win32") { if (process.platform !== "win32") {
chmodSync(path, 0o644); chmodSync(path, 0o644);
assert.throws(() => loadCredentials(path), /0600/); assert.throws(() => loadCredentials(path), /0600/);