@muleiwu/mdoc-mcp 0.1.0
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/.claude/settings.local.json +8 -0
- package/.env.example +8 -0
- package/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +121 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +111 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/get-article-content.d.ts +18 -0
- package/dist/tools/get-article-content.d.ts.map +1 -0
- package/dist/tools/get-article-content.js +45 -0
- package/dist/tools/get-article-content.js.map +1 -0
- package/dist/tools/get-manifest.d.ts +13 -0
- package/dist/tools/get-manifest.d.ts.map +1 -0
- package/dist/tools/get-manifest.js +33 -0
- package/dist/tools/get-manifest.js.map +1 -0
- package/dist/url-parser.d.ts +24 -0
- package/dist/url-parser.d.ts.map +1 -0
- package/dist/url-parser.js +94 -0
- package/dist/url-parser.js.map +1 -0
- package/package.json +26 -0
- package/src/client.ts +184 -0
- package/src/index.ts +127 -0
- package/src/tools/get-article-content.ts +66 -0
- package/src/tools/get-manifest.ts +45 -0
- package/src/url-parser.ts +114 -0
- package/tsconfig.json +17 -0
package/.env.example
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 muleiwu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# mdoc-mcp
|
|
2
|
+
|
|
3
|
+
[mdoc](https://mdoc.cc) 的 MCP (Model Context Protocol) Server,让 AI 能够直接读取 mdoc 文档的目录结构和文章内容。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- **`get_manifest`** — 获取文档目录清单(Markdown 格式),包含所有文章的层级结构和链接
|
|
8
|
+
- **`get_article_content`** — 获取文章的原始 Markdown 内容
|
|
9
|
+
|
|
10
|
+
## 安装与配置
|
|
11
|
+
|
|
12
|
+
### 1. 克隆并构建
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://github.com/muleiwu/mdoc-mcp.git
|
|
16
|
+
cd mdoc-mcp
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### 2. 配置 API Key
|
|
22
|
+
|
|
23
|
+
在 [mdoc](https://mdoc.cc) 用户设置页面创建 API Key,获取 `AccessKeyID` 和 `SecretAccessKey`。
|
|
24
|
+
|
|
25
|
+
鉴权使用 AWS Signature Version 4 协议,与服务端完全兼容。
|
|
26
|
+
|
|
27
|
+
### 3. 在 Cursor / Claude Desktop 中配置
|
|
28
|
+
|
|
29
|
+
编辑 MCP 配置文件(如 `~/.cursor/mcp.json` 或 Claude Desktop 的配置),添加:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"mdoc": {
|
|
35
|
+
"command": "node",
|
|
36
|
+
"args": ["/path/to/mdoc-mcp/dist/index.js"],
|
|
37
|
+
"env": {
|
|
38
|
+
"MDOC_ACCESS_KEY_ID": "your_access_key_id",
|
|
39
|
+
"MDOC_SECRET_ACCESS_KEY": "your_secret_access_key"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
如果使用私有部署的 mdoc,额外添加:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
"MDOC_API_BASE_URL": "https://your-mdoc-instance.com"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 工具说明
|
|
53
|
+
|
|
54
|
+
### `get_manifest` — 获取文档目录
|
|
55
|
+
|
|
56
|
+
获取文档的完整目录清单,AI 可据此了解文档结构并选择要读取的文章。
|
|
57
|
+
|
|
58
|
+
**参数**(提供 `url` 或 `orgSlug` + `docSlug` 之一):
|
|
59
|
+
|
|
60
|
+
| 参数 | 类型 | 说明 |
|
|
61
|
+
|------|------|------|
|
|
62
|
+
| `url` | string (可选) | mdoc 网址,如 `https://mdoc.cc/mliev/1ms` 或带版本 `https://mdoc.cc/mliev/1ms/v1.0.0` |
|
|
63
|
+
| `orgSlug` | string (可选) | 组织标识,如 `mliev` |
|
|
64
|
+
| `docSlug` | string (可选) | 文档标识,如 `1ms` |
|
|
65
|
+
| `version` | string (可选) | 版本名称,如 `v1.0.0`,不指定则使用默认版本 |
|
|
66
|
+
|
|
67
|
+
**返回**:Markdown 格式的文档目录,包含文章链接(可直接用于 `get_article_content`)
|
|
68
|
+
|
|
69
|
+
### `get_article_content` — 获取文章内容
|
|
70
|
+
|
|
71
|
+
获取指定文章的原始 Markdown 内容。
|
|
72
|
+
|
|
73
|
+
**参数**(提供 `url` 或 `orgSlug` + `docSlug` + `articleId` 之一):
|
|
74
|
+
|
|
75
|
+
| 参数 | 类型 | 说明 |
|
|
76
|
+
|------|------|------|
|
|
77
|
+
| `url` | string (可选) | 网址或 manifest 中的 content.md 链接 |
|
|
78
|
+
| `orgSlug` | string (可选) | 组织标识 |
|
|
79
|
+
| `docSlug` | string (可选) | 文档标识 |
|
|
80
|
+
| `articleId` | string (可选) | 文章 ID |
|
|
81
|
+
| `version` | string (可选) | 版本名称 |
|
|
82
|
+
|
|
83
|
+
支持的 `url` 格式:
|
|
84
|
+
- `https://mdoc.cc/mliev/1ms/v1.0.0/16`(mdoc 网址,第 4 段为 articleId)
|
|
85
|
+
- `https://mdoc.cc/openapi/organizations/mliev/documents/1ms/articles/16/content.md`(manifest 返回的 API 链接)
|
|
86
|
+
|
|
87
|
+
## 典型使用流程
|
|
88
|
+
|
|
89
|
+
1. AI 调用 `get_manifest` 获取文档目录
|
|
90
|
+
2. 从目录中找到相关文章的链接
|
|
91
|
+
3. AI 调用 `get_article_content` 读取具体文章内容
|
|
92
|
+
|
|
93
|
+
## 环境变量
|
|
94
|
+
|
|
95
|
+
| 变量 | 必需 | 默认值 | 说明 |
|
|
96
|
+
|------|------|--------|------|
|
|
97
|
+
| `MDOC_ACCESS_KEY_ID` | 是 | — | API Key 的 Access Key ID |
|
|
98
|
+
| `MDOC_SECRET_ACCESS_KEY` | 是 | — | API Key 的 Secret Access Key |
|
|
99
|
+
| `MDOC_API_BASE_URL` | 否 | `https://mdoc.cc` | API 基础地址(私有部署时使用) |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface MdocClientConfig {
|
|
2
|
+
accessKeyId: string;
|
|
3
|
+
secretAccessKey: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RequestOptions {
|
|
7
|
+
path: string;
|
|
8
|
+
query?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export declare class MdocClient {
|
|
11
|
+
private config;
|
|
12
|
+
private readonly service;
|
|
13
|
+
private readonly region;
|
|
14
|
+
constructor(config: MdocClientConfig);
|
|
15
|
+
/**
|
|
16
|
+
* 发起带 AWS Signature V4 签名的 GET 请求
|
|
17
|
+
*/
|
|
18
|
+
get(options: RequestOptions): Promise<string>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从环境变量创建客户端实例
|
|
22
|
+
*/
|
|
23
|
+
export declare function createClientFromEnv(): MdocClient;
|
|
24
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,gBAAgB;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AA4ED,qBAAa,UAAU;IACrB,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAU;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAe;gBAE1B,MAAM,EAAE,gBAAgB;IAIpC;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC;CA+DpD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,UAAU,CAahD"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { createHmac, createHash } from 'crypto';
|
|
2
|
+
import { request as httpsRequest } from 'https';
|
|
3
|
+
import { request as httpRequest } from 'http';
|
|
4
|
+
function hmac(key, data) {
|
|
5
|
+
return createHmac('sha256', key).update(data).digest();
|
|
6
|
+
}
|
|
7
|
+
function sha256Hex(data) {
|
|
8
|
+
return createHash('sha256').update(data).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 构建 AWS Signature V4 Authorization 头
|
|
12
|
+
*
|
|
13
|
+
* 注意:Go 的 net/http 服务器会将 Host 头从 req.Header 移到 req.Host,
|
|
14
|
+
* 导致 gohttpsig 的 CanonicalizeHeaders 无法找到 host 的值。
|
|
15
|
+
* 因此这里签名时不包含 host 头,只签 x-amz-date。
|
|
16
|
+
*/
|
|
17
|
+
function buildAuthHeader(accessKeyId, secretAccessKey, method, path, query, dateTime, service, region) {
|
|
18
|
+
const dateShort = dateTime.slice(0, 8);
|
|
19
|
+
// 只签 x-amz-date(不含 host,因为服务端 Go HTTP 会将 Host 头移出 req.Header)
|
|
20
|
+
const signedHeaders = 'x-amz-date';
|
|
21
|
+
const canonicalHeaders = `x-amz-date:${dateTime}\n`;
|
|
22
|
+
const payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; // SHA256("")
|
|
23
|
+
const canonicalRequest = [
|
|
24
|
+
method,
|
|
25
|
+
path,
|
|
26
|
+
query,
|
|
27
|
+
canonicalHeaders,
|
|
28
|
+
signedHeaders,
|
|
29
|
+
payloadHash,
|
|
30
|
+
].join('\n');
|
|
31
|
+
const credentialScope = `${dateShort}/${region}/${service}/aws4_request`;
|
|
32
|
+
const stringToSign = [
|
|
33
|
+
'AWS4-HMAC-SHA256',
|
|
34
|
+
dateTime,
|
|
35
|
+
credentialScope,
|
|
36
|
+
sha256Hex(canonicalRequest),
|
|
37
|
+
].join('\n');
|
|
38
|
+
const signingKey = hmac(hmac(hmac(hmac(`AWS4${secretAccessKey}`, dateShort), region), service), 'aws4_request');
|
|
39
|
+
const signature = createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
|
40
|
+
return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
41
|
+
}
|
|
42
|
+
function readBody(res) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
46
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
47
|
+
res.on('error', reject);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export class MdocClient {
|
|
51
|
+
config;
|
|
52
|
+
service = 'mdoc';
|
|
53
|
+
region = 'us-east-1';
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.config = config;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 发起带 AWS Signature V4 签名的 GET 请求
|
|
59
|
+
*/
|
|
60
|
+
async get(options) {
|
|
61
|
+
const url = new URL(this.config.baseUrl);
|
|
62
|
+
const isHttps = url.protocol === 'https:';
|
|
63
|
+
const host = url.hostname;
|
|
64
|
+
const port = url.port
|
|
65
|
+
? parseInt(url.port)
|
|
66
|
+
: isHttps ? 443 : 80;
|
|
67
|
+
// 构建查询字符串(按 key 排序,符合 AWS 规范)
|
|
68
|
+
let queryString = '';
|
|
69
|
+
if (options.query && Object.keys(options.query).length > 0) {
|
|
70
|
+
const filtered = Object.entries(options.query).filter(([, v]) => v !== undefined && v !== '');
|
|
71
|
+
queryString = filtered
|
|
72
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
73
|
+
.sort()
|
|
74
|
+
.join('&');
|
|
75
|
+
}
|
|
76
|
+
const fullPath = queryString ? `${options.path}?${queryString}` : options.path;
|
|
77
|
+
// 生成时间戳(ISO8601 格式,去除特殊字符)
|
|
78
|
+
const dateTime = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
79
|
+
// 构建 Authorization 头
|
|
80
|
+
const authHeader = buildAuthHeader(this.config.accessKeyId, this.config.secretAccessKey, 'GET', options.path, queryString, dateTime, this.service, this.region);
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const reqOptions = {
|
|
83
|
+
hostname: host,
|
|
84
|
+
port,
|
|
85
|
+
path: fullPath,
|
|
86
|
+
method: 'GET',
|
|
87
|
+
headers: {
|
|
88
|
+
'X-Amz-Date': dateTime,
|
|
89
|
+
'Authorization': authHeader,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
const req = (isHttps ? httpsRequest : httpRequest)(reqOptions, async (res) => {
|
|
93
|
+
const body = await readBody(res);
|
|
94
|
+
const statusCode = res.statusCode ?? 0;
|
|
95
|
+
if (statusCode >= 400) {
|
|
96
|
+
reject(new Error(`HTTP ${statusCode}: ${body}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
resolve(body);
|
|
100
|
+
});
|
|
101
|
+
req.on('error', reject);
|
|
102
|
+
req.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 从环境变量创建客户端实例
|
|
108
|
+
*/
|
|
109
|
+
export function createClientFromEnv() {
|
|
110
|
+
const accessKeyId = process.env.MDOC_ACCESS_KEY_ID;
|
|
111
|
+
const secretAccessKey = process.env.MDOC_SECRET_ACCESS_KEY;
|
|
112
|
+
const baseUrl = process.env.MDOC_API_BASE_URL ?? 'https://mdoc.cc';
|
|
113
|
+
if (!accessKeyId) {
|
|
114
|
+
throw new Error('缺少环境变量 MDOC_ACCESS_KEY_ID');
|
|
115
|
+
}
|
|
116
|
+
if (!secretAccessKey) {
|
|
117
|
+
throw new Error('缺少环境变量 MDOC_SECRET_ACCESS_KEY');
|
|
118
|
+
}
|
|
119
|
+
return new MdocClient({ accessKeyId, secretAccessKey, baseUrl });
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import * as z from 'zod/v4';
|
|
5
|
+
import { createClientFromEnv } from './client.js';
|
|
6
|
+
import { getManifest } from './tools/get-manifest.js';
|
|
7
|
+
import { getArticleContent } from './tools/get-article-content.js';
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'mdoc-mcp',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
});
|
|
12
|
+
// Tool: get_manifest
|
|
13
|
+
server.registerTool('get_manifest', {
|
|
14
|
+
title: '获取文档目录清单',
|
|
15
|
+
description: '获取 mdoc 文档的目录清单(Markdown 格式),包含所有文章的标题和链接。' +
|
|
16
|
+
'可以通过 mdoc.cc 网址或 orgSlug/docSlug 参数指定文档。' +
|
|
17
|
+
'示例网址格式:https://mdoc.cc/mliev/1ms 或 https://mdoc.cc/mliev/1ms/v1.0.0',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
url: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('mdoc 文档网址,如 https://mdoc.cc/mliev/1ms 或 https://mdoc.cc/mliev/1ms/v1.0.0。' +
|
|
23
|
+
'提供 url 时,orgSlug 和 docSlug 可省略。'),
|
|
24
|
+
orgSlug: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe('组织标识,如 mliev。当未提供 url 时必填。'),
|
|
28
|
+
docSlug: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('文档标识,如 1ms。当未提供 url 时必填。'),
|
|
32
|
+
version: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('版本名称,如 v1.0.0。不指定则使用文档默认版本。'),
|
|
36
|
+
},
|
|
37
|
+
}, async (args) => {
|
|
38
|
+
try {
|
|
39
|
+
const client = createClientFromEnv();
|
|
40
|
+
const content = await getManifest(client, args);
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: content }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
return {
|
|
48
|
+
content: [{ type: 'text', text: `错误: ${message}` }],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
// Tool: get_article_content
|
|
54
|
+
server.registerTool('get_article_content', {
|
|
55
|
+
title: '获取文章内容',
|
|
56
|
+
description: '获取 mdoc 文档中某篇文章的原始 Markdown 内容。' +
|
|
57
|
+
'可通过 mdoc.cc 网址(含 articleId)、manifest 返回的 content.md 链接,' +
|
|
58
|
+
'或 orgSlug/docSlug/articleId 参数指定文章。' +
|
|
59
|
+
'示例网址格式:https://mdoc.cc/mliev/1ms/v1.0.0/16',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
url: z
|
|
62
|
+
.string()
|
|
63
|
+
.optional()
|
|
64
|
+
.describe('文章网址,支持两种格式:\n' +
|
|
65
|
+
'1. mdoc.cc 网址:https://mdoc.cc/mliev/1ms/v1.0.0/16\n' +
|
|
66
|
+
'2. manifest 中返回的 content.md API 链接,如 ' +
|
|
67
|
+
'https://mdoc.cc/openapi/organizations/mliev/documents/1ms/articles/16/content.md'),
|
|
68
|
+
orgSlug: z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe('组织标识,如 mliev。当未提供 url 时必填。'),
|
|
72
|
+
docSlug: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe('文档标识,如 1ms。当未提供 url 时必填。'),
|
|
76
|
+
articleId: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe('文章 ID,如 16。当未提供 url 时必填。'),
|
|
80
|
+
version: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.describe('版本名称,如 v1.0.0。不指定则使用文档默认版本。'),
|
|
84
|
+
},
|
|
85
|
+
}, async (args) => {
|
|
86
|
+
try {
|
|
87
|
+
const client = createClientFromEnv();
|
|
88
|
+
const content = await getArticleContent(client, args);
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: 'text', text: content }],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: 'text', text: `错误: ${message}` }],
|
|
97
|
+
isError: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
async function main() {
|
|
102
|
+
const transport = new StdioServerTransport();
|
|
103
|
+
await server.connect(transport);
|
|
104
|
+
// 使用 stderr 避免干扰 stdio 传输协议
|
|
105
|
+
process.stderr.write('mdoc MCP server 已启动(stdio 模式)\n');
|
|
106
|
+
}
|
|
107
|
+
main().catch((error) => {
|
|
108
|
+
process.stderr.write(`启动失败: ${error}\n`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
});
|
|
111
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAC5B,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAEnE,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,qBAAqB;AACrB,MAAM,CAAC,YAAY,CACjB,cAAc,EACd;IACE,KAAK,EAAE,UAAU;IACjB,WAAW,EACT,4CAA4C;QAC5C,0CAA0C;QAC1C,qEAAqE;IACvE,WAAW,EAAE;QACX,GAAG,EAAE,CAAC;aACH,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,2EAA2E;YAC3E,iCAAiC,CAClC;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,4BAA4B,CAAC;QACzC,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,0BAA0B,CAAC;QACvC,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,6BAA6B,CAAC;KAC3C;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAChD,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;SAC3C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,OAAO,EAAE,EAAE,CAAC;YACnD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,4BAA4B;AAC5B,MAAM,CAAC,YAAY,CACjB,qBAAqB,EACrB;IACE,KAAK,EAAE,QAAQ;IACf,WAAW,EACT,iCAAiC;QACjC,yDAAyD;QACzD,qCAAqC;QACrC,4CAA4C;IAC9C,WAAW,EAAE;QACX,GAAG,EAAE,CAAC;aACH,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,gBAAgB;YAChB,qDAAqD;YACrD,uCAAuC;YACvC,kFAAkF,CACnF;QACH,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,4BAA4B,CAAC;QACzC,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,0BAA0B,CAAC;QACvC,SAAS,EAAE,CAAC;aACT,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,0BAA0B,CAAC;QACvC,OAAO,EAAE,CAAC;aACP,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,6BAA6B,CAAC;KAC3C;CACF,EACD,KAAK,EAAE,IAAI,EAAE,EAAE;IACb,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,mBAAmB,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,MAAM,iBAAiB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACtD,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;SAC3C,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,OAAO,EAAE,EAAE,CAAC;YACnD,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;AACH,CAAC,CACF,CAAC;AAEF,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,4BAA4B;IAC5B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;AAC1D,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MdocClient } from '../client.js';
|
|
2
|
+
export interface GetArticleContentInput {
|
|
3
|
+
url?: string;
|
|
4
|
+
orgSlug?: string;
|
|
5
|
+
docSlug?: string;
|
|
6
|
+
articleId?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 获取文章的原始 Markdown 内容
|
|
11
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
12
|
+
*
|
|
13
|
+
* url 参数支持两种格式:
|
|
14
|
+
* 1. mdoc.cc 网址: https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
15
|
+
* 2. API content URL: .../openapi/organizations/.../articles/:articleId/content.md
|
|
16
|
+
*/
|
|
17
|
+
export declare function getArticleContent(client: MdocClient, input: GetArticleContentInput): Promise<string>;
|
|
18
|
+
//# sourceMappingURL=get-article-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-article-content.d.ts","sourceRoot":"","sources":["../../src/tools/get-article-content.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,WAAW,sBAAsB;IACrC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,MAAM,CAAC,CA2CjB"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { parseMdocUrl } from '../url-parser.js';
|
|
2
|
+
/**
|
|
3
|
+
* 获取文章的原始 Markdown 内容
|
|
4
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
5
|
+
*
|
|
6
|
+
* url 参数支持两种格式:
|
|
7
|
+
* 1. mdoc.cc 网址: https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
8
|
+
* 2. API content URL: .../openapi/organizations/.../articles/:articleId/content.md
|
|
9
|
+
*/
|
|
10
|
+
export async function getArticleContent(client, input) {
|
|
11
|
+
let orgSlug;
|
|
12
|
+
let docSlug;
|
|
13
|
+
let articleId;
|
|
14
|
+
let version = input.version;
|
|
15
|
+
if (input.url) {
|
|
16
|
+
const parsed = parseMdocUrl(input.url);
|
|
17
|
+
if (!parsed.articleId) {
|
|
18
|
+
throw new Error(`无法从 URL 中解析出 articleId,请确认 URL 格式正确(如 https://mdoc.cc/org/doc/v1.0.0/16),` +
|
|
19
|
+
`或单独提供 orgSlug、docSlug 和 articleId 参数`);
|
|
20
|
+
}
|
|
21
|
+
orgSlug = parsed.orgSlug;
|
|
22
|
+
docSlug = parsed.docSlug;
|
|
23
|
+
articleId = parsed.articleId;
|
|
24
|
+
if (!version && parsed.version) {
|
|
25
|
+
version = parsed.version;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (input.orgSlug && input.docSlug && input.articleId) {
|
|
29
|
+
orgSlug = input.orgSlug;
|
|
30
|
+
docSlug = input.docSlug;
|
|
31
|
+
articleId = input.articleId;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
throw new Error('请提供 url 参数,或同时提供 orgSlug、docSlug 和 articleId 参数');
|
|
35
|
+
}
|
|
36
|
+
const path = `/openapi/organizations/${encodeURIComponent(orgSlug)}` +
|
|
37
|
+
`/documents/${encodeURIComponent(docSlug)}` +
|
|
38
|
+
`/articles/${encodeURIComponent(articleId)}/content.md`;
|
|
39
|
+
const query = {};
|
|
40
|
+
if (version) {
|
|
41
|
+
query['version'] = version;
|
|
42
|
+
}
|
|
43
|
+
return client.get({ path, query });
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=get-article-content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-article-content.js","sourceRoot":"","sources":["../../src/tools/get-article-content.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAUhD;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,MAAkB,EAClB,KAA6B;IAE7B,IAAI,OAAe,CAAC;IACpB,IAAI,OAAe,CAAC;IACpB,IAAI,SAAiB,CAAC;IACtB,IAAI,OAAO,GAAuB,KAAK,CAAC,OAAO,CAAC;IAEhD,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAEvC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CACb,2EAA2E;gBAC3E,sCAAsC,CACvC,CAAC;QACJ,CAAC;QAED,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QACzB,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QACzB,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAC7B,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/B,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC3B,CAAC;IACH,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;QAC7D,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QACxB,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QACxB,SAAS,GAAG,KAAK,CAAC,SAAS,CAAC;IAC9B,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CACb,iDAAiD,CAClD,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GACR,0BAA0B,kBAAkB,CAAC,OAAO,CAAC,EAAE;QACvD,cAAc,kBAAkB,CAAC,OAAO,CAAC,EAAE;QAC3C,aAAa,kBAAkB,CAAC,SAAS,CAAC,aAAa,CAAC;IAE1D,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC;IAC7B,CAAC;IAED,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { MdocClient } from '../client.js';
|
|
2
|
+
export interface GetManifestInput {
|
|
3
|
+
url?: string;
|
|
4
|
+
orgSlug?: string;
|
|
5
|
+
docSlug?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 获取文档目录清单(Markdown 格式)
|
|
10
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/manifest
|
|
11
|
+
*/
|
|
12
|
+
export declare function getManifest(client: MdocClient, input: GetManifestInput): Promise<string>;
|
|
13
|
+
//# sourceMappingURL=get-manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-manifest.d.ts","sourceRoot":"","sources":["../../src/tools/get-manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1C,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,gBAAgB,GACtB,OAAO,CAAC,MAAM,CAAC,CA2BjB"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parseMdocUrl } from '../url-parser.js';
|
|
2
|
+
/**
|
|
3
|
+
* 获取文档目录清单(Markdown 格式)
|
|
4
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/manifest
|
|
5
|
+
*/
|
|
6
|
+
export async function getManifest(client, input) {
|
|
7
|
+
let orgSlug;
|
|
8
|
+
let docSlug;
|
|
9
|
+
let version = input.version;
|
|
10
|
+
if (input.url) {
|
|
11
|
+
const parsed = parseMdocUrl(input.url);
|
|
12
|
+
orgSlug = parsed.orgSlug;
|
|
13
|
+
docSlug = parsed.docSlug;
|
|
14
|
+
// URL 中解析到的版本优先级低于显式传入的 version 参数
|
|
15
|
+
if (!version && parsed.version) {
|
|
16
|
+
version = parsed.version;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else if (input.orgSlug && input.docSlug) {
|
|
20
|
+
orgSlug = input.orgSlug;
|
|
21
|
+
docSlug = input.docSlug;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error('请提供 url 参数,或同时提供 orgSlug 和 docSlug 参数');
|
|
25
|
+
}
|
|
26
|
+
const path = `/openapi/organizations/${encodeURIComponent(orgSlug)}/documents/${encodeURIComponent(docSlug)}/manifest`;
|
|
27
|
+
const query = {};
|
|
28
|
+
if (version) {
|
|
29
|
+
query['version'] = version;
|
|
30
|
+
}
|
|
31
|
+
return client.get({ path, query });
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=get-manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"get-manifest.js","sourceRoot":"","sources":["../../src/tools/get-manifest.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAShD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAkB,EAClB,KAAuB;IAEvB,IAAI,OAAe,CAAC;IACpB,IAAI,OAAe,CAAC;IACpB,IAAI,OAAO,GAAuB,KAAK,CAAC,OAAO,CAAC;IAEhD,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACd,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACvC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QACzB,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QACzB,mCAAmC;QACnC,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC/B,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC3B,CAAC;IACH,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAC1C,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QACxB,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,IAAI,GAAG,0BAA0B,kBAAkB,CAAC,OAAO,CAAC,cAAc,kBAAkB,CAAC,OAAO,CAAC,WAAW,CAAC;IACvH,MAAM,KAAK,GAA2B,EAAE,CAAC;IACzC,IAAI,OAAO,EAAE,CAAC;QACZ,KAAK,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC;IAC7B,CAAC;IAED,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AACrC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdoc URL 解析器
|
|
3
|
+
*
|
|
4
|
+
* 支持以下格式:
|
|
5
|
+
* 1. mdoc.cc 网站 URL:
|
|
6
|
+
* - https://mdoc.cc/:orgSlug/:docSlug
|
|
7
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version
|
|
8
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
9
|
+
*
|
|
10
|
+
* 2. OpenAPI content URL (从 manifest 返回的链接):
|
|
11
|
+
* - .../openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
12
|
+
*/
|
|
13
|
+
export interface ParsedMdocUrl {
|
|
14
|
+
orgSlug: string;
|
|
15
|
+
docSlug: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
articleId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 解析 mdoc URL,提取参数
|
|
21
|
+
* @throws {Error} 如果 URL 格式无法识别
|
|
22
|
+
*/
|
|
23
|
+
export declare function parseMdocUrl(rawUrl: string): ParsedMdocUrl;
|
|
24
|
+
//# sourceMappingURL=url-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-parser.d.ts","sourceRoot":"","sources":["../src/url-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAiBD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CA0E1D"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdoc URL 解析器
|
|
3
|
+
*
|
|
4
|
+
* 支持以下格式:
|
|
5
|
+
* 1. mdoc.cc 网站 URL:
|
|
6
|
+
* - https://mdoc.cc/:orgSlug/:docSlug
|
|
7
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version
|
|
8
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
9
|
+
*
|
|
10
|
+
* 2. OpenAPI content URL (从 manifest 返回的链接):
|
|
11
|
+
* - .../openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 判断字符串是否看起来像版本号(以字母开头,如 v1.0.0,或纯数字组成的版本)
|
|
15
|
+
* 版本号特征:包含字母前缀或包含点号
|
|
16
|
+
*/
|
|
17
|
+
function looksLikeVersion(segment) {
|
|
18
|
+
return /^v\d/.test(segment) || /^\d+\.\d+/.test(segment);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 判断字符串是否看起来像文章 ID(纯数字)
|
|
22
|
+
*/
|
|
23
|
+
function looksLikeArticleId(segment) {
|
|
24
|
+
return /^\d+$/.test(segment);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 解析 mdoc URL,提取参数
|
|
28
|
+
* @throws {Error} 如果 URL 格式无法识别
|
|
29
|
+
*/
|
|
30
|
+
export function parseMdocUrl(rawUrl) {
|
|
31
|
+
// 补全协议头
|
|
32
|
+
if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://')) {
|
|
33
|
+
rawUrl = 'https://' + rawUrl;
|
|
34
|
+
}
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(rawUrl);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`无效的 URL 格式: ${rawUrl}`);
|
|
41
|
+
}
|
|
42
|
+
const pathname = url.pathname;
|
|
43
|
+
// 去掉首尾斜杠,拆分路径段
|
|
44
|
+
const segments = pathname.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
|
|
45
|
+
// 尝试匹配 OpenAPI content URL:
|
|
46
|
+
// /openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
47
|
+
const openapiMatch = pathname.match(/\/openapi\/organizations\/([^/]+)\/documents\/([^/]+)\/articles\/([^/]+)\/content\.md/);
|
|
48
|
+
if (openapiMatch) {
|
|
49
|
+
return {
|
|
50
|
+
orgSlug: openapiMatch[1],
|
|
51
|
+
docSlug: openapiMatch[2],
|
|
52
|
+
articleId: openapiMatch[3],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// 尝试匹配 OpenAPI manifest URL:
|
|
56
|
+
// /openapi/organizations/:orgSlug/documents/:docSlug/manifest
|
|
57
|
+
const manifestMatch = pathname.match(/\/openapi\/organizations\/([^/]+)\/documents\/([^/]+)\/manifest/);
|
|
58
|
+
if (manifestMatch) {
|
|
59
|
+
const version = url.searchParams.get('version') ?? undefined;
|
|
60
|
+
return {
|
|
61
|
+
orgSlug: manifestMatch[1],
|
|
62
|
+
docSlug: manifestMatch[2],
|
|
63
|
+
version,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// 解析 mdoc.cc 风格的网址: /:orgSlug/:docSlug[/:version[/:articleId]]
|
|
67
|
+
if (segments.length < 2) {
|
|
68
|
+
throw new Error(`URL 路径段不足,期望格式: /:orgSlug/:docSlug[/:version[/:articleId]],实际路径: ${pathname}`);
|
|
69
|
+
}
|
|
70
|
+
const [orgSlug, docSlug, seg3, seg4] = segments;
|
|
71
|
+
let version;
|
|
72
|
+
let articleId;
|
|
73
|
+
if (seg3) {
|
|
74
|
+
if (looksLikeArticleId(seg3) && !seg4) {
|
|
75
|
+
// 只有纯数字且没有第四段:当做 articleId
|
|
76
|
+
articleId = seg3;
|
|
77
|
+
}
|
|
78
|
+
else if (looksLikeVersion(seg3)) {
|
|
79
|
+
version = seg3;
|
|
80
|
+
if (seg4) {
|
|
81
|
+
articleId = seg4;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// 第三段既不像版本也不像 articleId,作为 version 兜底处理
|
|
86
|
+
version = seg3;
|
|
87
|
+
if (seg4) {
|
|
88
|
+
articleId = seg4;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { orgSlug, docSlug, version, articleId };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=url-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-parser.js","sourceRoot":"","sources":["../src/url-parser.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@muleiwu/mdoc-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for mdoc document reading via OpenAPI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mdoc-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsc --watch",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.5.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"typescript": "^5.7.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { createHmac, createHash } from 'crypto';
|
|
2
|
+
import { request as httpsRequest } from 'https';
|
|
3
|
+
import { request as httpRequest } from 'http';
|
|
4
|
+
import { IncomingMessage } from 'http';
|
|
5
|
+
|
|
6
|
+
export interface MdocClientConfig {
|
|
7
|
+
accessKeyId: string;
|
|
8
|
+
secretAccessKey: string;
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RequestOptions {
|
|
13
|
+
path: string;
|
|
14
|
+
query?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function hmac(key: Buffer | string, data: string): Buffer {
|
|
18
|
+
return createHmac('sha256', key).update(data).digest();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sha256Hex(data: string): string {
|
|
22
|
+
return createHash('sha256').update(data).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 构建 AWS Signature V4 Authorization 头
|
|
27
|
+
*
|
|
28
|
+
* 注意:Go 的 net/http 服务器会将 Host 头从 req.Header 移到 req.Host,
|
|
29
|
+
* 导致 gohttpsig 的 CanonicalizeHeaders 无法找到 host 的值。
|
|
30
|
+
* 因此这里签名时不包含 host 头,只签 x-amz-date。
|
|
31
|
+
*/
|
|
32
|
+
function buildAuthHeader(
|
|
33
|
+
accessKeyId: string,
|
|
34
|
+
secretAccessKey: string,
|
|
35
|
+
method: string,
|
|
36
|
+
path: string,
|
|
37
|
+
query: string,
|
|
38
|
+
dateTime: string,
|
|
39
|
+
service: string,
|
|
40
|
+
region: string
|
|
41
|
+
): string {
|
|
42
|
+
const dateShort = dateTime.slice(0, 8);
|
|
43
|
+
|
|
44
|
+
// 只签 x-amz-date(不含 host,因为服务端 Go HTTP 会将 Host 头移出 req.Header)
|
|
45
|
+
const signedHeaders = 'x-amz-date';
|
|
46
|
+
const canonicalHeaders = `x-amz-date:${dateTime}\n`;
|
|
47
|
+
const payloadHash = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; // SHA256("")
|
|
48
|
+
|
|
49
|
+
const canonicalRequest = [
|
|
50
|
+
method,
|
|
51
|
+
path,
|
|
52
|
+
query,
|
|
53
|
+
canonicalHeaders,
|
|
54
|
+
signedHeaders,
|
|
55
|
+
payloadHash,
|
|
56
|
+
].join('\n');
|
|
57
|
+
|
|
58
|
+
const credentialScope = `${dateShort}/${region}/${service}/aws4_request`;
|
|
59
|
+
const stringToSign = [
|
|
60
|
+
'AWS4-HMAC-SHA256',
|
|
61
|
+
dateTime,
|
|
62
|
+
credentialScope,
|
|
63
|
+
sha256Hex(canonicalRequest),
|
|
64
|
+
].join('\n');
|
|
65
|
+
|
|
66
|
+
const signingKey = hmac(
|
|
67
|
+
hmac(
|
|
68
|
+
hmac(
|
|
69
|
+
hmac(`AWS4${secretAccessKey}`, dateShort),
|
|
70
|
+
region
|
|
71
|
+
),
|
|
72
|
+
service
|
|
73
|
+
),
|
|
74
|
+
'aws4_request'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const signature = createHmac('sha256', signingKey).update(stringToSign).digest('hex');
|
|
78
|
+
|
|
79
|
+
return `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readBody(res: IncomingMessage): Promise<string> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const chunks: Buffer[] = [];
|
|
85
|
+
res.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
86
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
87
|
+
res.on('error', reject);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class MdocClient {
|
|
92
|
+
private config: MdocClientConfig;
|
|
93
|
+
private readonly service = 'mdoc';
|
|
94
|
+
private readonly region = 'us-east-1';
|
|
95
|
+
|
|
96
|
+
constructor(config: MdocClientConfig) {
|
|
97
|
+
this.config = config;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 发起带 AWS Signature V4 签名的 GET 请求
|
|
102
|
+
*/
|
|
103
|
+
async get(options: RequestOptions): Promise<string> {
|
|
104
|
+
const url = new URL(this.config.baseUrl);
|
|
105
|
+
const isHttps = url.protocol === 'https:';
|
|
106
|
+
const host = url.hostname;
|
|
107
|
+
const port = url.port
|
|
108
|
+
? parseInt(url.port)
|
|
109
|
+
: isHttps ? 443 : 80;
|
|
110
|
+
|
|
111
|
+
// 构建查询字符串(按 key 排序,符合 AWS 规范)
|
|
112
|
+
let queryString = '';
|
|
113
|
+
if (options.query && Object.keys(options.query).length > 0) {
|
|
114
|
+
const filtered = Object.entries(options.query).filter(([, v]) => v !== undefined && v !== '');
|
|
115
|
+
queryString = filtered
|
|
116
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
117
|
+
.sort()
|
|
118
|
+
.join('&');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fullPath = queryString ? `${options.path}?${queryString}` : options.path;
|
|
122
|
+
|
|
123
|
+
// 生成时间戳(ISO8601 格式,去除特殊字符)
|
|
124
|
+
const dateTime = new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
125
|
+
|
|
126
|
+
// 构建 Authorization 头
|
|
127
|
+
const authHeader = buildAuthHeader(
|
|
128
|
+
this.config.accessKeyId,
|
|
129
|
+
this.config.secretAccessKey,
|
|
130
|
+
'GET',
|
|
131
|
+
options.path,
|
|
132
|
+
queryString,
|
|
133
|
+
dateTime,
|
|
134
|
+
this.service,
|
|
135
|
+
this.region
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
const reqOptions = {
|
|
140
|
+
hostname: host,
|
|
141
|
+
port,
|
|
142
|
+
path: fullPath,
|
|
143
|
+
method: 'GET',
|
|
144
|
+
headers: {
|
|
145
|
+
'X-Amz-Date': dateTime,
|
|
146
|
+
'Authorization': authHeader,
|
|
147
|
+
} as Record<string, string>,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const req = (isHttps ? httpsRequest : httpRequest)(reqOptions, async (res) => {
|
|
151
|
+
const body = await readBody(res);
|
|
152
|
+
const statusCode = res.statusCode ?? 0;
|
|
153
|
+
|
|
154
|
+
if (statusCode >= 400) {
|
|
155
|
+
reject(new Error(`HTTP ${statusCode}: ${body}`));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolve(body);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
req.on('error', reject);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 从环境变量创建客户端实例
|
|
170
|
+
*/
|
|
171
|
+
export function createClientFromEnv(): MdocClient {
|
|
172
|
+
const accessKeyId = process.env.MDOC_ACCESS_KEY_ID;
|
|
173
|
+
const secretAccessKey = process.env.MDOC_SECRET_ACCESS_KEY;
|
|
174
|
+
const baseUrl = process.env.MDOC_API_BASE_URL ?? 'https://mdoc.cc';
|
|
175
|
+
|
|
176
|
+
if (!accessKeyId) {
|
|
177
|
+
throw new Error('缺少环境变量 MDOC_ACCESS_KEY_ID');
|
|
178
|
+
}
|
|
179
|
+
if (!secretAccessKey) {
|
|
180
|
+
throw new Error('缺少环境变量 MDOC_SECRET_ACCESS_KEY');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return new MdocClient({ accessKeyId, secretAccessKey, baseUrl });
|
|
184
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import * as z from 'zod/v4';
|
|
5
|
+
import { createClientFromEnv } from './client.js';
|
|
6
|
+
import { getManifest } from './tools/get-manifest.js';
|
|
7
|
+
import { getArticleContent } from './tools/get-article-content.js';
|
|
8
|
+
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'mdoc-mcp',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Tool: get_manifest
|
|
15
|
+
server.registerTool(
|
|
16
|
+
'get_manifest',
|
|
17
|
+
{
|
|
18
|
+
title: '获取文档目录清单',
|
|
19
|
+
description:
|
|
20
|
+
'获取 mdoc 文档的目录清单(Markdown 格式),包含所有文章的标题和链接。' +
|
|
21
|
+
'可以通过 mdoc.cc 网址或 orgSlug/docSlug 参数指定文档。' +
|
|
22
|
+
'示例网址格式:https://mdoc.cc/mliev/1ms 或 https://mdoc.cc/mliev/1ms/v1.0.0',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
url: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(
|
|
28
|
+
'mdoc 文档网址,如 https://mdoc.cc/mliev/1ms 或 https://mdoc.cc/mliev/1ms/v1.0.0。' +
|
|
29
|
+
'提供 url 时,orgSlug 和 docSlug 可省略。'
|
|
30
|
+
),
|
|
31
|
+
orgSlug: z
|
|
32
|
+
.string()
|
|
33
|
+
.optional()
|
|
34
|
+
.describe('组织标识,如 mliev。当未提供 url 时必填。'),
|
|
35
|
+
docSlug: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('文档标识,如 1ms。当未提供 url 时必填。'),
|
|
39
|
+
version: z
|
|
40
|
+
.string()
|
|
41
|
+
.optional()
|
|
42
|
+
.describe('版本名称,如 v1.0.0。不指定则使用文档默认版本。'),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
async (args) => {
|
|
46
|
+
try {
|
|
47
|
+
const client = createClientFromEnv();
|
|
48
|
+
const content = await getManifest(client, args);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: 'text', text: content }],
|
|
51
|
+
};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: 'text', text: `错误: ${message}` }],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Tool: get_article_content
|
|
63
|
+
server.registerTool(
|
|
64
|
+
'get_article_content',
|
|
65
|
+
{
|
|
66
|
+
title: '获取文章内容',
|
|
67
|
+
description:
|
|
68
|
+
'获取 mdoc 文档中某篇文章的原始 Markdown 内容。' +
|
|
69
|
+
'可通过 mdoc.cc 网址(含 articleId)、manifest 返回的 content.md 链接,' +
|
|
70
|
+
'或 orgSlug/docSlug/articleId 参数指定文章。' +
|
|
71
|
+
'示例网址格式:https://mdoc.cc/mliev/1ms/v1.0.0/16',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
url: z
|
|
74
|
+
.string()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(
|
|
77
|
+
'文章网址,支持两种格式:\n' +
|
|
78
|
+
'1. mdoc.cc 网址:https://mdoc.cc/mliev/1ms/v1.0.0/16\n' +
|
|
79
|
+
'2. manifest 中返回的 content.md API 链接,如 ' +
|
|
80
|
+
'https://mdoc.cc/openapi/organizations/mliev/documents/1ms/articles/16/content.md'
|
|
81
|
+
),
|
|
82
|
+
orgSlug: z
|
|
83
|
+
.string()
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('组织标识,如 mliev。当未提供 url 时必填。'),
|
|
86
|
+
docSlug: z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe('文档标识,如 1ms。当未提供 url 时必填。'),
|
|
90
|
+
articleId: z
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('文章 ID,如 16。当未提供 url 时必填。'),
|
|
94
|
+
version: z
|
|
95
|
+
.string()
|
|
96
|
+
.optional()
|
|
97
|
+
.describe('版本名称,如 v1.0.0。不指定则使用文档默认版本。'),
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
async (args) => {
|
|
101
|
+
try {
|
|
102
|
+
const client = createClientFromEnv();
|
|
103
|
+
const content = await getArticleContent(client, args);
|
|
104
|
+
return {
|
|
105
|
+
content: [{ type: 'text', text: content }],
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: `错误: ${message}` }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
async function main() {
|
|
118
|
+
const transport = new StdioServerTransport();
|
|
119
|
+
await server.connect(transport);
|
|
120
|
+
// 使用 stderr 避免干扰 stdio 传输协议
|
|
121
|
+
process.stderr.write('mdoc MCP server 已启动(stdio 模式)\n');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
main().catch((error) => {
|
|
125
|
+
process.stderr.write(`启动失败: ${error}\n`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { MdocClient } from '../client.js';
|
|
2
|
+
import { parseMdocUrl } from '../url-parser.js';
|
|
3
|
+
|
|
4
|
+
export interface GetArticleContentInput {
|
|
5
|
+
url?: string;
|
|
6
|
+
orgSlug?: string;
|
|
7
|
+
docSlug?: string;
|
|
8
|
+
articleId?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 获取文章的原始 Markdown 内容
|
|
14
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
15
|
+
*
|
|
16
|
+
* url 参数支持两种格式:
|
|
17
|
+
* 1. mdoc.cc 网址: https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
18
|
+
* 2. API content URL: .../openapi/organizations/.../articles/:articleId/content.md
|
|
19
|
+
*/
|
|
20
|
+
export async function getArticleContent(
|
|
21
|
+
client: MdocClient,
|
|
22
|
+
input: GetArticleContentInput
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
let orgSlug: string;
|
|
25
|
+
let docSlug: string;
|
|
26
|
+
let articleId: string;
|
|
27
|
+
let version: string | undefined = input.version;
|
|
28
|
+
|
|
29
|
+
if (input.url) {
|
|
30
|
+
const parsed = parseMdocUrl(input.url);
|
|
31
|
+
|
|
32
|
+
if (!parsed.articleId) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`无法从 URL 中解析出 articleId,请确认 URL 格式正确(如 https://mdoc.cc/org/doc/v1.0.0/16),` +
|
|
35
|
+
`或单独提供 orgSlug、docSlug 和 articleId 参数`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
orgSlug = parsed.orgSlug;
|
|
40
|
+
docSlug = parsed.docSlug;
|
|
41
|
+
articleId = parsed.articleId;
|
|
42
|
+
if (!version && parsed.version) {
|
|
43
|
+
version = parsed.version;
|
|
44
|
+
}
|
|
45
|
+
} else if (input.orgSlug && input.docSlug && input.articleId) {
|
|
46
|
+
orgSlug = input.orgSlug;
|
|
47
|
+
docSlug = input.docSlug;
|
|
48
|
+
articleId = input.articleId;
|
|
49
|
+
} else {
|
|
50
|
+
throw new Error(
|
|
51
|
+
'请提供 url 参数,或同时提供 orgSlug、docSlug 和 articleId 参数'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const path =
|
|
56
|
+
`/openapi/organizations/${encodeURIComponent(orgSlug)}` +
|
|
57
|
+
`/documents/${encodeURIComponent(docSlug)}` +
|
|
58
|
+
`/articles/${encodeURIComponent(articleId)}/content.md`;
|
|
59
|
+
|
|
60
|
+
const query: Record<string, string> = {};
|
|
61
|
+
if (version) {
|
|
62
|
+
query['version'] = version;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return client.get({ path, query });
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MdocClient } from '../client.js';
|
|
2
|
+
import { parseMdocUrl } from '../url-parser.js';
|
|
3
|
+
|
|
4
|
+
export interface GetManifestInput {
|
|
5
|
+
url?: string;
|
|
6
|
+
orgSlug?: string;
|
|
7
|
+
docSlug?: string;
|
|
8
|
+
version?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 获取文档目录清单(Markdown 格式)
|
|
13
|
+
* 对应 API: GET /openapi/organizations/:orgSlug/documents/:docSlug/manifest
|
|
14
|
+
*/
|
|
15
|
+
export async function getManifest(
|
|
16
|
+
client: MdocClient,
|
|
17
|
+
input: GetManifestInput
|
|
18
|
+
): Promise<string> {
|
|
19
|
+
let orgSlug: string;
|
|
20
|
+
let docSlug: string;
|
|
21
|
+
let version: string | undefined = input.version;
|
|
22
|
+
|
|
23
|
+
if (input.url) {
|
|
24
|
+
const parsed = parseMdocUrl(input.url);
|
|
25
|
+
orgSlug = parsed.orgSlug;
|
|
26
|
+
docSlug = parsed.docSlug;
|
|
27
|
+
// URL 中解析到的版本优先级低于显式传入的 version 参数
|
|
28
|
+
if (!version && parsed.version) {
|
|
29
|
+
version = parsed.version;
|
|
30
|
+
}
|
|
31
|
+
} else if (input.orgSlug && input.docSlug) {
|
|
32
|
+
orgSlug = input.orgSlug;
|
|
33
|
+
docSlug = input.docSlug;
|
|
34
|
+
} else {
|
|
35
|
+
throw new Error('请提供 url 参数,或同时提供 orgSlug 和 docSlug 参数');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const path = `/openapi/organizations/${encodeURIComponent(orgSlug)}/documents/${encodeURIComponent(docSlug)}/manifest`;
|
|
39
|
+
const query: Record<string, string> = {};
|
|
40
|
+
if (version) {
|
|
41
|
+
query['version'] = version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return client.get({ path, query });
|
|
45
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdoc URL 解析器
|
|
3
|
+
*
|
|
4
|
+
* 支持以下格式:
|
|
5
|
+
* 1. mdoc.cc 网站 URL:
|
|
6
|
+
* - https://mdoc.cc/:orgSlug/:docSlug
|
|
7
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version
|
|
8
|
+
* - https://mdoc.cc/:orgSlug/:docSlug/:version/:articleId
|
|
9
|
+
*
|
|
10
|
+
* 2. OpenAPI content URL (从 manifest 返回的链接):
|
|
11
|
+
* - .../openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export interface ParsedMdocUrl {
|
|
15
|
+
orgSlug: string;
|
|
16
|
+
docSlug: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
articleId?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 判断字符串是否看起来像版本号(以字母开头,如 v1.0.0,或纯数字组成的版本)
|
|
23
|
+
* 版本号特征:包含字母前缀或包含点号
|
|
24
|
+
*/
|
|
25
|
+
function looksLikeVersion(segment: string): boolean {
|
|
26
|
+
return /^v\d/.test(segment) || /^\d+\.\d+/.test(segment);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 判断字符串是否看起来像文章 ID(纯数字)
|
|
31
|
+
*/
|
|
32
|
+
function looksLikeArticleId(segment: string): boolean {
|
|
33
|
+
return /^\d+$/.test(segment);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析 mdoc URL,提取参数
|
|
38
|
+
* @throws {Error} 如果 URL 格式无法识别
|
|
39
|
+
*/
|
|
40
|
+
export function parseMdocUrl(rawUrl: string): ParsedMdocUrl {
|
|
41
|
+
// 补全协议头
|
|
42
|
+
if (!rawUrl.startsWith('http://') && !rawUrl.startsWith('https://')) {
|
|
43
|
+
rawUrl = 'https://' + rawUrl;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let url: URL;
|
|
47
|
+
try {
|
|
48
|
+
url = new URL(rawUrl);
|
|
49
|
+
} catch {
|
|
50
|
+
throw new Error(`无效的 URL 格式: ${rawUrl}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const pathname = url.pathname;
|
|
54
|
+
// 去掉首尾斜杠,拆分路径段
|
|
55
|
+
const segments = pathname.replace(/^\/|\/$/g, '').split('/').filter(Boolean);
|
|
56
|
+
|
|
57
|
+
// 尝试匹配 OpenAPI content URL:
|
|
58
|
+
// /openapi/organizations/:orgSlug/documents/:docSlug/articles/:articleId/content.md
|
|
59
|
+
const openapiMatch = pathname.match(
|
|
60
|
+
/\/openapi\/organizations\/([^/]+)\/documents\/([^/]+)\/articles\/([^/]+)\/content\.md/
|
|
61
|
+
);
|
|
62
|
+
if (openapiMatch) {
|
|
63
|
+
return {
|
|
64
|
+
orgSlug: openapiMatch[1],
|
|
65
|
+
docSlug: openapiMatch[2],
|
|
66
|
+
articleId: openapiMatch[3],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 尝试匹配 OpenAPI manifest URL:
|
|
71
|
+
// /openapi/organizations/:orgSlug/documents/:docSlug/manifest
|
|
72
|
+
const manifestMatch = pathname.match(
|
|
73
|
+
/\/openapi\/organizations\/([^/]+)\/documents\/([^/]+)\/manifest/
|
|
74
|
+
);
|
|
75
|
+
if (manifestMatch) {
|
|
76
|
+
const version = url.searchParams.get('version') ?? undefined;
|
|
77
|
+
return {
|
|
78
|
+
orgSlug: manifestMatch[1],
|
|
79
|
+
docSlug: manifestMatch[2],
|
|
80
|
+
version,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 解析 mdoc.cc 风格的网址: /:orgSlug/:docSlug[/:version[/:articleId]]
|
|
85
|
+
if (segments.length < 2) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`URL 路径段不足,期望格式: /:orgSlug/:docSlug[/:version[/:articleId]],实际路径: ${pathname}`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const [orgSlug, docSlug, seg3, seg4] = segments;
|
|
92
|
+
let version: string | undefined;
|
|
93
|
+
let articleId: string | undefined;
|
|
94
|
+
|
|
95
|
+
if (seg3) {
|
|
96
|
+
if (looksLikeArticleId(seg3) && !seg4) {
|
|
97
|
+
// 只有纯数字且没有第四段:当做 articleId
|
|
98
|
+
articleId = seg3;
|
|
99
|
+
} else if (looksLikeVersion(seg3)) {
|
|
100
|
+
version = seg3;
|
|
101
|
+
if (seg4) {
|
|
102
|
+
articleId = seg4;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
// 第三段既不像版本也不像 articleId,作为 version 兜底处理
|
|
106
|
+
version = seg3;
|
|
107
|
+
if (seg4) {
|
|
108
|
+
articleId = seg4;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { orgSlug, docSlug, version, articleId };
|
|
114
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|