@thenavidm/threads-mcp-cli 1.1.2 → 2.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 +106 -53
- package/SKILL.md +9 -8
- package/dist/app.d.ts +9 -0
- package/dist/app.js +101 -0
- package/dist/app.js.map +1 -0
- package/dist/auth/login.js +2 -1
- package/dist/auth/login.js.map +1 -1
- package/dist/auth/refresh.d.ts +8 -0
- package/dist/auth/refresh.js +39 -0
- package/dist/auth/refresh.js.map +1 -0
- package/dist/doctor.d.ts +11 -4
- package/dist/doctor.js +64 -125
- package/dist/doctor.js.map +1 -1
- package/dist/{server.d.ts → guide.d.ts} +14 -17
- package/dist/{server.js → guide.js} +18 -122
- package/dist/guide.js.map +1 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +9 -162
- package/dist/index.js.map +1 -1
- package/dist/tools/accounts.d.ts +5 -18
- package/dist/tools/discover.d.ts +5 -46
- package/dist/tools/insights.d.ts +5 -36
- package/dist/tools/kit.d.ts +23 -49
- package/dist/tools/kit.js +64 -79
- package/dist/tools/kit.js.map +1 -1
- package/dist/tools/posts.d.ts +10 -161
- package/dist/tools/read.d.ts +3 -24
- package/dist/tools/replies.d.ts +8 -79
- package/package.json +11 -6
- package/dist/cli.d.ts +0 -59
- package/dist/cli.js +0 -444
- package/dist/cli.js.map +0 -1
- package/dist/safety.d.ts +0 -52
- package/dist/safety.js +0 -85
- package/dist/safety.js.map +0 -1
- package/dist/server.js.map +0 -1
- package/dist/transport/http.d.ts +0 -28
- package/dist/transport/http.js +0 -107
- package/dist/transport/http.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,167 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Both binaries. `threads-mcp` with no arguments serves MCP over stdio,
|
|
4
|
+
* `--http` serves it over HTTP, and any command runs one tool from the shell.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `threads-mcp doctor` check the setup and say what is wrong
|
|
9
|
-
* `threads-mcp --http` HTTP, for running it somewhere always on
|
|
10
|
-
*
|
|
11
|
-
* `threads-cli` the same tools as shell commands
|
|
12
|
-
*/
|
|
13
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
-
import { buildServer, VERSION } from "./server.js";
|
|
15
|
-
import { loadConfig } from "./config.js";
|
|
16
|
-
import { accountsFromStore } from "./auth/store.js";
|
|
17
|
-
import { httpOptionsFromEnv, startHttpServer } from "./transport/http.js";
|
|
18
|
-
import { daysRemaining } from "./auth/tokens.js";
|
|
19
|
-
import { runCli, isCliCommand } from "./cli.js";
|
|
20
|
-
const HELP = `threads-mcp ${VERSION}
|
|
21
|
-
|
|
22
|
-
threads-mcp Run over stdio. This is what an MCP client launches.
|
|
23
|
-
threads-mcp login Authorise a Threads profile and store a 60-day token.
|
|
24
|
-
threads-mcp refresh Extend every stored token by another 60 days.
|
|
25
|
-
threads-mcp doctor Check the setup and report what is wrong.
|
|
26
|
-
threads-mcp --http [--port=N] Run over HTTP, for a machine that is always on.
|
|
27
|
-
threads-mcp --version Print the version.
|
|
28
|
-
|
|
29
|
-
threads-cli List every tool as a shell command.
|
|
30
|
-
threads-cli <command> --help What one command takes.
|
|
31
|
-
threads-cli schema <command> The JSON schema an MCP client sees.
|
|
32
|
-
|
|
33
|
-
Credentials, in priority order:
|
|
34
|
-
THREADS_ACCOUNTS JSON array, for several profiles at once:
|
|
35
|
-
[{"access_token":"THQ...","username":"you"}]
|
|
36
|
-
THREADS_ACCESS_TOKEN a long-lived token for one profile
|
|
37
|
-
THREADS_USER_ID numeric profile id. Resolved from the token when absent
|
|
38
|
-
THREADS_USERNAME username, for matching and display. Also resolved
|
|
39
|
-
the token store written by \`login\`, and the only source this
|
|
40
|
-
server can keep refreshed on its own
|
|
41
|
-
|
|
42
|
-
For \`login\` only:
|
|
43
|
-
THREADS_APP_ID Threads app id from developers.facebook.com
|
|
44
|
-
THREADS_APP_SECRET Threads app secret
|
|
45
|
-
|
|
46
|
-
Options:
|
|
47
|
-
THREADS_DEFAULT_ACCOUNT which username acts when a tool names none
|
|
48
|
-
THREADS_READ_ONLY=1 hide every write from the tool list
|
|
49
|
-
THREADS_ALLOW_DESTRUCTIVE=0 keep writes, block posting and deleting
|
|
50
|
-
THREADS_TOKEN_STORE where tokens are kept, default ~/.threads-mcp/tokens.json
|
|
51
|
-
THREADS_PERSIST_TOKENS=0 stop writing refreshed tokens back to the store
|
|
52
|
-
THREADS_REFRESH_WINDOW_DAYS refresh this many days before expiry, default 20
|
|
53
|
-
THREADS_CONTAINER_TIMEOUT_MS how long to wait for media, default 120000
|
|
54
|
-
THREADS_REQUEST_TIMEOUT_MS per-request deadline, default 30000
|
|
55
|
-
THREADS_MIN_REQUEST_INTERVAL_MS spacing between requests, default 120
|
|
56
|
-
THREADS_MAX_RETRIES retries on 5xx and quota codes, default 3
|
|
57
|
-
THREADS_AUDIT_LOG append-only log of every attempted write
|
|
58
|
-
THREADS_GRAPH_HOST override the Graph host, default graph.threads.net
|
|
59
|
-
THREADS_USER_AGENT override the User-Agent sent to Meta
|
|
60
|
-
THREADS_HTTP_PORT / _HOST / _TOKEN for --http
|
|
61
|
-
|
|
62
|
-
https://github.com/thenavidm/threads-mcp-cli
|
|
63
|
-
`;
|
|
64
|
-
/**
|
|
65
|
-
* One entry point, two programs. `threads-mcp` is the server and must stay
|
|
66
|
-
* silent on stdout; `threads-cli` is the one a person types. Running the CLI
|
|
67
|
-
* binary with no arguments is someone asking what they can type, so it lists
|
|
68
|
-
* the commands rather than hanging on a transport that will never speak.
|
|
6
|
+
* Node's compile cache goes on before the app loads, so every launch after the
|
|
7
|
+
* first skips compiling it again. Node before 22.8 has no compile cache and
|
|
8
|
+
* starts as before; NODE_DISABLE_COMPILE_CACHE=1 turns it off.
|
|
69
9
|
*/
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
async function main() {
|
|
75
|
-
const argv = process.argv.slice(2);
|
|
76
|
-
const command = argv[0];
|
|
77
|
-
if (invokedAsCli() && argv.length === 0) {
|
|
78
|
-
process.exitCode = await runCli(["tools"]);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
// Checked before --help and --version so `<tool> --help` reaches the tool.
|
|
82
|
-
// A bare `--help` starts with a dash, so it falls through to the block below.
|
|
83
|
-
if (isCliCommand(argv)) {
|
|
84
|
-
process.exitCode = await runCli(argv);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
// An unknown word used to fall through and start the server, which then sat
|
|
88
|
-
// waiting on stdin: a typo looked like a hang, and scripts saw exit code 0.
|
|
89
|
-
//
|
|
90
|
-
// `doctor`, `login` and `refresh` belong to the entry point rather than the
|
|
91
|
-
// tool list, and they are the first things someone types when nothing works.
|
|
92
|
-
// Rejecting them as unknown commands sent them to the server binary to
|
|
93
|
-
// diagnose the CLI.
|
|
94
|
-
const ENTRY_COMMANDS = new Set(["doctor", "login", "refresh", "help"]);
|
|
95
|
-
if (invokedAsCli() &&
|
|
96
|
-
command !== undefined &&
|
|
97
|
-
!command.startsWith("-") &&
|
|
98
|
-
!ENTRY_COMMANDS.has(command)) {
|
|
99
|
-
process.stderr.write(`${JSON.stringify({ error: `Unknown command '${command}'. Run \`threads-cli\` to list them.` }, null, 2)}\n`);
|
|
100
|
-
process.exitCode = 1;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
if (argv.includes("--help") || argv.includes("-h") || command === "help") {
|
|
104
|
-
process.stdout.write(HELP);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
if (argv.includes("--version") || argv.includes("-v")) {
|
|
108
|
-
process.stdout.write(`${VERSION}\n`);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (command === "login") {
|
|
112
|
-
const { runLogin } = await import("./auth/login.js");
|
|
113
|
-
process.exitCode = await runLogin(argv.slice(1));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (command === "doctor") {
|
|
117
|
-
const { runDoctor } = await import("./doctor.js");
|
|
118
|
-
process.exitCode = await runDoctor();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (command === "refresh") {
|
|
122
|
-
const { runRefresh } = await import("./doctor.js");
|
|
123
|
-
process.exitCode = await runRefresh();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
// The store is read here rather than inside loadConfig so that the config
|
|
127
|
-
// module stays free of filesystem access and remains trivially testable.
|
|
128
|
-
const stored = accountsFromStore(loadConfig().storePath);
|
|
129
|
-
const config = loadConfig(stored);
|
|
130
|
-
const built = buildServer(config);
|
|
131
|
-
// Warn, never block. A network check at startup would delay the handshake,
|
|
132
|
-
// and the failure is more actionable on the tool call that hits it.
|
|
133
|
-
if (config.accounts.length === 0) {
|
|
134
|
-
process.stderr.write("[threads-mcp] No credentials configured. Every tool will report the missing setup. Run `threads-mcp login`.\n");
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
const soon = config.accounts.filter((a) => {
|
|
138
|
-
const days = daysRemaining(a);
|
|
139
|
-
return days !== undefined && days <= 7;
|
|
140
|
-
});
|
|
141
|
-
if (soon.length) {
|
|
142
|
-
process.stderr.write(`[threads-mcp] ${soon.length} token(s) expire within a week and will be refreshed automatically on the next call. An expired Threads token cannot be recovered.\n`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
const shutdown = async (close) => {
|
|
146
|
-
if (close)
|
|
147
|
-
await close().catch(() => undefined);
|
|
148
|
-
process.exit(0);
|
|
149
|
-
};
|
|
150
|
-
if (argv.includes("--http")) {
|
|
151
|
-
const { close } = await startHttpServer(built, httpOptionsFromEnv(argv));
|
|
152
|
-
process.on("SIGTERM", () => void shutdown(close));
|
|
153
|
-
process.on("SIGINT", () => void shutdown(close));
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
const transport = new StdioServerTransport();
|
|
157
|
-
await built.server.connect(transport);
|
|
158
|
-
// Handled so `docker stop` and a client shutting down return promptly rather
|
|
159
|
-
// than waiting out a grace period.
|
|
160
|
-
process.on("SIGTERM", () => void shutdown());
|
|
161
|
-
process.on("SIGINT", () => void shutdown());
|
|
162
|
-
}
|
|
163
|
-
main().catch((error) => {
|
|
164
|
-
process.stderr.write(`[threads-mcp] ${error.message}\n`);
|
|
165
|
-
process.exit(1);
|
|
166
|
-
});
|
|
10
|
+
import * as nodeModule from "node:module";
|
|
11
|
+
nodeModule.enableCompileCache?.();
|
|
12
|
+
const { app } = await import("./app.js");
|
|
13
|
+
await app.main();
|
|
167
14
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;GAOG;AAEH,OAAO,KAAK,UAAU,MAAM,aAAa,CAAC;AAE1C,UAAU,CAAC,kBAAkB,EAAE,EAAE,CAAC;AAClC,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAC;AACzC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC"}
|
package/dist/tools/accounts.d.ts
CHANGED
|
@@ -5,23 +5,10 @@
|
|
|
5
5
|
* tools rather than footnotes: the 60-day token clock, and the daily publishing
|
|
6
6
|
* quota. Both fail silently and both are cheap to check.
|
|
7
7
|
*/
|
|
8
|
-
import { z } from "zod";
|
|
9
8
|
import { clamp } from "./kit.js";
|
|
10
|
-
export declare const listAccounts: import("./kit.js").
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export declare const
|
|
14
|
-
|
|
15
|
-
}>;
|
|
16
|
-
export declare const getPublishingLimit: import("./kit.js").ToolSpec<{
|
|
17
|
-
account: z.ZodOptional<z.ZodString>;
|
|
18
|
-
}>;
|
|
19
|
-
export declare const refreshToken: import("./kit.js").ToolSpec<{
|
|
20
|
-
account: z.ZodOptional<z.ZodString>;
|
|
21
|
-
}>;
|
|
22
|
-
export declare const ACCOUNT_TOOLS: (import("./kit.js").ToolSpec<{
|
|
23
|
-
fast: z.ZodOptional<z.ZodBoolean>;
|
|
24
|
-
}> | import("./kit.js").ToolSpec<{
|
|
25
|
-
account: z.ZodOptional<z.ZodString>;
|
|
26
|
-
}>)[];
|
|
9
|
+
export declare const listAccounts: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
10
|
+
export declare const whoami: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
11
|
+
export declare const getPublishingLimit: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
12
|
+
export declare const refreshToken: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
13
|
+
export declare const ACCOUNT_TOOLS: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>[];
|
|
27
14
|
export { clamp };
|
package/dist/tools/discover.d.ts
CHANGED
|
@@ -8,49 +8,8 @@
|
|
|
8
8
|
* indistinguishable from a niche query. So these tools say which mode they are
|
|
9
9
|
* in rather than letting a model draw a conclusion from a filtered corpus.
|
|
10
10
|
*/
|
|
11
|
-
|
|
12
|
-
export declare const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
q: z.ZodString;
|
|
17
|
-
search_type: z.ZodOptional<z.ZodEnum<["TOP", "RECENT"]>>;
|
|
18
|
-
media_type: z.ZodOptional<z.ZodEnum<["TEXT_POST", "IMAGE", "VIDEO", "CAROUSEL_ALBUM", "REPOST_FACADE"]>>;
|
|
19
|
-
since: z.ZodOptional<z.ZodString>;
|
|
20
|
-
until: z.ZodOptional<z.ZodString>;
|
|
21
|
-
}>;
|
|
22
|
-
export declare const searchTopicTag: import("./kit.js").ToolSpec<{
|
|
23
|
-
account: z.ZodOptional<z.ZodString>;
|
|
24
|
-
limit: z.ZodOptional<z.ZodNumber>;
|
|
25
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
26
|
-
tag: z.ZodString;
|
|
27
|
-
search_type: z.ZodOptional<z.ZodEnum<["TOP", "RECENT"]>>;
|
|
28
|
-
}>;
|
|
29
|
-
export declare const lookupProfile: import("./kit.js").ToolSpec<{
|
|
30
|
-
account: z.ZodOptional<z.ZodString>;
|
|
31
|
-
username: z.ZodString;
|
|
32
|
-
}>;
|
|
33
|
-
export declare const listAllowlistedCountries: import("./kit.js").ToolSpec<{
|
|
34
|
-
account: z.ZodOptional<z.ZodString>;
|
|
35
|
-
}>;
|
|
36
|
-
export declare const DISCOVER_TOOLS: (import("./kit.js").ToolSpec<{
|
|
37
|
-
account: z.ZodOptional<z.ZodString>;
|
|
38
|
-
limit: z.ZodOptional<z.ZodNumber>;
|
|
39
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
40
|
-
q: z.ZodString;
|
|
41
|
-
search_type: z.ZodOptional<z.ZodEnum<["TOP", "RECENT"]>>;
|
|
42
|
-
media_type: z.ZodOptional<z.ZodEnum<["TEXT_POST", "IMAGE", "VIDEO", "CAROUSEL_ALBUM", "REPOST_FACADE"]>>;
|
|
43
|
-
since: z.ZodOptional<z.ZodString>;
|
|
44
|
-
until: z.ZodOptional<z.ZodString>;
|
|
45
|
-
}> | import("./kit.js").ToolSpec<{
|
|
46
|
-
account: z.ZodOptional<z.ZodString>;
|
|
47
|
-
limit: z.ZodOptional<z.ZodNumber>;
|
|
48
|
-
cursor: z.ZodOptional<z.ZodString>;
|
|
49
|
-
tag: z.ZodString;
|
|
50
|
-
search_type: z.ZodOptional<z.ZodEnum<["TOP", "RECENT"]>>;
|
|
51
|
-
}> | import("./kit.js").ToolSpec<{
|
|
52
|
-
account: z.ZodOptional<z.ZodString>;
|
|
53
|
-
username: z.ZodString;
|
|
54
|
-
}> | import("./kit.js").ToolSpec<{
|
|
55
|
-
account: z.ZodOptional<z.ZodString>;
|
|
56
|
-
}>)[];
|
|
11
|
+
export declare const searchKeyword: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
12
|
+
export declare const searchTopicTag: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
13
|
+
export declare const lookupProfile: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
14
|
+
export declare const listAllowlistedCountries: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
15
|
+
export declare const DISCOVER_TOOLS: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>[];
|
package/dist/tools/insights.d.ts
CHANGED
|
@@ -17,39 +17,8 @@
|
|
|
17
17
|
* views tells you which ones worked. That means a fetch per post, so it is
|
|
18
18
|
* bounded and says what it sampled.
|
|
19
19
|
*/
|
|
20
|
-
|
|
21
|
-
export declare const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export declare const getAccountInsights: import("./kit.js").ToolSpec<{
|
|
26
|
-
account: z.ZodOptional<z.ZodString>;
|
|
27
|
-
since: z.ZodOptional<z.ZodString>;
|
|
28
|
-
until: z.ZodOptional<z.ZodString>;
|
|
29
|
-
metrics: z.ZodOptional<z.ZodArray<z.ZodEnum<["views", "likes", "replies", "reposts", "quotes", "clicks", "followers_count"]>, "many">>;
|
|
30
|
-
}>;
|
|
31
|
-
export declare const getFollowerDemographics: import("./kit.js").ToolSpec<{
|
|
32
|
-
account: z.ZodOptional<z.ZodString>;
|
|
33
|
-
breakdown: z.ZodEnum<["country", "city", "age", "gender"]>;
|
|
34
|
-
}>;
|
|
35
|
-
export declare const getTopPosts: import("./kit.js").ToolSpec<{
|
|
36
|
-
account: z.ZodOptional<z.ZodString>;
|
|
37
|
-
sample: z.ZodOptional<z.ZodNumber>;
|
|
38
|
-
sort_by: z.ZodOptional<z.ZodEnum<["engagement_rate", "views", "likes", "replies", "reposts", "quotes"]>>;
|
|
39
|
-
}>;
|
|
40
|
-
export declare const INSIGHT_TOOLS: (import("./kit.js").ToolSpec<{
|
|
41
|
-
account: z.ZodOptional<z.ZodString>;
|
|
42
|
-
id: z.ZodString;
|
|
43
|
-
}> | import("./kit.js").ToolSpec<{
|
|
44
|
-
account: z.ZodOptional<z.ZodString>;
|
|
45
|
-
since: z.ZodOptional<z.ZodString>;
|
|
46
|
-
until: z.ZodOptional<z.ZodString>;
|
|
47
|
-
metrics: z.ZodOptional<z.ZodArray<z.ZodEnum<["views", "likes", "replies", "reposts", "quotes", "clicks", "followers_count"]>, "many">>;
|
|
48
|
-
}> | import("./kit.js").ToolSpec<{
|
|
49
|
-
account: z.ZodOptional<z.ZodString>;
|
|
50
|
-
breakdown: z.ZodEnum<["country", "city", "age", "gender"]>;
|
|
51
|
-
}> | import("./kit.js").ToolSpec<{
|
|
52
|
-
account: z.ZodOptional<z.ZodString>;
|
|
53
|
-
sample: z.ZodOptional<z.ZodNumber>;
|
|
54
|
-
sort_by: z.ZodOptional<z.ZodEnum<["engagement_rate", "views", "likes", "replies", "reposts", "quotes"]>>;
|
|
55
|
-
}>)[];
|
|
20
|
+
export declare const getPostInsights: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
21
|
+
export declare const getAccountInsights: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
22
|
+
export declare const getFollowerDemographics: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
23
|
+
export declare const getTopPosts: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>;
|
|
24
|
+
export declare const INSIGHT_TOOLS: import("@thenavidm/slipway").Tool<import("./kit.js").ToolContext>[];
|
package/dist/tools/kit.d.ts
CHANGED
|
@@ -1,51 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared plumbing every tool uses.
|
|
2
|
+
* Shared plumbing every tool uses, now on Slipway.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Tool modules keep describing themselves with a Zod shape, a risk and a
|
|
5
|
+
* handler. This adapter turns each into a Slipway tool, so the MCP server, the
|
|
6
|
+
* CLI, the write guard, annotations and errors all come from the framework
|
|
7
|
+
* instead of a copy kept in this repo.
|
|
7
8
|
*/
|
|
8
|
-
import type
|
|
9
|
-
import { z, type ZodRawShape } from "zod";
|
|
9
|
+
import { z, type Risk, type SlipwayError, type Tool } from "@thenavidm/slipway";
|
|
10
10
|
import type { ThreadsClient } from "../api/client.js";
|
|
11
|
+
import { ThreadsError } from "../api/errors.js";
|
|
11
12
|
import type { Account, Config } from "../config.js";
|
|
12
|
-
import { type Risk, type WriteGuard } from "../safety.js";
|
|
13
13
|
export type ToolContext = {
|
|
14
14
|
client: ThreadsClient;
|
|
15
15
|
config: Config;
|
|
16
|
-
guard: WriteGuard;
|
|
17
16
|
/** Resolve which profile this call acts as. */
|
|
18
17
|
account: (hint?: string) => Account;
|
|
19
18
|
};
|
|
20
|
-
export type ToolResult = {
|
|
21
|
-
content: {
|
|
22
|
-
type: "text";
|
|
23
|
-
text: string;
|
|
24
|
-
}[];
|
|
25
|
-
isError?: boolean;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* A tool returns either a pre-rendered string or a value to serialise.
|
|
29
|
-
*
|
|
30
|
-
* The reading tools return the tagged format from `format/posts.ts`, which is
|
|
31
|
-
* already text. The writing tools return a small object, an id and a permalink,
|
|
32
|
-
* where JSON is clearer than tags. Both go through here so neither has to think
|
|
33
|
-
* about the MCP content envelope.
|
|
34
|
-
*/
|
|
35
|
-
export declare function ok(data: unknown): ToolResult;
|
|
36
|
-
/**
|
|
37
|
-
* Errors come back as a normal result with `isError`, not a thrown exception.
|
|
38
|
-
*
|
|
39
|
-
* A thrown MCP error reaches the model as a protocol failure with no structure.
|
|
40
|
-
* A result it can read tells it what went wrong and usually how to fix it,
|
|
41
|
-
* which is the difference between a correct retry and a give-up.
|
|
42
|
-
*/
|
|
43
|
-
export declare function fail(error: unknown): ToolResult;
|
|
44
19
|
/** The optional argument that picks a profile, on every account-scoped tool. */
|
|
45
20
|
export declare const accountArg: {
|
|
46
21
|
account: z.ZodOptional<z.ZodString>;
|
|
47
22
|
};
|
|
48
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Kept so tool modules read the same, but never sent: Slipway adds `confirm`
|
|
25
|
+
* to every irreversible tool itself, with one description everywhere.
|
|
26
|
+
*/
|
|
49
27
|
export declare const confirmArg: {
|
|
50
28
|
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
51
29
|
};
|
|
@@ -54,7 +32,8 @@ export declare const pageArgs: {
|
|
|
54
32
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
55
33
|
cursor: z.ZodOptional<z.ZodString>;
|
|
56
34
|
};
|
|
57
|
-
|
|
35
|
+
type Shape = Record<string, z.ZodType>;
|
|
36
|
+
export type ToolSpec<S extends Shape> = {
|
|
58
37
|
name: string;
|
|
59
38
|
/** One line, imperative. Shown in tool pickers. */
|
|
60
39
|
title: string;
|
|
@@ -67,24 +46,19 @@ export type ToolSpec<S extends ZodRawShape> = {
|
|
|
67
46
|
/** One line for the audit log and the confirm message, when this is a write. */
|
|
68
47
|
summary?: (args: z.infer<z.ZodObject<S>>) => string;
|
|
69
48
|
};
|
|
70
|
-
export
|
|
49
|
+
export type AnyToolSpec = Tool<ToolContext>;
|
|
71
50
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* inside each `defineTool` call, where schema and handler are checked against
|
|
78
|
-
* each other. This only loosens the seam where they are collected.
|
|
51
|
+
* Meta answers almost every failure with HTTP 400, an expired token and a
|
|
52
|
+
* spent quota included, and puts what happened in `code` and `error_subcode`.
|
|
53
|
+
* `api/errors.ts` already sorts them into classes, so the exit code and error
|
|
54
|
+
* code follow the class, not the status, and Meta's own fields ride along in
|
|
55
|
+
* `details` for the model to read.
|
|
79
56
|
*/
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
84
|
-
/** Register one tool against the server, with guarding and error handling applied. */
|
|
85
|
-
export declare function register(server: McpServer, contextFor: (extra: unknown) => ToolContext, spec: AnyToolSpec): void;
|
|
86
|
-
export declare function makeContext(client: ThreadsClient, config: Config, guard: WriteGuard): ToolContext;
|
|
57
|
+
export declare function toSlipway(error: ThreadsError): SlipwayError;
|
|
58
|
+
export declare function defineTool<S extends Shape>(spec: ToolSpec<S>): Tool<ToolContext>;
|
|
59
|
+
export declare function makeContext(client: ThreadsClient, config: Config): ToolContext;
|
|
87
60
|
/** Clamp a caller-supplied limit into a range Threads will accept. */
|
|
88
61
|
export declare function clamp(value: number | undefined, fallback: number, max?: number): number;
|
|
89
62
|
/** Trim a summary to one readable line for the audit log. */
|
|
90
63
|
export declare function snippet(text: string | undefined, length?: number): string;
|
|
64
|
+
export {};
|
package/dist/tools/kit.js
CHANGED
|
@@ -1,39 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared plumbing every tool uses.
|
|
2
|
+
* Shared plumbing every tool uses, now on Slipway.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Tool modules keep describing themselves with a Zod shape, a risk and a
|
|
5
|
+
* handler. This adapter turns each into a Slipway tool, so the MCP server, the
|
|
6
|
+
* CLI, the write guard, annotations and errors all come from the framework
|
|
7
|
+
* instead of a copy kept in this repo.
|
|
7
8
|
*/
|
|
8
|
-
import { z } from "
|
|
9
|
-
import { ThreadsError } from "../api/errors.js";
|
|
9
|
+
import { ApiError, AuthError, NotFoundError as SlipwayNotFound, RateLimitError as SlipwayRateLimit, RefusedError, TimeoutError as SlipwayTimeout, toolkit, UsageError, z, } from "@thenavidm/slipway";
|
|
10
|
+
import { AuthenticationError, NotFoundError, PermissionError, RateLimitError, TextTooLongError, ThreadsError, TimeoutError, ValidationError, WriteBlockedError, } from "../api/errors.js";
|
|
10
11
|
import { selectAccount } from "../config.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A tool returns either a pre-rendered string or a value to serialise.
|
|
14
|
-
*
|
|
15
|
-
* The reading tools return the tagged format from `format/posts.ts`, which is
|
|
16
|
-
* already text. The writing tools return a small object, an id and a permalink,
|
|
17
|
-
* where JSON is clearer than tags. Both go through here so neither has to think
|
|
18
|
-
* about the MCP content envelope.
|
|
19
|
-
*/
|
|
20
|
-
export function ok(data) {
|
|
21
|
-
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
22
|
-
return { content: [{ type: "text", text }] };
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Errors come back as a normal result with `isError`, not a thrown exception.
|
|
26
|
-
*
|
|
27
|
-
* A thrown MCP error reaches the model as a protocol failure with no structure.
|
|
28
|
-
* A result it can read tells it what went wrong and usually how to fix it,
|
|
29
|
-
* which is the difference between a correct retry and a give-up.
|
|
30
|
-
*/
|
|
31
|
-
export function fail(error) {
|
|
32
|
-
const payload = error instanceof ThreadsError
|
|
33
|
-
? error.toJSON()
|
|
34
|
-
: { error: error?.message ?? String(error) };
|
|
35
|
-
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }], isError: true };
|
|
36
|
-
}
|
|
12
|
+
const kit = toolkit();
|
|
37
13
|
/** The optional argument that picks a profile, on every account-scoped tool. */
|
|
38
14
|
export const accountArg = {
|
|
39
15
|
account: z
|
|
@@ -41,67 +17,76 @@ export const accountArg = {
|
|
|
41
17
|
.optional()
|
|
42
18
|
.describe("Which connected Threads profile to act as, by username (for example 'thenavidm'). Defaults to the first connected profile. Call list_accounts to see them."),
|
|
43
19
|
};
|
|
44
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Kept so tool modules read the same, but never sent: Slipway adds `confirm`
|
|
22
|
+
* to every irreversible tool itself, with one description everywhere.
|
|
23
|
+
*/
|
|
45
24
|
export const confirmArg = {
|
|
46
|
-
confirm: z
|
|
47
|
-
.boolean()
|
|
48
|
-
.optional()
|
|
49
|
-
.describe("Must be true for this to run. The result is public immediately or cannot be undone, so it is refused without an explicit confirmation. Threads has no edit endpoint and no unsend."),
|
|
25
|
+
confirm: z.boolean().optional(),
|
|
50
26
|
};
|
|
51
27
|
/** Cursor and limit, on every paginating tool. */
|
|
52
28
|
export const pageArgs = {
|
|
53
|
-
limit: z
|
|
54
|
-
.number()
|
|
55
|
-
.int()
|
|
56
|
-
.min(1)
|
|
57
|
-
.max(100)
|
|
58
|
-
.optional()
|
|
59
|
-
.describe("How many to return, 1-100."),
|
|
29
|
+
limit: z.number().int().min(1).max(100).optional().describe("How many to return, 1-100."),
|
|
60
30
|
cursor: z
|
|
61
31
|
.string()
|
|
62
32
|
.optional()
|
|
63
33
|
.describe("Continue from a previous page. Pass the `cursor` attribute from the last result."),
|
|
64
34
|
};
|
|
65
|
-
|
|
66
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Meta answers almost every failure with HTTP 400, an expired token and a
|
|
37
|
+
* spent quota included, and puts what happened in `code` and `error_subcode`.
|
|
38
|
+
* `api/errors.ts` already sorts them into classes, so the exit code and error
|
|
39
|
+
* code follow the class, not the status, and Meta's own fields ride along in
|
|
40
|
+
* `details` for the model to read.
|
|
41
|
+
*/
|
|
42
|
+
export function toSlipway(error) {
|
|
43
|
+
const details = Object.fromEntries(Object.entries({
|
|
44
|
+
endpoint: error.endpoint,
|
|
45
|
+
meta_code: error.code || undefined,
|
|
46
|
+
meta_subcode: error.subcode || undefined,
|
|
47
|
+
meta_type: error.type || undefined,
|
|
48
|
+
detail: error.detail || undefined,
|
|
49
|
+
trace_id: error.traceId || undefined,
|
|
50
|
+
}).filter(([, value]) => value !== undefined));
|
|
51
|
+
const options = { ...(error.status ? { status: error.status } : {}), details, cause: error };
|
|
52
|
+
if (error instanceof AuthenticationError || error instanceof PermissionError)
|
|
53
|
+
return new AuthError(error.message, options);
|
|
54
|
+
if (error instanceof NotFoundError)
|
|
55
|
+
return new SlipwayNotFound(error.message, options);
|
|
56
|
+
if (error instanceof RateLimitError)
|
|
57
|
+
return new SlipwayRateLimit(error.message, options);
|
|
58
|
+
if (error instanceof ValidationError || error instanceof TextTooLongError)
|
|
59
|
+
return new UsageError(error.message, options);
|
|
60
|
+
if (error instanceof WriteBlockedError)
|
|
61
|
+
return new RefusedError(error.message, options);
|
|
62
|
+
if (error instanceof TimeoutError)
|
|
63
|
+
return new SlipwayTimeout(error.message, options);
|
|
64
|
+
// A server error, a container that never finished processing, or no answer at all: the service's failure, worth a retry.
|
|
65
|
+
return new ApiError(error.message, options);
|
|
67
66
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
export function defineTool(spec) {
|
|
68
|
+
const { confirm: _confirm, ...shape } = spec.schema;
|
|
69
|
+
const handler = spec.handler;
|
|
70
|
+
return kit.defineTool({
|
|
71
|
+
name: spec.name,
|
|
71
72
|
title: spec.title,
|
|
72
73
|
description: spec.description,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// generic over the same shape, but TypeScript cannot prove the two are equal
|
|
81
|
-
// through the indirection, so the cast lives at this single boundary rather
|
|
82
|
-
// than in every tool definition.
|
|
83
|
-
(async (args, extra) => {
|
|
84
|
-
try {
|
|
85
|
-
const ctx = contextFor(extra);
|
|
86
|
-
if (spec.risk !== "read") {
|
|
87
|
-
const summary = spec.summary?.(args) ?? spec.name;
|
|
88
|
-
const confirm = args.confirm;
|
|
89
|
-
ctx.guard.check(spec.name, spec.risk, confirm, summary);
|
|
74
|
+
input: z.object(shape),
|
|
75
|
+
risk: spec.risk,
|
|
76
|
+
...(spec.idempotent !== undefined ? { idempotent: spec.idempotent } : {}),
|
|
77
|
+
...(spec.summary ? { summary: spec.summary } : {}),
|
|
78
|
+
handler: async (args, ctx) => {
|
|
79
|
+
try {
|
|
80
|
+
return await handler(args, ctx);
|
|
90
81
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}));
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw error instanceof ThreadsError ? toSlipway(error) : error;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
});
|
|
97
87
|
}
|
|
98
|
-
export function makeContext(client, config
|
|
99
|
-
return {
|
|
100
|
-
client,
|
|
101
|
-
config,
|
|
102
|
-
guard,
|
|
103
|
-
account: (hint) => selectAccount(config, hint),
|
|
104
|
-
};
|
|
88
|
+
export function makeContext(client, config) {
|
|
89
|
+
return { client, config, account: (hint) => selectAccount(config, hint) };
|
|
105
90
|
}
|
|
106
91
|
/** Clamp a caller-supplied limit into a range Threads will accept. */
|
|
107
92
|
export function clamp(value, fallback, max = 100) {
|
package/dist/tools/kit.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kit.js","sourceRoot":"","sources":["../../src/tools/kit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"kit.js","sourceRoot":"","sources":["../../src/tools/kit.ts"],"names":[],"mappings":"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"}
|