feat: MCP 后端切到 deta.city 远程端点 + 账号登录门槛
- .mcp.json / cordis.patch.yml 改为远程 streamable-http(https://deta.city/mcp-api/mcp),凭据经环境变量 DETA_TOKEN - 新增 bin/deta-login.mjs:deta.city 账号登录,写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600) - 新增 plugins/deta/README.md:端点/凭据语义、两种宿主安装、本地 stdio 开发形态 - 本地 stdio 形态改为默认注释关闭(仅本机开发,不校验账号、不隔离工作区)
This commit is contained in:
@@ -16,6 +16,10 @@ DETA 领域能力在各类 Agent harness 上的分发仓:**一份产物,多
|
|||||||
## 安装(Codex)
|
## 安装(Codex)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 先登录(一次性,需要 deta.city 账号):换取 DETA_TOKEN
|
||||||
|
node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs
|
||||||
|
source ~/.deta/env # 让 DETA_TOKEN 进入当前 shell 环境
|
||||||
|
|
||||||
# 从 Git(分发用)
|
# 从 Git(分发用)
|
||||||
codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git
|
codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git
|
||||||
|
|
||||||
@@ -26,6 +30,9 @@ codex plugin list
|
|||||||
codex plugin add deta@deta
|
codex plugin add deta@deta
|
||||||
```
|
```
|
||||||
|
|
||||||
|
未登录(环境里没有 `DETA_TOKEN`)时,MCP 端点会返回 **401**——含义是
|
||||||
|
「当前环境没有可用的 deta.city 登录态」,重新执行上面的登录两步即可。
|
||||||
|
|
||||||
## 内容
|
## 内容
|
||||||
|
|
||||||
| 插件 | 技能 | 能力 | 依赖 |
|
| 插件 | 技能 | 能力 | 依赖 |
|
||||||
@@ -34,22 +41,37 @@ codex plugin add deta@deta
|
|||||||
|
|
||||||
## 前置条件
|
## 前置条件
|
||||||
|
|
||||||
`deta` 插件的 MCP 后端是领域服务的无头 MCP 服务:
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| MCP 端点 | `https://deta.city/mcp-api/mcp`(MCP Streamable HTTP) |
|
||||||
|
| 凭据来源 | **deta.city 账号**;`bin/deta-login.mjs` 登录取令牌,经环境变量 `DETA_TOKEN` 传给 harness |
|
||||||
|
| 凭据文件 | `~/.deta/credentials.json`(令牌 + 账号 + 端点)、`~/.deta/env`(`export DETA_TOKEN=...`),均 0600 |
|
||||||
|
|
||||||
|
端点除探活接口(服务端 `GET /health`,经 Nginx 前缀对外为 `/mcp-api/health`)外全部需要鉴权:
|
||||||
|
|
||||||
|
- 未带凭据 / 凭据失效 → `401`(提示重新登录,不回显凭据);
|
||||||
|
- 服务端认证服务不可用 → `500`,同样不放行;
|
||||||
|
- 各账号在服务端有独立工作区(`<工作区根>/<账号 ID>`),互不可见。
|
||||||
|
|
||||||
|
### 本地 stdio 形态(开发用,默认关闭)
|
||||||
|
|
||||||
|
本机开发时也可以直连领域服务的无头 MCP 服务(`bin/deta-mcp.sh`,stdio),
|
||||||
|
它**不校验 deta.city 账号、不隔离工作区**,仅供本机迭代领域能力:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd ~/Dev/ant/ant-copilot && npm run build
|
cd ~/Dev/ant/ant-copilot && npm run build
|
||||||
node dist/src/headless/mcp-server.js # 冒烟:应等待 stdin/stdout
|
node dist/src/headless/mcp-server.js # 冒烟:应等待 stdin/stdout
|
||||||
```
|
```
|
||||||
|
|
||||||
环境变量:
|
该形态的环境变量:
|
||||||
|
|
||||||
| 变量 | 缺省 | 说明 |
|
| 变量 | 缺省 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `DETA_SERVER_ROOT` | `~/Dev/ant/ant-copilot` | 领域服务仓库根目录(内部路径) |
|
| `DETA_SERVER_ROOT` | `~/Dev/ant/ant-copilot` | 领域服务仓库根目录(内部路径) |
|
||||||
| `ANT_HEADLESS_WORKSPACE` | `<cwd>/workspace` | AML 工作目录(`aml.xml` / `building.json` 落盘处,名称由底层服务决定) |
|
| `ANT_HEADLESS_WORKSPACE` | `<cwd>/workspace` | AML 工作目录(`aml.xml` / `building.json` 落盘处,名称由底层服务决定) |
|
||||||
|
|
||||||
编译与校验类工具需要编译服务在线(`localhost:8000`);未在线时返回明确错误,
|
编译与校验类工具需要编译服务在线(服务端为 `DETA_GSA_URL`,缺省 `localhost:8000`);
|
||||||
读写类工具可离线工作。
|
未在线时返回明确错误,读写类工具可离线工作。
|
||||||
|
|
||||||
## 本地迭代
|
## 本地迭代
|
||||||
|
|
||||||
@@ -117,8 +139,9 @@ node apps/cli/lib/bin.js --profile headless --dump-config \
|
|||||||
# 输出中应出现 `# == .../plugins/deta/cordis.patch.yml` 与 deta-mcp 行
|
# 输出中应出现 `# == .../plugins/deta/cordis.patch.yml` 与 deta-mcp 行
|
||||||
```
|
```
|
||||||
|
|
||||||
`cordis.patch.yml` 里远程端点那段已按公开形态写好(`streamable-http` + Bearer 头),
|
`cordis.patch.yml` 当前默认启用**远程端点**(`streamable-http` +
|
||||||
当前默认启用的是本地 stdio 行;服务端化完成后注释切换即可。
|
`Authorization: Bearer ${DETA_TOKEN}`,指向 `https://deta.city/mcp-api/mcp`);
|
||||||
|
本地 stdio 段保留为注释、标注「本机开发用,默认关闭」,需要时手工切换。
|
||||||
|
|
||||||
## 已知缺陷(实测,2026-09-10)
|
## 已知缺陷(实测,2026-09-10)
|
||||||
|
|
||||||
@@ -160,9 +183,11 @@ Duplicate namespace name 'deta'; Namespace names must be unique.
|
|||||||
.agents/plugins/api_marketplace.json # Codex API key 登录用户市场
|
.agents/plugins/api_marketplace.json # Codex API key 登录用户市场
|
||||||
plugins/deta/ # 同一目录 = Codex 插件根 = dsh 组合包根
|
plugins/deta/ # 同一目录 = Codex 插件根 = dsh 组合包根
|
||||||
├── .codex-plugin/plugin.json # Codex 插件清单
|
├── .codex-plugin/plugin.json # Codex 插件清单
|
||||||
├── .mcp.json # Codex MCP 声明(含 Codex 私有键)
|
├── .mcp.json # Codex MCP 声明(远程 streamable-http)
|
||||||
├── package.json # dsh 组合包声明(dsh.bundle.patch)
|
├── package.json # dsh 组合包声明(dsh.bundle.patch)
|
||||||
├── cordis.patch.yml # dsh 配置层(挂载 MCP 服务)
|
├── cordis.patch.yml # dsh 配置层(挂载 MCP 服务)
|
||||||
├── bin/deta-mcp.sh # 本地 MCP 启动器(开发用)
|
├── bin/deta-login.mjs # 登录助手(~/.deta/credentials.json + ~/.deta/env)
|
||||||
|
├── bin/deta-mcp.sh # 本地 stdio 启动器(开发用,默认关闭)
|
||||||
|
├── README.md # 插件说明(安装/凭据/401 语义)
|
||||||
└── skills/aml-design/SKILL.md # 领域技能(两宿主共享)
|
└── skills/aml-design/SKILL.md # 领域技能(两宿主共享)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"deta": {
|
"deta": {
|
||||||
"command": "./bin/deta-mcp.sh",
|
"type": "streamable-http",
|
||||||
"args": [],
|
"url": "https://deta.city/mcp-api/mcp",
|
||||||
"cwd": ".",
|
"bearer_token_env_var": "DETA_TOKEN"
|
||||||
"env_vars": [
|
|
||||||
"DETA_SERVER_ROOT",
|
|
||||||
"ANT_HEADLESS_WORKSPACE"
|
|
||||||
],
|
|
||||||
"default_tools_approval_mode": "approve",
|
|
||||||
"supports_parallel_tool_calls": false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# deta 插件(Codex 插件 / dsh 组合包)
|
||||||
|
|
||||||
|
DETA 领域能力(AML 建筑模型读写、编译校验、围护结构统计、空间邻接查询)
|
||||||
|
以**远程 MCP 服务**形态挂进 Agent harness:同一个目录既是 Codex 插件根,
|
||||||
|
也是 dsh 组合包根,两端共享 `skills/` 与同一份 MCP 后端。
|
||||||
|
|
||||||
|
## 端点与凭据
|
||||||
|
|
||||||
|
| 项 | 值 |
|
||||||
|
|----|-----|
|
||||||
|
| 端点 | `https://deta.city/mcp-api/mcp`(MCP Streamable HTTP) |
|
||||||
|
| 凭据来源 | **deta.city 账号**(令牌经 `POST https://deta.city/prod-api/login` 换取) |
|
||||||
|
| 传参方式 | 环境变量 `DETA_TOKEN`(Codex 侧 `bearer_token_env_var`;dsh 侧 `Authorization: Bearer ${DETA_TOKEN}`) |
|
||||||
|
|
||||||
|
端点除探活接口(服务端 `GET /health`,经 Nginx 前缀对外为 `/mcp-api/health`)外**全部需要鉴权**:
|
||||||
|
|
||||||
|
- 请求未带凭据、凭据失效/过期 → **401**(响应体为可读提示,不回显凭据)。
|
||||||
|
401 的含义是「当前环境没有可用的 deta.city 登录态」,不是工具或模型出错;
|
||||||
|
重新执行 `node bin/deta-login.mjs` 并 `source ~/.deta/env` 即可。
|
||||||
|
- 服务端认证服务不可用(`ANT_ADMIN_SERVER` 未配置或不可达)→ 500,同样不放行。
|
||||||
|
|
||||||
|
账号的工作区在服务端按账号隔离(`<工作区根>/<账号 ID>`),
|
||||||
|
不同账号的设计源码互不可见;同一账号的新会话继续使用自己的工作区。
|
||||||
|
|
||||||
|
## 安装(Codex)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 先登录(一次性;需要 deta.city 账号)
|
||||||
|
node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs
|
||||||
|
# → 写入 ~/.deta/credentials.json 与 ~/.deta/env(均 0600)
|
||||||
|
|
||||||
|
# 2. 让 DETA_TOKEN 进入当前 shell 环境
|
||||||
|
source ~/.deta/env
|
||||||
|
|
||||||
|
# 3. 安装插件
|
||||||
|
codex plugin marketplace add https://git.deta.city/deta/deta-agent-plugins.git
|
||||||
|
codex plugin add deta@deta
|
||||||
|
```
|
||||||
|
|
||||||
|
登录助手也支持自建/预发环境:`node bin/deta-login.mjs --base-url https://<host>`。
|
||||||
|
|
||||||
|
## 安装(dsh)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ~/Dev/ant/deta-agent-plugins/plugins/deta/bin/deta-login.mjs
|
||||||
|
source ~/.deta/env
|
||||||
|
dsh plugin --profile <name> add ~/Dev/ant/deta-agent-plugins/plugins/deta
|
||||||
|
```
|
||||||
|
|
||||||
|
`cordis.patch.yml` 默认启用远程 `streamable-http` 段(Bearer 取自 `DETA_TOKEN`)。
|
||||||
|
|
||||||
|
## 本地 stdio 形态(开发用)
|
||||||
|
|
||||||
|
`bin/deta-mcp.sh` 走本机 stdio(`node dist/src/headless/mcp-server.js`),
|
||||||
|
**仅本机开发用,默认关闭**:不校验 deta.city 账号、不隔离工作区。
|
||||||
|
需要在本机迭代领域能力时,打开 `cordis.patch.yml` 里注释掉的 stdio 段
|
||||||
|
(并注释掉远程段),设置 `DETA_SERVER_ROOT` 指向已构建的领域服务仓库:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Dev/ant/ant-copilot && npm run build
|
||||||
|
node dist/src/headless/mcp-server.js # 冒烟:应等待 stdin/stdout
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件
|
||||||
|
|
||||||
|
```
|
||||||
|
.codex-plugin/plugin.json Codex 插件清单
|
||||||
|
.mcp.json Codex MCP 声明(远程 streamable-http)
|
||||||
|
package.json dsh 组合包声明(dsh.bundle.patch)
|
||||||
|
cordis.patch.yml dsh 配置层(挂载 MCP 服务)
|
||||||
|
bin/deta-login.mjs 登录助手(写 ~/.deta/credentials.json + ~/.deta/env)
|
||||||
|
bin/deta-mcp.sh 本地 stdio 启动器(开发用)
|
||||||
|
skills/aml-design/SKILL.md 领域技能
|
||||||
|
```
|
||||||
Executable
+228
@@ -0,0 +1,228 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* DETA 登录助手(Node ESM,零第三方依赖)
|
||||||
|
*
|
||||||
|
* 用 deta.city 账号换取 API 令牌,供 MCP 远程端点鉴权使用:
|
||||||
|
*
|
||||||
|
* node bin/deta-login.mjs [--base-url https://deta.city]
|
||||||
|
*
|
||||||
|
* 流程:
|
||||||
|
* 1. 交互式读取用户名/密码(密码不回显);
|
||||||
|
* 2. POST <base>/prod-api/login { username, password };
|
||||||
|
* 3. 成功 → 写 ~/.deta/credentials.json 与 ~/.deta/env(均 0600);
|
||||||
|
* 4. 失败 → 打印服务端 msg,退出码非 0。
|
||||||
|
*
|
||||||
|
* 凭据纪律:token 只写在本次 stdout 与上述两个 0600 文件里,
|
||||||
|
* 不写日志、不写 stderr、不写其它位置。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdir, writeFile, chmod } from 'node:fs/promises';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
|
||||||
|
const DEFAULT_BASE_URL = 'https://deta.city';
|
||||||
|
const REQUEST_TIMEOUT_MS = 20_000;
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(`[deta-login] ${message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `--base-url <url>` / `--base-url=<url>`;缺省 https://deta.city,末尾斜杠去掉 */
|
||||||
|
function parseBaseUrl(argv) {
|
||||||
|
let baseUrl = process.env.DETA_BASE_URL || DEFAULT_BASE_URL;
|
||||||
|
for (let i = 0; i < argv.length; i++) {
|
||||||
|
const arg = argv[i];
|
||||||
|
if (arg === '--base-url') {
|
||||||
|
const next = argv[i + 1];
|
||||||
|
if (!next) fail('--base-url 缺少取值');
|
||||||
|
baseUrl = next;
|
||||||
|
i++;
|
||||||
|
} else if (arg.startsWith('--base-url=')) {
|
||||||
|
baseUrl = arg.slice('--base-url='.length);
|
||||||
|
} else if (arg === '-h' || arg === '--help') {
|
||||||
|
console.log('用法:node bin/deta-login.mjs [--base-url https://deta.city]');
|
||||||
|
process.exit(0);
|
||||||
|
} else {
|
||||||
|
fail(`未知参数:${arg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseUrl.trim().replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读一行输入。hidden=true 时不回显(密码);
|
||||||
|
* 非 TTY(管道/CI)时同样按行读取,便于自动化。
|
||||||
|
*/
|
||||||
|
let pendingInput = '';
|
||||||
|
|
||||||
|
/** 从暂存缓冲里切出一行(跨 chunk 保留剩余数据),无完整行返回 null */
|
||||||
|
function takeLine() {
|
||||||
|
const index = pendingInput.search(/[\r\n]/);
|
||||||
|
if (index < 0) return null;
|
||||||
|
const line = pendingInput.slice(0, index);
|
||||||
|
const skip = pendingInput[index] === '\r' && pendingInput[index + 1] === '\n' ? 2 : 1;
|
||||||
|
pendingInput = pendingInput.slice(index + skip);
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLine({ hidden }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 上一次读取可能已经缓冲了后续行(管道一次性写入多行)
|
||||||
|
const buffered = takeLine();
|
||||||
|
if (buffered !== null) {
|
||||||
|
resolve(buffered);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdin = process.stdin;
|
||||||
|
const isTTY = Boolean(stdin.isTTY);
|
||||||
|
let buffer = '';
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
stdin.removeListener('data', onData);
|
||||||
|
stdin.removeListener('end', onEnd);
|
||||||
|
stdin.removeListener('error', onError);
|
||||||
|
if (isTTY && typeof stdin.setRawMode === 'function') stdin.setRawMode(false);
|
||||||
|
stdin.pause();
|
||||||
|
};
|
||||||
|
const finish = (value) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
// TTY 下 raw mode 不回显回车,这里补一个换行;非 TTY(管道)不额外输出
|
||||||
|
if (isTTY) process.stdout.write('\n');
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
const onData = (chunk) => {
|
||||||
|
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
||||||
|
if (!isTTY) {
|
||||||
|
// 非交互:不回显,按行切分,剩余数据留给下一次读取
|
||||||
|
pendingInput += text;
|
||||||
|
const line = takeLine();
|
||||||
|
if (line !== null) finish(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const ch of [...text]) {
|
||||||
|
if (ch === '\n' || ch === '\r') return finish(buffer);
|
||||||
|
if (ch === '\u0003') {
|
||||||
|
// Ctrl-C
|
||||||
|
process.stdout.write('\n');
|
||||||
|
process.exit(130);
|
||||||
|
}
|
||||||
|
if (ch === '\u0004') return finish(buffer); // Ctrl-D
|
||||||
|
if (ch === '\u007f' || ch === '\b') {
|
||||||
|
if (buffer && !hidden) process.stdout.write('\b \b');
|
||||||
|
buffer = buffer.slice(0, -1);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
buffer += ch;
|
||||||
|
if (!hidden) process.stdout.write(ch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 输入结束:非 TTY 时把残留(末行无换行)也交出去
|
||||||
|
const onEnd = () => {
|
||||||
|
if (!isTTY && pendingInput) {
|
||||||
|
const rest = pendingInput;
|
||||||
|
pendingInput = '';
|
||||||
|
finish(rest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
finish(buffer);
|
||||||
|
};
|
||||||
|
const onError = (err) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
cleanup();
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isTTY && typeof stdin.setRawMode === 'function') stdin.setRawMode(true);
|
||||||
|
stdin.resume();
|
||||||
|
stdin.on('data', onData);
|
||||||
|
stdin.on('end', onEnd);
|
||||||
|
stdin.on('error', onError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单引号安全转义,供 `export DETA_TOKEN='...'` 使用 */
|
||||||
|
function shellSingleQuote(value) {
|
||||||
|
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writePrivateFile(filePath, content) {
|
||||||
|
await writeFile(filePath, content, { mode: 0o600 });
|
||||||
|
// 已存在的文件不被 mode 改写,显式收紧
|
||||||
|
await chmod(filePath, 0o600).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const baseUrl = parseBaseUrl(process.argv.slice(2));
|
||||||
|
|
||||||
|
process.stdout.write(`DETA 登录(${baseUrl})\n`);
|
||||||
|
process.stdout.write('用户名:');
|
||||||
|
const username = (await readLine({ hidden: false })).trim();
|
||||||
|
if (!username) fail('用户名不能为空');
|
||||||
|
|
||||||
|
process.stdout.write('密码:');
|
||||||
|
const password = await readLine({ hidden: true });
|
||||||
|
if (!password) fail('密码不能为空');
|
||||||
|
|
||||||
|
const loginUrl = `${baseUrl}/prod-api/login`;
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(loginUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
fail(`无法连接 ${loginUrl}(${err instanceof Error ? err.message : String(err)})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload = null;
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch {
|
||||||
|
payload = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = typeof payload?.token === 'string' ? payload.token : '';
|
||||||
|
if (!response.ok || !token || (typeof payload?.code === 'number' && payload.code !== 200)) {
|
||||||
|
const msg = payload?.msg || `登录失败(HTTP ${response.status})`;
|
||||||
|
fail(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginUsername = typeof payload.username === 'string' && payload.username ? payload.username : username;
|
||||||
|
const dir = join(homedir(), '.deta');
|
||||||
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
||||||
|
await chmod(dir, 0o700).catch(() => undefined);
|
||||||
|
|
||||||
|
const credentials = {
|
||||||
|
token,
|
||||||
|
username: loginUsername,
|
||||||
|
baseUrl,
|
||||||
|
loginAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const credentialsPath = join(dir, 'credentials.json');
|
||||||
|
const envPath = join(dir, 'env');
|
||||||
|
await writePrivateFile(credentialsPath, `${JSON.stringify(credentials, null, 2)}\n`);
|
||||||
|
await writePrivateFile(
|
||||||
|
envPath,
|
||||||
|
`# DETA MCP 凭据(由 bin/deta-login.mjs 生成,勿提交版本库)\n` +
|
||||||
|
`# 用法:source ~/.deta/env\n` +
|
||||||
|
`export DETA_TOKEN=${shellSingleQuote(token)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// token 只在这里(stdout)出现一次;stderr 与其它文件均不含凭据
|
||||||
|
console.log(`DETA_TOKEN=${token}`);
|
||||||
|
console.log(`✅ 登录成功:${loginUsername} @ ${baseUrl}`);
|
||||||
|
console.log(`凭据已写入 ${credentialsPath}(0600),环境变量已写入 ${envPath}(0600)`);
|
||||||
|
console.log(`使用:source ~/.deta/env 后再启动 harness(DETA_TOKEN 随环境传给 https://deta.city/mcp-api/mcp)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
fail(err instanceof Error ? err.message : String(err));
|
||||||
|
});
|
||||||
@@ -4,29 +4,37 @@
|
|||||||
# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。
|
# 本层只做一件事:把 DETA 领域能力作为 MCP 服务挂进 harness。
|
||||||
# 工具以 mcp__deta__<tool> 暴露(与 Codex 侧同名)。
|
# 工具以 mcp__deta__<tool> 暴露(与 Codex 侧同名)。
|
||||||
#
|
#
|
||||||
# ── 公开发布形态(远程端点,推荐)─────────────────────────────
|
# 凭据:deta.city 账号。先 `node bin/deta-login.mjs` 登录(写入 ~/.deta/credentials.json
|
||||||
# 服务端化完成后启用本节,并删除下面的本地 stdio 节。
|
# 与 ~/.deta/env),再 `source ~/.deta/env` 让 DETA_TOKEN 进入环境。
|
||||||
|
# 端点 https://deta.city/mcp-api/mcp 除 /health 外都要鉴权,无凭据一律 401。
|
||||||
|
#
|
||||||
|
# ── 远程端点形态(Streamable HTTP,默认启用)───────────────────
|
||||||
|
# 服务端:deta.city 上的无头 MCP 服务(ant-copilot src/headless/mcp-http-server.ts),
|
||||||
|
# 经 Nginx 反代暴露;各账号的工作区在服务端按账号隔离。
|
||||||
|
#
|
||||||
|
- insert:
|
||||||
|
- id: deta-mcp
|
||||||
|
name: '@deepseek-ai/dsh-mcp-client'
|
||||||
|
config:
|
||||||
|
serverName: deta
|
||||||
|
transport: streamable-http
|
||||||
|
url: https://deta.city/mcp-api/mcp
|
||||||
|
headers:
|
||||||
|
Authorization: !!js '`Bearer ${process.env.DETA_TOKEN ?? ''}`'
|
||||||
|
#
|
||||||
|
# ── 本地形态(本机开发用,默认关闭)───────────────────────────
|
||||||
|
# 走 stdio 直连本机已构建的领域服务,不校验 deta.city 账号、不隔离工作区,
|
||||||
|
# 只在迭代领域能力时临时启用:把下面整节取消注释,同时注释掉上面的远程节。
|
||||||
|
# 需要 DETA_SERVER_ROOT 指向已构建的领域服务仓库(缺省见启动器脚本)。
|
||||||
#
|
#
|
||||||
# - insert:
|
# - insert:
|
||||||
# - id: deta-mcp
|
# - id: deta-mcp
|
||||||
# name: '@deepseek-ai/dsh-mcp-client'
|
# name: '@deepseek-ai/dsh-mcp-client'
|
||||||
# config:
|
# config:
|
||||||
# serverName: deta
|
# serverName: deta
|
||||||
# transport: streamable-http
|
# transport: stdio
|
||||||
# url: https://deta.city/workspace-api/mcp
|
# command: node
|
||||||
# headers:
|
# args:
|
||||||
# Authorization: !!js '`Bearer ${process.env.DETA_TOKEN ?? ''}`'
|
# - !!js '`${process.env.DETA_SERVER_ROOT ?? process.env.HOME + "/Dev/ant/ant-copilot"}/dist/src/headless/mcp-server.js`'
|
||||||
#
|
# env:
|
||||||
# ── 本地形态(当前可用:本机已构建的领域服务,stdio)─────────
|
# ANT_HEADLESS_WORKSPACE: !!js process.env.ANT_HEADLESS_WORKSPACE ?? ''
|
||||||
# 需要 DETA_SERVER_ROOT 指向已构建的领域服务仓库(缺省见启动器脚本)。
|
|
||||||
- insert:
|
|
||||||
- id: deta-mcp
|
|
||||||
name: '@deepseek-ai/dsh-mcp-client'
|
|
||||||
config:
|
|
||||||
serverName: deta
|
|
||||||
transport: stdio
|
|
||||||
command: node
|
|
||||||
args:
|
|
||||||
- !!js '`${process.env.DETA_SERVER_ROOT ?? process.env.HOME + "/Dev/ant/ant-copilot"}/dist/src/headless/mcp-server.js`'
|
|
||||||
env:
|
|
||||||
ANT_HEADLESS_WORKSPACE: !!js process.env.ANT_HEADLESS_WORKSPACE ?? ''
|
|
||||||
|
|||||||
Reference in New Issue
Block a user