@lmcl/ailo-mcp-sdk 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +187 -0
- package/dist/ailo-client.d.ts +31 -0
- package/dist/ailo-client.d.ts.map +1 -0
- package/dist/ailo-client.js +140 -0
- package/dist/bootstrap.d.ts +35 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +148 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @lmcl/ailo-mcp-sdk
|
|
2
|
+
|
|
3
|
+
AILO 通道与工具开发 SDK。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @lmcl/ailo-mcp-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**MCP → AILO 方向**:MCP 的 name 就是通道名,将来收到消息时用的就是这个名字。
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 场景1:纯工具(AILO → MCP 客户端)
|
|
14
|
+
|
|
15
|
+
AILO 调用你的工具,你返回结果。
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { runMcp } from "@lmcl/ailo-mcp-sdk";
|
|
21
|
+
|
|
22
|
+
const server = new McpServer({ name: "my-tools", version: "1.0.0" });
|
|
23
|
+
|
|
24
|
+
server.registerTool("get_weather", { description: "查询天气", inputSchema: { city: z.string() } }, async ({ city }) => {
|
|
25
|
+
return { content: [{ type: "text", text: `北京: 晴 25°C` }] };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
runMcp(server);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 场景2:纯接收(MCP 客户端 → AILO)
|
|
34
|
+
|
|
35
|
+
平台有事件时,你推送给 AILO。无工具。
|
|
36
|
+
|
|
37
|
+
### 1. 实现 BridgeHandler
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import type { BridgeHandler, BridgeMessage } from "@lmcl/ailo-mcp-sdk";
|
|
41
|
+
|
|
42
|
+
class MyHandler implements BridgeHandler {
|
|
43
|
+
private onMessage?: (msg: BridgeMessage) => void;
|
|
44
|
+
|
|
45
|
+
setOnMessage(fn: (msg: BridgeMessage) => void) {
|
|
46
|
+
this.onMessage = fn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async start() {
|
|
50
|
+
// 启动平台连接(WebSocket / long polling 等)
|
|
51
|
+
// 收到平台消息时调用:this.onMessage?.({ chatId, chatType: "群聊"|"私聊", text, senderId, senderName, chatName?, isPrivate?, ... })
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
stop?() {
|
|
55
|
+
// 断开平台连接
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. 启动(mcpServer 可空或只注册 status)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { runMcpChannel } from "@lmcl/ailo-mcp-sdk";
|
|
64
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
65
|
+
|
|
66
|
+
const handler = new MyHandler();
|
|
67
|
+
const server = new McpServer({ name: "my-channel", version: "1.0.0" });
|
|
68
|
+
// 纯接收无需注册工具,或注册 status 供 AILO 查看健康
|
|
69
|
+
|
|
70
|
+
runMcpChannel({
|
|
71
|
+
handler,
|
|
72
|
+
mcpServer: server,
|
|
73
|
+
buildChannelPrompt: () => "本通道规则说明",
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 场景3:双向收发(AILO ↔ MCP 客户端)
|
|
80
|
+
|
|
81
|
+
AILO → MCP + MCP → AILO 组合。既推送事件给 AILO,也注册工具供 AILO 调用。
|
|
82
|
+
|
|
83
|
+
### 1. 实现 BridgeHandler(同纯接收)
|
|
84
|
+
|
|
85
|
+
### 2. 创建 MCP Server 并注册工具
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
89
|
+
import { z } from "zod";
|
|
90
|
+
|
|
91
|
+
const server = new McpServer({ name: "my-channel", version: "1.0.0" });
|
|
92
|
+
|
|
93
|
+
server.registerTool("send_message", {
|
|
94
|
+
description: "发送消息到平台",
|
|
95
|
+
inputSchema: { chat_id: z.string(), text: z.string() },
|
|
96
|
+
}, async ({ chat_id, text }) => {
|
|
97
|
+
// 调用 handler 或平台 API 发消息
|
|
98
|
+
return { content: [{ type: "text", text: "已发送" }] };
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 3. 启动
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { runMcpChannel } from "@lmcl/ailo-mcp-sdk";
|
|
106
|
+
|
|
107
|
+
const handler = new MyHandler();
|
|
108
|
+
|
|
109
|
+
runMcpChannel({
|
|
110
|
+
handler,
|
|
111
|
+
mcpServer: server,
|
|
112
|
+
buildChannelPrompt: () => "本通道的规则说明,如 @提及格式、ID 格式等",
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 4. 需要存点数据时(可选)
|
|
117
|
+
|
|
118
|
+
数据存在 AILO 本体,所以只有 MCP → AILO 方向(有 WebSocket 连接)才有。SDK 连接后注入一个带 get/set/delete 的对象,直接用即可,自动持久化。更复杂的需求自行实现(不通过我们)。
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
class MyHandler implements BridgeHandler {
|
|
122
|
+
private kv: { getData: (k: string) => Promise<string | null>; setData: (k: string, v: string) => Promise<void>; deleteData: (k: string) => Promise<void> } | null = null;
|
|
123
|
+
|
|
124
|
+
setDataProvider?(s) {
|
|
125
|
+
this.kv = s;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 直接用:await this.kv?.getData("key")
|
|
129
|
+
// await this.kv?.setData("key", "value")
|
|
130
|
+
// await this.kv?.deleteData("key")
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 配置项(runMcpChannel)
|
|
137
|
+
|
|
138
|
+
### AILO → MCP 方向(AILO 调用你的工具)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
| 字段 | 必填 | 说明 |
|
|
142
|
+
| ----------- | --- | --------------- |
|
|
143
|
+
| `mcpServer` | 是 | 需注册工具,AILO 才能调用 |
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### MCP → AILO 方向(你推送事件给 AILO)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
| 字段 | 必填 | 说明 |
|
|
150
|
+
| -------------------- | --- | ---------------------------- |
|
|
151
|
+
| `handler` | 是 | BridgeHandler 实例 |
|
|
152
|
+
| `buildChannelPrompt` | 否 | 通道规则提示词 |
|
|
153
|
+
| `ailoWsUrl` | 否 | stdio 时从 env 读取;**HTTP 时必须传**(通道与网关不同网络) |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## 环境变量(MCP → AILO 方向)
|
|
158
|
+
|
|
159
|
+
| 变量 | stdio 注入方式 | HTTP 注入方式 |
|
|
160
|
+
| --------------- | ---------------------- | ---------------------- |
|
|
161
|
+
| `AIDO_WS_URL` | env 注入,主程序自动 | 主程序以 header `ailo-mcp-ws-url` 传给远程 MCP。**通道与网关不同网络时,你必须在 MCP 配置的 env 中填 `ailo-mcp-ws-url` 或 `AIDO_WS_URL`**,主程序会读取后放入 header |
|
|
162
|
+
| `AIDO_TOKEN` | env 注入,主程序自动 | 主程序以 header `ailo-mcp-token` 传给远程 MCP,不可配置 |
|
|
163
|
+
| `AIDO_MCP_NAME` | env 注入,主程序自动 | 主程序以 header `ailo-mcp-name` 传给远程 MCP,严格按 MCP 的 name,不可配置 |
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
*无需了解*:通道回连时,服务端会校验 MCP 身份与凭证,双向链路均有认证保护,你只需写业务逻辑即可。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 时空场格式
|
|
171
|
+
|
|
172
|
+
WebSocket(MCP → AILO)场景下,推送事件时用。
|
|
173
|
+
|
|
174
|
+
- **格式**:KV 数组。每项 `{ key, desc, value }`,`key` 框架用,`desc` 给 LLM 看,`value` 标签值。
|
|
175
|
+
- **顺序**:敏感,顺序决定语义。
|
|
176
|
+
- **coreForSenseContext**:与数组一一对应,`true` 表示该项参与时空场键。最左匹配,遇 `false` 即停。私聊全 `true`;群聊 `chat_type`/`chat_id`/`chat_name` 为 `true`,`sender_name` 起为 `false`。
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## 类型速查
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
| 类型 | 说明 |
|
|
184
|
+
| --------------- | ---- |
|
|
185
|
+
| `BridgeHandler`、`BridgeMessage` | WebSocket(MCP → AILO)场景才用,推送给 AILO 的接口与时空场格式 |
|
|
186
|
+
|
|
187
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ChannelAcceptParams } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* 反向 WebSocket 信号客户端。
|
|
4
|
+
*
|
|
5
|
+
* 连接 Ailo 网关,connect 时一并传入 channel 与 prompt,一步完成注册。
|
|
6
|
+
* 负责 channel.accept(入站信号投递)。
|
|
7
|
+
*
|
|
8
|
+
* 出站(AI → 平台)由 MCP stdio 工具处理,不经过此客户端。
|
|
9
|
+
*/
|
|
10
|
+
export declare class AiloClient {
|
|
11
|
+
private ws;
|
|
12
|
+
private url;
|
|
13
|
+
private token;
|
|
14
|
+
private channel;
|
|
15
|
+
private channelPrompt;
|
|
16
|
+
private reconnectTimer;
|
|
17
|
+
private reqId;
|
|
18
|
+
constructor(url: string, token: string, channel: string, channelPrompt?: string);
|
|
19
|
+
private request;
|
|
20
|
+
connect(): Promise<void>;
|
|
21
|
+
private scheduleReconnect;
|
|
22
|
+
sendMessage(params: ChannelAcceptParams): Promise<{
|
|
23
|
+
text?: string;
|
|
24
|
+
}>;
|
|
25
|
+
/** 简单 KV,数据存 AILO 本体,自动持久化 */
|
|
26
|
+
getData(key: string): Promise<string | null>;
|
|
27
|
+
setData(key: string, value: string): Promise<void>;
|
|
28
|
+
deleteData(key: string): Promise<void>;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=ailo-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ailo-client.d.ts","sourceRoot":"","sources":["../src/ailo-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AActD;;;;;;;GAOG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,cAAc,CAA8C;IACpE,OAAO,CAAC,KAAK,CAAK;gBAEN,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,SAAK;IAO3E,OAAO,CAAC,OAAO;IAuBf,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAwDxB,OAAO,CAAC,iBAAiB;IAUzB,WAAW,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAapE,8BAA8B;IACxB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAQ5C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5C,KAAK,IAAI,IAAI;CAUd"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import WebSocket from "ws";
|
|
2
|
+
/**
|
|
3
|
+
* 反向 WebSocket 信号客户端。
|
|
4
|
+
*
|
|
5
|
+
* 连接 Ailo 网关,connect 时一并传入 channel 与 prompt,一步完成注册。
|
|
6
|
+
* 负责 channel.accept(入站信号投递)。
|
|
7
|
+
*
|
|
8
|
+
* 出站(AI → 平台)由 MCP stdio 工具处理,不经过此客户端。
|
|
9
|
+
*/
|
|
10
|
+
export class AiloClient {
|
|
11
|
+
ws = null;
|
|
12
|
+
url;
|
|
13
|
+
token;
|
|
14
|
+
channel;
|
|
15
|
+
channelPrompt;
|
|
16
|
+
reconnectTimer = null;
|
|
17
|
+
reqId = 0;
|
|
18
|
+
constructor(url, token, channel, channelPrompt = "") {
|
|
19
|
+
this.url = url;
|
|
20
|
+
this.token = token;
|
|
21
|
+
this.channel = channel;
|
|
22
|
+
this.channelPrompt = channelPrompt;
|
|
23
|
+
}
|
|
24
|
+
request(method, params) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
27
|
+
reject(new Error("Ailo WebSocket not connected"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const id = `${method}-${++this.reqId}`;
|
|
31
|
+
const handler = (raw) => {
|
|
32
|
+
const frame = JSON.parse(raw.toString());
|
|
33
|
+
if (frame.type === "res" && frame.id === id) {
|
|
34
|
+
this.ws?.off("message", handler);
|
|
35
|
+
if (frame.ok) {
|
|
36
|
+
resolve(frame.payload ?? {});
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
reject(new Error(frame.error?.message ?? `${method} failed`));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
this.ws.on("message", handler);
|
|
44
|
+
this.ws.send(JSON.stringify({ type: "req", id, method, params }));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
connect() {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const ws = new WebSocket(this.url);
|
|
50
|
+
this.ws = ws;
|
|
51
|
+
ws.on("open", async () => {
|
|
52
|
+
try {
|
|
53
|
+
const id = `connect-${++this.reqId}`;
|
|
54
|
+
ws.send(JSON.stringify({
|
|
55
|
+
type: "req",
|
|
56
|
+
id,
|
|
57
|
+
method: "connect",
|
|
58
|
+
params: {
|
|
59
|
+
role: "channel",
|
|
60
|
+
token: this.token,
|
|
61
|
+
channel: this.channel,
|
|
62
|
+
prompt: this.channelPrompt,
|
|
63
|
+
capabilities: ["text", "media"],
|
|
64
|
+
direction: "bidirectional",
|
|
65
|
+
},
|
|
66
|
+
}));
|
|
67
|
+
await new Promise((res, rej) => {
|
|
68
|
+
const onMsg = (raw) => {
|
|
69
|
+
const frame = JSON.parse(raw.toString());
|
|
70
|
+
if (frame.type === "res" && frame.id === id) {
|
|
71
|
+
ws.off("message", onMsg);
|
|
72
|
+
if (frame.ok) {
|
|
73
|
+
res();
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
rej(new Error(frame.error?.message ?? "connect failed"));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
ws.on("message", onMsg);
|
|
81
|
+
});
|
|
82
|
+
resolve();
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
reject(err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
ws.on("close", () => {
|
|
89
|
+
this.ws = null;
|
|
90
|
+
this.scheduleReconnect(resolve);
|
|
91
|
+
});
|
|
92
|
+
ws.on("error", (err) => {
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
scheduleReconnect(onReconnect) {
|
|
98
|
+
if (this.reconnectTimer)
|
|
99
|
+
return;
|
|
100
|
+
this.reconnectTimer = setTimeout(() => {
|
|
101
|
+
this.reconnectTimer = null;
|
|
102
|
+
this.connect()
|
|
103
|
+
.then(() => onReconnect?.())
|
|
104
|
+
.catch(() => this.scheduleReconnect(onReconnect));
|
|
105
|
+
}, 3000);
|
|
106
|
+
}
|
|
107
|
+
sendMessage(params) {
|
|
108
|
+
const payload = {
|
|
109
|
+
chatId: params.chatId,
|
|
110
|
+
text: params.text,
|
|
111
|
+
contextTags: params.contextTags,
|
|
112
|
+
attachments: params.attachments ?? [],
|
|
113
|
+
};
|
|
114
|
+
if (params.coreForSenseContext != null && params.coreForSenseContext.length > 0) {
|
|
115
|
+
payload.coreForSenseContext = params.coreForSenseContext;
|
|
116
|
+
}
|
|
117
|
+
return this.request("channel.accept", payload);
|
|
118
|
+
}
|
|
119
|
+
/** 简单 KV,数据存 AILO 本体,自动持久化 */
|
|
120
|
+
async getData(key) {
|
|
121
|
+
const res = await this.request("channel.data.get", { key });
|
|
122
|
+
return res.found ? (res.value ?? null) : null;
|
|
123
|
+
}
|
|
124
|
+
async setData(key, value) {
|
|
125
|
+
await this.request("channel.data.set", { key, value });
|
|
126
|
+
}
|
|
127
|
+
async deleteData(key) {
|
|
128
|
+
await this.request("channel.data.delete", { key });
|
|
129
|
+
}
|
|
130
|
+
close() {
|
|
131
|
+
if (this.reconnectTimer) {
|
|
132
|
+
clearTimeout(this.reconnectTimer);
|
|
133
|
+
this.reconnectTimer = null;
|
|
134
|
+
}
|
|
135
|
+
if (this.ws) {
|
|
136
|
+
this.ws.close();
|
|
137
|
+
this.ws = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { BridgeHandler } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* MCP 通道启动配置
|
|
5
|
+
*
|
|
6
|
+
* AIDO_WS_URL、AIDO_TOKEN、AIDO_MCP_NAME 由主程序在拉起 MCP 时注入,SDK 自动读取。
|
|
7
|
+
* 通道开发者只需提供 handler、mcpServer、buildChannelPrompt 等业务配置。
|
|
8
|
+
*/
|
|
9
|
+
export interface McpChannelConfig {
|
|
10
|
+
/** MCP 名称(如 channel:feishu),connect 时与 token 一起发送供服务端校验。不传则从 AIDO_MCP_NAME / AILO_MCP_NAME 读取 */
|
|
11
|
+
channelName?: string;
|
|
12
|
+
/** 平台 Handler 实例(需实现 BridgeHandler 接口) */
|
|
13
|
+
handler: BridgeHandler;
|
|
14
|
+
/** Ailo WebSocket 网关地址。不传则从 AIDO_WS_URL / AILO_WS_URL 读取 */
|
|
15
|
+
ailoWsUrl?: string;
|
|
16
|
+
/** Ailo 网关认证 Token。不传则从 AIDO_TOKEN / AILO_TOKEN 读取 */
|
|
17
|
+
ailoToken?: string;
|
|
18
|
+
/**
|
|
19
|
+
* 构建通道静态提示词(connect 时注册)。
|
|
20
|
+
* 连接时调用一次,注册该通道的特殊规则。
|
|
21
|
+
*/
|
|
22
|
+
buildChannelPrompt?: () => string;
|
|
23
|
+
/** 预配置的 MCP Server 实例(已注册好工具) */
|
|
24
|
+
mcpServer: McpServer;
|
|
25
|
+
}
|
|
26
|
+
export declare function defaultBuildChannelPrompt(): string;
|
|
27
|
+
/**
|
|
28
|
+
* 启动 MCP Server(纯工具 / 单向发通道用)
|
|
29
|
+
*
|
|
30
|
+
* 本 SDK 作为脚手架:只需创建 MCP Server、注册工具,调用此函数即可启动。
|
|
31
|
+
* stdout 被 MCP stdio 占用,日志自动重定向到 stderr。
|
|
32
|
+
*/
|
|
33
|
+
export declare function runMcp(mcpServer: McpServer): void;
|
|
34
|
+
export declare function runMcpChannel(config: McpChannelConfig): void;
|
|
35
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../src/bootstrap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAiB,MAAM,YAAY,CAAC;AAE/D;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,MAAM,CAAC;IAClC,iCAAiC;IACjC,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CASjD;AAiBD,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,IAAI,CAwI5D"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { AiloClient } from "./ailo-client.js";
|
|
3
|
+
export function defaultBuildChannelPrompt() {
|
|
4
|
+
return `用户 @你 时会在消息中标注。`;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 启动 MCP Server(纯工具 / 单向发通道用)
|
|
8
|
+
*
|
|
9
|
+
* 本 SDK 作为脚手架:只需创建 MCP Server、注册工具,调用此函数即可启动。
|
|
10
|
+
* stdout 被 MCP stdio 占用,日志自动重定向到 stderr。
|
|
11
|
+
*/
|
|
12
|
+
export function runMcp(mcpServer) {
|
|
13
|
+
console.log = (...args) => console.error(...args);
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
15
|
+
mcpServer.connect(transport).then(() => {
|
|
16
|
+
console.log("[mcp] MCP stdio server started");
|
|
17
|
+
}).catch((err) => {
|
|
18
|
+
console.error("[mcp] MCP start failed:", err);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 启动 MCP 通道
|
|
24
|
+
*
|
|
25
|
+
* 通用流程:
|
|
26
|
+
* 1. 重定向 console.log 到 stderr(stdout 被 MCP stdio 占用)
|
|
27
|
+
* 2. 启动 MCP stdio server(暴露出站工具)
|
|
28
|
+
* 3. 建立反向 WebSocket 连接(connect 时传入 channel + prompt,一步完成注册)
|
|
29
|
+
* 4. 接线入站:handler.setOnMessage → 组装 contextTags → channel.accept
|
|
30
|
+
* 5. 启动平台 Handler
|
|
31
|
+
* 6. 注册 SIGINT / SIGTERM 优雅退出
|
|
32
|
+
*/
|
|
33
|
+
function envOr(name, fallback) {
|
|
34
|
+
return process.env[name] ?? process.env[fallback] ?? "";
|
|
35
|
+
}
|
|
36
|
+
export function runMcpChannel(config) {
|
|
37
|
+
const { handler, mcpServer } = config;
|
|
38
|
+
const channelName = config.channelName ?? envOr("AIDO_MCP_NAME", "AILO_MCP_NAME");
|
|
39
|
+
const ailoWsUrl = config.ailoWsUrl ?? envOr("AIDO_WS_URL", "AILO_WS_URL");
|
|
40
|
+
const ailoToken = config.ailoToken ?? envOr("AIDO_TOKEN", "AILO_TOKEN");
|
|
41
|
+
if (!ailoWsUrl || !ailoToken || !channelName) {
|
|
42
|
+
console.error("Missing AIDO_WS_URL/AILO_WS_URL, AIDO_TOKEN/AILO_TOKEN or AIDO_MCP_NAME/AILO_MCP_NAME. Channel must be started by Ailo MCP.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// stdout 被 MCP stdio 占用,日志全部走 stderr
|
|
46
|
+
const _origLog = console.log;
|
|
47
|
+
console.log = (...args) => console.error(...args);
|
|
48
|
+
const tag = `[${channelName}]`;
|
|
49
|
+
const channelPrompt = config.buildChannelPrompt
|
|
50
|
+
? config.buildChannelPrompt()
|
|
51
|
+
: defaultBuildChannelPrompt();
|
|
52
|
+
const client = new AiloClient(ailoWsUrl, ailoToken, channelName, channelPrompt);
|
|
53
|
+
// 入站:平台 → Ailo(channel.accept)
|
|
54
|
+
handler.setOnMessage(async (msg) => {
|
|
55
|
+
const hasContent = (msg.text ?? "").trim() !== "" || (msg.attachments?.length ?? 0) > 0;
|
|
56
|
+
if (!hasContent) {
|
|
57
|
+
console.log(`${tag} skipped ${msg.chatType} ${msg.chatId} (no text or attachments)`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(`${tag} ${msg.chatType} ${msg.chatId} from ${msg.senderName ? msg.senderName + "(" + (msg.senderId ?? "") + ")" : msg.senderId ?? "unknown"}: ${(msg.text ?? "").slice(0, 80)}`);
|
|
61
|
+
try {
|
|
62
|
+
const isPrivate = msg.isPrivate ?? false;
|
|
63
|
+
const groupLabel = !isPrivate &&
|
|
64
|
+
(msg.chatName || (msg.chatId ? `群${msg.chatId.slice(-8)}` : ""));
|
|
65
|
+
const contextTags = [
|
|
66
|
+
{ key: "chat_type", desc: "类型", value: msg.chatType },
|
|
67
|
+
{ key: "chat_id", desc: "会话", value: msg.chatId },
|
|
68
|
+
];
|
|
69
|
+
if (groupLabel) {
|
|
70
|
+
contextTags.push({ key: "chat_name", desc: "群名", value: groupLabel });
|
|
71
|
+
}
|
|
72
|
+
contextTags.push({ key: "sender_name", desc: "昵称", value: msg.senderName ?? "" }, { key: "sender_id", desc: "用户ID", value: msg.senderId ?? "" });
|
|
73
|
+
if (msg.mentionsSelf) {
|
|
74
|
+
contextTags.push({ key: "mentions_self", desc: "提及自己", value: "true" });
|
|
75
|
+
}
|
|
76
|
+
if (msg.timestamp != null) {
|
|
77
|
+
let tsMs;
|
|
78
|
+
if (typeof msg.timestamp === "number") {
|
|
79
|
+
tsMs = msg.timestamp;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
tsMs = parseInt(msg.timestamp, 10);
|
|
83
|
+
}
|
|
84
|
+
if (!isNaN(tsMs) && tsMs > 0) {
|
|
85
|
+
const d = new Date(tsMs);
|
|
86
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
87
|
+
const formatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
88
|
+
contextTags.push({ key: "sent_at", desc: "发送时间", value: formatted });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const coreForSenseContext = [];
|
|
92
|
+
coreForSenseContext.push(true, true);
|
|
93
|
+
if (groupLabel) {
|
|
94
|
+
coreForSenseContext.push(true);
|
|
95
|
+
}
|
|
96
|
+
for (let i = coreForSenseContext.length; i < contextTags.length; i++) {
|
|
97
|
+
coreForSenseContext.push(isPrivate);
|
|
98
|
+
}
|
|
99
|
+
await client.sendMessage({
|
|
100
|
+
chatId: msg.chatId,
|
|
101
|
+
text: msg.text ?? "",
|
|
102
|
+
contextTags,
|
|
103
|
+
coreForSenseContext,
|
|
104
|
+
attachments: msg.attachments ?? [],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.error(`${tag} send to Ailo failed:`, err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// 优雅退出
|
|
112
|
+
const shutdown = () => {
|
|
113
|
+
console.log(`${tag} shutting down...`);
|
|
114
|
+
handler.stop?.();
|
|
115
|
+
client.close();
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
process.on("SIGINT", shutdown);
|
|
119
|
+
process.on("SIGTERM", shutdown);
|
|
120
|
+
(async () => {
|
|
121
|
+
// 1. 启动 MCP stdio server
|
|
122
|
+
const transport = new StdioServerTransport();
|
|
123
|
+
await mcpServer.connect(transport);
|
|
124
|
+
console.log(`${tag} MCP stdio server started`);
|
|
125
|
+
// 2. 建立反向 WebSocket 连接
|
|
126
|
+
try {
|
|
127
|
+
await client.connect();
|
|
128
|
+
console.log(`${tag} reverse WebSocket connected`);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.error(`${tag} reverse WebSocket connect failed:`, err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
// 3. 启动平台 Handler
|
|
135
|
+
console.log(`${tag} starting handler...`);
|
|
136
|
+
const startResult = handler.start();
|
|
137
|
+
if (startResult && typeof startResult.then === "function") {
|
|
138
|
+
try {
|
|
139
|
+
await startResult;
|
|
140
|
+
console.log(`${tag} handler started successfully`);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error(`${tag} handler start failed:`, err);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AiloClient } from "./ailo-client.js";
|
|
2
|
+
export { runMcp, runMcpChannel, defaultBuildChannelPrompt } from "./bootstrap.js";
|
|
3
|
+
export type { McpChannelConfig } from "./bootstrap.js";
|
|
4
|
+
export type { Attachment, BridgeHandler, BridgeMessage, ChannelAcceptParams, ContextTag, } from "./types.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAClF,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EACV,UAAU,EACV,aAAa,EACb,aAAa,EACb,mBAAmB,EACnB,UAAU,GACX,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 附件类型(图片/音频/视频/文件等)
|
|
3
|
+
* 入站图片:path/url/base64 三选一,直接使用 LLM 多模态。其他类型:path/url/ref+channel/base64。
|
|
4
|
+
* 出站:file_path 或 base64 或 url。
|
|
5
|
+
*/
|
|
6
|
+
export type Attachment = {
|
|
7
|
+
type: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
path?: string;
|
|
10
|
+
ref?: string;
|
|
11
|
+
channel?: string;
|
|
12
|
+
base64?: string;
|
|
13
|
+
mime?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
file_path?: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 桥接器入站消息(平台 → Ailo)
|
|
19
|
+
*
|
|
20
|
+
* 所有平台 Handler 的 onMessage 回调统一使用此类型。
|
|
21
|
+
*/
|
|
22
|
+
export type BridgeMessage = {
|
|
23
|
+
chatId: string;
|
|
24
|
+
chatType: string;
|
|
25
|
+
text?: string;
|
|
26
|
+
senderId?: string;
|
|
27
|
+
senderName?: string;
|
|
28
|
+
chatName?: string;
|
|
29
|
+
isPrivate?: boolean;
|
|
30
|
+
mentionsSelf?: boolean;
|
|
31
|
+
attachments?: Attachment[];
|
|
32
|
+
timestamp?: number | string;
|
|
33
|
+
messageId?: string;
|
|
34
|
+
};
|
|
35
|
+
/** setDataProvider 接收的对象,SDK 注入,直接用 get/set/delete 即可 */
|
|
36
|
+
type ChannelStorage = {
|
|
37
|
+
getData(key: string): Promise<string | null>;
|
|
38
|
+
setData(key: string, value: string): Promise<void>;
|
|
39
|
+
deleteData(key: string): Promise<void>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 通道 Handler 统一接口
|
|
43
|
+
*/
|
|
44
|
+
export interface BridgeHandler {
|
|
45
|
+
setOnMessage(handler: (msg: BridgeMessage) => void | Promise<void>): void;
|
|
46
|
+
start(): void | Promise<void>;
|
|
47
|
+
stop?(): void;
|
|
48
|
+
/** 可选,SDK 连接后注入带 get/set/delete 的对象,直接用 */
|
|
49
|
+
setDataProvider?(storage: ChannelStorage): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* channel.accept 的 contextTags 项
|
|
53
|
+
*
|
|
54
|
+
* key: 框架内部路由用(英文标识)
|
|
55
|
+
* desc: 给 LLM 看的人类可读标签(中文)
|
|
56
|
+
* value: 标签值
|
|
57
|
+
*/
|
|
58
|
+
export type ContextTag = {
|
|
59
|
+
key: string;
|
|
60
|
+
desc: string;
|
|
61
|
+
value: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* channel.accept 的消息参数
|
|
65
|
+
*
|
|
66
|
+
* coreForSenseContext: 与 contextTags 一一对应,true 表示该标签参与时空场键生成。
|
|
67
|
+
* 最左匹配:遇 false 即停。私聊全 true;群聊 chat_type/chat_id/chat_name 为 true,sender_name 起为 false。
|
|
68
|
+
* 未传时框架回退 Channel+ChatID。
|
|
69
|
+
*/
|
|
70
|
+
export type ChannelAcceptParams = {
|
|
71
|
+
chatId: string;
|
|
72
|
+
text: string;
|
|
73
|
+
contextTags: ContextTag[];
|
|
74
|
+
coreForSenseContext?: boolean[];
|
|
75
|
+
attachments?: Attachment[];
|
|
76
|
+
};
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,yDAAyD;AACzD,KAAK,cAAc,GAAG;IACpB,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1E,KAAK,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,CAAC,IAAI,IAAI,CAAC;IACd,2CAA2C;IAC3C,eAAe,CAAC,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CAAC;CACjD;AAED;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,mBAAmB,CAAC,EAAE,OAAO,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lmcl/ailo-mcp-sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Ailo MCP SDK - 通道通过 MCP 注册发送消息能力,用于开发感知通道",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/lmcl/ailo-mcp-sdk.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ailo",
|
|
23
|
+
"mcp",
|
|
24
|
+
"sdk",
|
|
25
|
+
"channel",
|
|
26
|
+
"bridge"
|
|
27
|
+
],
|
|
28
|
+
"author": "",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
32
|
+
"ws": "^8.18.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.10.0",
|
|
36
|
+
"@types/ws": "^8.5.13",
|
|
37
|
+
"typescript": "^5.7.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|