@andocorp/openclaw-plugin 0.0.2 → 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/LICENSE +21 -0
- package/README.md +12 -17
- package/package.json +9 -8
- package/src/channel.ts +14 -36
- package/src/client.ts +1 -1
- package/src/mcp-tool-catalog.ts +51 -2
- package/src/mcp-tools.ts +24 -18
- package/src/types.ts +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ando Corp
|
|
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
CHANGED
|
@@ -2,37 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
OpenClaw adapter for Ando.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Configure `channels.ando.apiKey` in OpenClaw. REST requests use
|
|
6
|
+
`https://api.ando.so` unless `channels.ando.baseUrl` is set. Realtime monitoring
|
|
7
|
+
uses `sokachu.ando.so` unless `channels.ando.realtimeHost` is set. MCP tool
|
|
8
|
+
proxying uses `https://mcp.ando.so/mcp` unless `channels.ando.mcpUrl` is set.
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
## Publish
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- `npm pack` / `npm publish` will produce a broken package.
|
|
12
|
+
This package is published from the open-source Ando package monorepo. Publish
|
|
13
|
+
with `pnpm` so the `@andocorp/sdk` workspace dependency is rewritten in the
|
|
14
|
+
published tarball.
|
|
13
15
|
|
|
14
|
-
Before publishing
|
|
15
|
-
1. Ensure `@andocorp/sdk@<current version>` is already published.
|
|
16
|
-
2. Run the package verification.
|
|
17
|
-
3. Publish `@andocorp/openclaw-plugin`.
|
|
16
|
+
Before publishing, publish `@andocorp/sdk@<current version>` first.
|
|
18
17
|
|
|
19
18
|
From the package directory:
|
|
20
19
|
|
|
21
20
|
```sh
|
|
22
|
-
pnpm
|
|
23
|
-
pnpm publish --access restricted --no-git-checks
|
|
21
|
+
pnpm publish --access public --no-git-checks
|
|
24
22
|
```
|
|
25
23
|
|
|
26
24
|
From the repo root:
|
|
27
25
|
|
|
28
26
|
```sh
|
|
29
|
-
pnpm --filter @andocorp/openclaw-plugin
|
|
30
|
-
pnpm --filter @andocorp/openclaw-plugin publish --access restricted --no-git-checks
|
|
27
|
+
pnpm --filter @andocorp/openclaw-plugin publish --access public --no-git-checks
|
|
31
28
|
```
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Do not use `npm`:
|
|
30
|
+
Do not publish this package with `npm`:
|
|
36
31
|
|
|
37
32
|
```sh
|
|
38
33
|
npm pack
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andocorp/openclaw-plugin",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "OpenClaw adapter for Ando",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"zod": "^4.3.6",
|
|
18
|
-
"@andocorp/sdk": "0.0
|
|
18
|
+
"@andocorp/sdk": "0.1.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"openclaw": ">=2026.3.23"
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|
|
29
|
-
"access": "
|
|
29
|
+
"access": "public"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^24.10.9",
|
|
33
33
|
"openclaw": "2026.3.23",
|
|
34
34
|
"typescript": "^5.9.3",
|
|
35
|
-
"vitest": "^
|
|
35
|
+
"vitest": "^4.1.5"
|
|
36
36
|
},
|
|
37
37
|
"openclaw": {
|
|
38
38
|
"extensions": [
|
|
@@ -47,9 +47,10 @@
|
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"test
|
|
50
|
+
"build": "tsc --noEmit",
|
|
51
|
+
"lint": "tsc --noEmit",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:boundaries": "vitest run src/monitor.test.ts src/channel.test.ts"
|
|
54
55
|
}
|
|
55
56
|
}
|
package/src/channel.ts
CHANGED
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
import packageJson from "../package.json" with { type: "json" };
|
|
7
7
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-resolution";
|
|
8
8
|
import { createTopLevelChannelConfigBase } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_ANDO_BASE_URL, DEFAULT_ANDO_REALTIME_HOST, formatAndoTarget,
|
|
11
|
+
parseAndoTarget
|
|
12
|
+
} from "@andocorp/sdk";
|
|
10
13
|
import { z } from "zod";
|
|
11
14
|
import { postAndoMessage } from "./client.js";
|
|
12
15
|
import { monitorAndoProvider } from "./monitor.js";
|
|
@@ -20,6 +23,8 @@ const meta = packageJson.openclaw.channel as {
|
|
|
20
23
|
blurb: string;
|
|
21
24
|
};
|
|
22
25
|
|
|
26
|
+
export const DEFAULT_ANDO_MCP_URL = "https://mcp.ando.so/mcp";
|
|
27
|
+
|
|
23
28
|
type AndoConfig = {
|
|
24
29
|
baseUrl?: string;
|
|
25
30
|
apiKey?: string;
|
|
@@ -35,20 +40,8 @@ function normalizeUrl(value?: string | null): string {
|
|
|
35
40
|
return trimValue(value).replace(/\/+$/, "");
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
function waitForAbort(signal?: AbortSignal): Promise<void> {
|
|
39
|
-
return new Promise((resolve) => {
|
|
40
|
-
if (signal?.aborted) {
|
|
41
|
-
resolve();
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
signal?.addEventListener("abort", () => resolve(), {
|
|
46
|
-
once: true,
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
43
|
export function resolveAndoMcpUrl(params: {
|
|
51
|
-
baseUrl
|
|
44
|
+
baseUrl?: string | null;
|
|
52
45
|
mcpUrl?: string | null;
|
|
53
46
|
}): string {
|
|
54
47
|
const explicitMcpUrl = normalizeUrl(params.mcpUrl);
|
|
@@ -56,8 +49,7 @@ export function resolveAndoMcpUrl(params: {
|
|
|
56
49
|
return explicitMcpUrl;
|
|
57
50
|
}
|
|
58
51
|
|
|
59
|
-
|
|
60
|
-
return baseUrl ? `${baseUrl}/mcp` : "";
|
|
52
|
+
return DEFAULT_ANDO_MCP_URL;
|
|
61
53
|
}
|
|
62
54
|
export function resolveAndoAccount(cfg: OpenClawConfig): ResolvedAndoAccount {
|
|
63
55
|
const config = (cfg.channels?.ando ?? {}) as AndoConfig;
|
|
@@ -65,9 +57,9 @@ export function resolveAndoAccount(cfg: OpenClawConfig): ResolvedAndoAccount {
|
|
|
65
57
|
|
|
66
58
|
return {
|
|
67
59
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
68
|
-
baseUrl: normalizeUrl(config.baseUrl),
|
|
60
|
+
baseUrl: normalizeUrl(config.baseUrl) || DEFAULT_ANDO_BASE_URL,
|
|
69
61
|
apiKey: trimValue(config.apiKey),
|
|
70
|
-
realtimeHost: realtimeHost ||
|
|
62
|
+
realtimeHost: realtimeHost || DEFAULT_ANDO_REALTIME_HOST,
|
|
71
63
|
mcpUrl: resolveAndoMcpUrl({
|
|
72
64
|
baseUrl: config.baseUrl ?? "",
|
|
73
65
|
mcpUrl: config.mcpUrl,
|
|
@@ -105,25 +97,19 @@ type AndoOutboundParams = {
|
|
|
105
97
|
};
|
|
106
98
|
|
|
107
99
|
function isAndoRestConfigured(account: ResolvedAndoAccount): boolean {
|
|
108
|
-
return Boolean(account.
|
|
100
|
+
return Boolean(account.apiKey);
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
function assertAndoRestConfigured(account: ResolvedAndoAccount): void {
|
|
112
104
|
if (!isAndoRestConfigured(account)) {
|
|
113
105
|
throw new Error(
|
|
114
|
-
`[ando] account "${account.accountId}" is missing
|
|
106
|
+
`[ando] account "${account.accountId}" is missing apiKey`,
|
|
115
107
|
);
|
|
116
108
|
}
|
|
117
109
|
}
|
|
118
110
|
|
|
119
111
|
function assertAndoRealtimeConfigured(account: ResolvedAndoAccount): void {
|
|
120
112
|
assertAndoRestConfigured(account);
|
|
121
|
-
|
|
122
|
-
if (!account.realtimeHost) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`[ando] account "${account.accountId}" is missing realtimeHost for realtime monitoring`,
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
113
|
}
|
|
128
114
|
|
|
129
115
|
export function resolveThreadRootId(params: {
|
|
@@ -213,8 +199,8 @@ export const andoPlugin: ChannelPlugin<ResolvedAndoAccount> = {
|
|
|
213
199
|
...andoConfigBase,
|
|
214
200
|
isConfigured: isAndoRestConfigured,
|
|
215
201
|
unconfiguredReason: (account) => {
|
|
216
|
-
if (!account.
|
|
217
|
-
return "
|
|
202
|
+
if (!account.apiKey) {
|
|
203
|
+
return "apiKey is required";
|
|
218
204
|
}
|
|
219
205
|
return "Ando channel is configured";
|
|
220
206
|
},
|
|
@@ -261,14 +247,6 @@ export const andoPlugin: ChannelPlugin<ResolvedAndoAccount> = {
|
|
|
261
247
|
gateway: {
|
|
262
248
|
startAccount: async (ctx) => {
|
|
263
249
|
const account = ctx.account;
|
|
264
|
-
if (!account.realtimeHost) {
|
|
265
|
-
ctx.runtime.log?.(
|
|
266
|
-
`[ando] skipping realtime monitoring for account "${account.accountId}" because realtimeHost is not configured`
|
|
267
|
-
);
|
|
268
|
-
await waitForAbort(ctx.abortSignal);
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
250
|
assertAndoRealtimeConfigured(account);
|
|
273
251
|
const channelRuntime = ctx.channelRuntime;
|
|
274
252
|
if (!channelRuntime) {
|
package/src/client.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ResolvedAndoAccount } from "./types.js";
|
|
|
4
4
|
export function createAndoSdkClient(account: ResolvedAndoAccount) {
|
|
5
5
|
return new AndoClient({
|
|
6
6
|
baseUrl: account.baseUrl,
|
|
7
|
-
realtimeHost: account.realtimeHost
|
|
7
|
+
realtimeHost: account.realtimeHost,
|
|
8
8
|
auth: {
|
|
9
9
|
apiKey: account.apiKey,
|
|
10
10
|
},
|
package/src/mcp-tool-catalog.ts
CHANGED
|
@@ -38,7 +38,7 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
|
|
|
38
38
|
{
|
|
39
39
|
name: "search_messages",
|
|
40
40
|
description:
|
|
41
|
-
"Search messages across conversations by keyword
|
|
41
|
+
"Search messages across conversations by keyword. Requires a real search query — do not use wildcards like '*'. To browse recent messages or filter by author without a keyword, use get_conversation_messages instead.",
|
|
42
42
|
inputSchema: objectSchema(
|
|
43
43
|
{
|
|
44
44
|
q: stringProperty(
|
|
@@ -61,7 +61,7 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
|
|
|
61
61
|
type: "string",
|
|
62
62
|
enum: ["full-text", "semantic"],
|
|
63
63
|
description:
|
|
64
|
-
"
|
|
64
|
+
"Accepted for backwards compatibility; message search uses Convex keyword search",
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
["q"]
|
|
@@ -89,6 +89,17 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
|
|
|
89
89
|
["q"]
|
|
90
90
|
),
|
|
91
91
|
},
|
|
92
|
+
{
|
|
93
|
+
name: "list_conversations",
|
|
94
|
+
description:
|
|
95
|
+
"List conversations (channels and DMs) that the MCP caller is a member of. Optionally filter by conversation name or description.",
|
|
96
|
+
inputSchema: objectSchema({
|
|
97
|
+
q: stringProperty("Optional query text to filter conversations"),
|
|
98
|
+
limit: numberProperty(
|
|
99
|
+
"Maximum number of conversations to return (default 50, max 100)"
|
|
100
|
+
),
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
92
103
|
{
|
|
93
104
|
name: "search_clipboard",
|
|
94
105
|
description:
|
|
@@ -216,6 +227,44 @@ const CURATED_ANDO_MCP_TOOLS: ToolDefinition[] = [
|
|
|
216
227
|
["message_id"]
|
|
217
228
|
),
|
|
218
229
|
},
|
|
230
|
+
{
|
|
231
|
+
name: "list_conversation_members",
|
|
232
|
+
description:
|
|
233
|
+
"List active members in a conversation the MCP caller can access. Returns member IDs, display names, member types, and workspace roles.",
|
|
234
|
+
inputSchema: objectSchema(
|
|
235
|
+
{
|
|
236
|
+
conversation_id: stringProperty("The conversation ID"),
|
|
237
|
+
},
|
|
238
|
+
["conversation_id"]
|
|
239
|
+
),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: "react_to_message",
|
|
243
|
+
description:
|
|
244
|
+
"React to a message with a supported standard emoji or active workspace custom emoji.",
|
|
245
|
+
inputSchema: objectSchema(
|
|
246
|
+
{
|
|
247
|
+
message_id: stringProperty("The message ID to react to"),
|
|
248
|
+
emoji: stringProperty(
|
|
249
|
+
"Emoji to react with, such as :thumbs_up: or :ship-it:"
|
|
250
|
+
),
|
|
251
|
+
},
|
|
252
|
+
["message_id", "emoji"]
|
|
253
|
+
),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "delete_message",
|
|
257
|
+
description:
|
|
258
|
+
"Delete one of your own sent messages. This tool can only delete messages authored by the MCP caller; it cannot delete messages from other members or agents.",
|
|
259
|
+
inputSchema: objectSchema(
|
|
260
|
+
{
|
|
261
|
+
message_id: stringProperty(
|
|
262
|
+
"The ID of one of your own sent messages to delete"
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
["message_id"]
|
|
266
|
+
),
|
|
267
|
+
},
|
|
219
268
|
];
|
|
220
269
|
|
|
221
270
|
export function listCuratedAndoMcpTools(): ToolDefinition[] {
|
package/src/mcp-tools.ts
CHANGED
|
@@ -227,7 +227,10 @@ async function closeMcpSession(params: {
|
|
|
227
227
|
);
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
async function initializeMcpSession(
|
|
230
|
+
async function initializeMcpSession(
|
|
231
|
+
mcpUrl: string,
|
|
232
|
+
apiKey: string,
|
|
233
|
+
): Promise<string | null> {
|
|
231
234
|
const response = await runMcpJsonRpc({
|
|
232
235
|
mcpUrl,
|
|
233
236
|
apiKey,
|
|
@@ -250,11 +253,7 @@ async function initializeMcpSession(mcpUrl: string, apiKey: string): Promise<str
|
|
|
250
253
|
ensureMcpOk(payload);
|
|
251
254
|
|
|
252
255
|
const sessionId = response.sessionId?.trim();
|
|
253
|
-
|
|
254
|
-
throw new Error("Missing MCP session ID");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return sessionId;
|
|
256
|
+
return sessionId || null;
|
|
258
257
|
}
|
|
259
258
|
|
|
260
259
|
async function notifyMcpSessionInitialized(params: {
|
|
@@ -281,16 +280,18 @@ async function callAndoMcpTool(params: {
|
|
|
281
280
|
}): Promise<string> {
|
|
282
281
|
const sessionId = await initializeMcpSession(params.mcpUrl, params.apiKey);
|
|
283
282
|
try {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
283
|
+
if (sessionId) {
|
|
284
|
+
await notifyMcpSessionInitialized({
|
|
285
|
+
mcpUrl: params.mcpUrl,
|
|
286
|
+
apiKey: params.apiKey,
|
|
287
|
+
sessionId,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
289
290
|
|
|
290
291
|
const response = await runMcpJsonRpc({
|
|
291
292
|
mcpUrl: params.mcpUrl,
|
|
292
293
|
apiKey: params.apiKey,
|
|
293
|
-
sessionId,
|
|
294
|
+
...(sessionId ? { sessionId } : {}),
|
|
294
295
|
action: `tools/call(${params.name})`,
|
|
295
296
|
request: buildMcpJsonRpcRequest({
|
|
296
297
|
id: "2",
|
|
@@ -306,11 +307,13 @@ async function callAndoMcpTool(params: {
|
|
|
306
307
|
ensureMcpOk(payload);
|
|
307
308
|
return formatMcpToolResultText(asMcpToolResult(payload.result));
|
|
308
309
|
} finally {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
310
|
+
if (sessionId) {
|
|
311
|
+
await closeMcpSession({
|
|
312
|
+
mcpUrl: params.mcpUrl,
|
|
313
|
+
apiKey: params.apiKey,
|
|
314
|
+
sessionId,
|
|
315
|
+
}).catch(() => undefined);
|
|
316
|
+
}
|
|
314
317
|
}
|
|
315
318
|
}
|
|
316
319
|
|
|
@@ -356,7 +359,10 @@ export function registerAndoMcpTools(
|
|
|
356
359
|
} catch (error) {
|
|
357
360
|
const message =
|
|
358
361
|
error instanceof Error ? error.message : String(error);
|
|
359
|
-
throw new Error(
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Error calling Ando MCP tool ${tool.name}: ${message}`,
|
|
364
|
+
{ cause: error }
|
|
365
|
+
);
|
|
360
366
|
}
|
|
361
367
|
},
|
|
362
368
|
};
|