@nooticr/mcp 1.26.19
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 +521 -0
- package/dist/auth.js +170 -0
- package/dist/config.js +54 -0
- package/dist/idempotency.js +32 -0
- package/dist/index.js +417 -0
- package/dist/nooticr.js +6 -0
- package/dist/oauth.js +263 -0
- package/dist/shared/brand-watch.js +297 -0
- package/dist/shared/cimd.js +231 -0
- package/dist/shared/collab.js +447 -0
- package/dist/shared/comment-review.js +171 -0
- package/dist/shared/connections.js +80 -0
- package/dist/shared/evidence.js +410 -0
- package/dist/shared/handoff.js +526 -0
- package/dist/shared/jobs.js +1785 -0
- package/dist/shared/nooticr.js +249 -0
- package/dist/shared/oauth.js +132 -0
- package/dist/shared/output-schemas.js +1046 -0
- package/dist/shared/own-account.js +559 -0
- package/dist/shared/performance.js +156 -0
- package/dist/shared/prompts.js +383 -0
- package/dist/shared/spend.js +158 -0
- package/dist/shared/tasks.js +88 -0
- package/dist/shared/tools-def.js +387 -0
- package/dist/shared/tools.js +2027 -0
- package/dist/shared/ui-template.js +2856 -0
- package/dist/shared/video.js +152 -0
- package/dist/shared/view-meta.js +34 -0
- package/dist/shared/watchlist.js +603 -0
- package/dist/video.js +6 -0
- package/package.json +62 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Mirrors `mcp_tool_cost_for` in crates/server/src/mcp_tools.rs. */
|
|
2
|
+
export const SEARCH_PLATFORMS = [
|
|
3
|
+
"youtube",
|
|
4
|
+
"tiktok",
|
|
5
|
+
"instagram",
|
|
6
|
+
"douyin",
|
|
7
|
+
"xiaohongshu",
|
|
8
|
+
"twitter",
|
|
9
|
+
"bilibili",
|
|
10
|
+
"reddit",
|
|
11
|
+
"weibo",
|
|
12
|
+
];
|
|
13
|
+
/** Two per network, except the one that costs an order of magnitude more upstream. */
|
|
14
|
+
export const CREDITS_PER_NETWORK = 2;
|
|
15
|
+
export const XIAOHONGSHU_CREDITS = 5;
|
|
16
|
+
/** Per creator checked, so the price is the length of the watchlist. */
|
|
17
|
+
export const CREDITS_PER_CREATOR = 2;
|
|
18
|
+
/**
|
|
19
|
+
* The hard ceilings `search_spoken_mentions` cannot cross, whatever it is
|
|
20
|
+
* asked for.
|
|
21
|
+
*
|
|
22
|
+
* Every other fan-out in this file prices something whose size is already
|
|
23
|
+
* known once a cheap first call returns — the watchlist length, the post
|
|
24
|
+
* count of a feed already fetched. A transcript search cannot do that: there
|
|
25
|
+
* is no way to know how many of a niche's candidates actually say the term
|
|
26
|
+
* without transcribing them, so the candidate count can never be the number
|
|
27
|
+
* that sets the price. Left uncapped, a wide niche sweep could spend a hundred
|
|
28
|
+
* credits transcribing to find three hits. These are the caps that make that
|
|
29
|
+
* impossible regardless of what `maxTranscripts` or the handle list ask for —
|
|
30
|
+
* clamp server-side, never trust the argument alone.
|
|
31
|
+
*/
|
|
32
|
+
export const MAX_SPOKEN_TRANSCRIPTS = 20;
|
|
33
|
+
/** get_user_posts calls a spoken-mention search will make, across explicit handles and the watchlist combined. */
|
|
34
|
+
export const MAX_SPOKEN_HANDLE_CALLS = 8;
|
|
35
|
+
/**
|
|
36
|
+
* What each backend call costs, so a tool that fans out can price itself.
|
|
37
|
+
*
|
|
38
|
+
* The job tools in jobs.ts are compositions: `answer_my_audience` is one
|
|
39
|
+
* `get_user_posts` plus a `get_post_comments` per post it opens, and nothing
|
|
40
|
+
* else. Billing is therefore the sum of what it actually called, which is only
|
|
41
|
+
* knowable if the per-call prices are written down somewhere. Mirrors
|
|
42
|
+
* `mcp_tool_cost_for` in crates/server/src/mcp_tools.rs, same as the constants
|
|
43
|
+
* above — a call missing from here prices as zero, which would understate a
|
|
44
|
+
* confirmation rather than overstate it, so add to it when a tool grows a
|
|
45
|
+
* new fetch.
|
|
46
|
+
*/
|
|
47
|
+
export const BACKEND_CALL_CREDITS = {
|
|
48
|
+
get_social_media: 1,
|
|
49
|
+
get_post_transcript: 1,
|
|
50
|
+
get_user_posts: 2,
|
|
51
|
+
get_post_comments: 2,
|
|
52
|
+
discover_social_posts: 2,
|
|
53
|
+
search_creators: 2,
|
|
54
|
+
get_similar_creators: 2,
|
|
55
|
+
discover_sounds: 2,
|
|
56
|
+
discover_hashtags: 2,
|
|
57
|
+
get_post_frames: 2,
|
|
58
|
+
};
|
|
59
|
+
/** What a run of backend calls costs, named one per call actually made. */
|
|
60
|
+
export function costOf(calls) {
|
|
61
|
+
return calls.reduce((total, name) => total + (BACKEND_CALL_CREDITS[name] ?? 0), 0);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* What a `search_mentions` call will cost.
|
|
65
|
+
*
|
|
66
|
+
* Omitting `platforms` means every network — which is the expensive default,
|
|
67
|
+
* and the reason this function exists.
|
|
68
|
+
*/
|
|
69
|
+
export function searchMentionsCost(platforms) {
|
|
70
|
+
const chosen = platforms && platforms.length > 0
|
|
71
|
+
? platforms
|
|
72
|
+
.map((p) => String(p).toLowerCase())
|
|
73
|
+
.filter((p) => SEARCH_PLATFORMS.includes(p))
|
|
74
|
+
: [...SEARCH_PLATFORMS];
|
|
75
|
+
if (chosen.length === 0)
|
|
76
|
+
return CREDITS_PER_NETWORK;
|
|
77
|
+
return chosen.reduce((total, p) => total + (p === "xiaohongshu" ? XIAOHONGSHU_CREDITS : CREDITS_PER_NETWORK), 0);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Above this, ask. Below it, the number is small enough that a prompt costs
|
|
81
|
+
* the user more attention than the credits are worth.
|
|
82
|
+
*
|
|
83
|
+
* Six is deliberate: a three-network sweep (6) passes silently, a four-network
|
|
84
|
+
* one (8) does not, and the all-networks default (21) never does.
|
|
85
|
+
*/
|
|
86
|
+
export const CONFIRM_ABOVE_CREDITS = 6;
|
|
87
|
+
/** A client that never declared elicitation must not be sent one. */
|
|
88
|
+
function canAsk(server) {
|
|
89
|
+
const caps = server.getClientCapabilities();
|
|
90
|
+
return Boolean(caps && "elicitation" in caps && caps.elicitation);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Ask before spending. Returns whether to go ahead.
|
|
94
|
+
*
|
|
95
|
+
* Anything unexpected — a client that declared elicitation and then threw, a
|
|
96
|
+
* transport that dropped — proceeds. The failure mode of a broken confirmation
|
|
97
|
+
* should be the behaviour we had before it existed, not a tool that stopped
|
|
98
|
+
* working.
|
|
99
|
+
*/
|
|
100
|
+
export async function confirmSpend(server, opts) {
|
|
101
|
+
if (!opts.always && opts.credits <= CONFIRM_ABOVE_CREDITS)
|
|
102
|
+
return { proceed: true };
|
|
103
|
+
if (!canAsk(server))
|
|
104
|
+
return { proceed: true };
|
|
105
|
+
const cost = opts.prompt?.cost ?? `This costs ${opts.credits} nooticr credits.`;
|
|
106
|
+
const lines = [`${opts.summary} ${cost}`];
|
|
107
|
+
if (opts.cheaper)
|
|
108
|
+
lines.push(opts.cheaper);
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
result = (await server.elicitInput({
|
|
112
|
+
mode: "form",
|
|
113
|
+
message: lines.join(" "),
|
|
114
|
+
requestedSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
proceed: {
|
|
118
|
+
type: "boolean",
|
|
119
|
+
title: opts.prompt?.title ?? `Spend ${opts.credits} credits`,
|
|
120
|
+
description: opts.prompt?.action ?? "Run the search now.",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
required: ["proceed"],
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
// "accept" with proceed:false is a user who read the form and said no; it
|
|
127
|
+
// is not the same as dismissing the dialog, but it is still a no.
|
|
128
|
+
const accepted = result.action === "accept" &&
|
|
129
|
+
result.content?.proceed !== false;
|
|
130
|
+
if (accepted)
|
|
131
|
+
return { proceed: true, approved: true };
|
|
132
|
+
return { proceed: false, declined: result.action === "decline" ? "decline" : "cancel" };
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return { proceed: true };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What the caller returns when the user said no. Not an error — a choice.
|
|
140
|
+
*
|
|
141
|
+
* `cheaper` is the way out. It defaults to the platforms argument because
|
|
142
|
+
* `search_mentions` was the first caller and that is its lever, but a tool
|
|
143
|
+
* that fans out over posts rather than networks has a different one, and
|
|
144
|
+
* telling a user to narrow a "platforms" argument the tool does not have is
|
|
145
|
+
* worse than saying nothing.
|
|
146
|
+
*/
|
|
147
|
+
export function declinedResult(credits, what, cheaper) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: `Cancelled — no credits were spent. ${what} would have cost ${credits} credits. ` +
|
|
153
|
+
(cheaper ?? `Narrow it with the "platforms" argument to spend less.`),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
structuredContent: { cancelled: true, wouldHaveCost: credits },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long calls, made honest.
|
|
3
|
+
*
|
|
4
|
+
* Every tool here declared `taskSupport: "forbidden"` — the SDK default, never
|
|
5
|
+
* chosen — including the ones that watch a video. POLL_TIMEOUT_MS is five
|
|
6
|
+
* minutes, so analyze_post could hold a synchronous tools/call open for that
|
|
7
|
+
* long and a host had no way to put it in the background. Measured against the
|
|
8
|
+
* live server, discover_social_posts takes 10.3s and compare_posts 10.4s; the
|
|
9
|
+
* multimodal tools wrapped here are the slow ones above them.
|
|
10
|
+
*
|
|
11
|
+
* The three tools that run longest now accept a task. What they do is
|
|
12
|
+
* unchanged: the existing handler is moved onto a background run and its
|
|
13
|
+
* result stored, so a client that asks for a task gets a task id it can poll,
|
|
14
|
+
* and a client that does not is auto-polled by the SDK and sees exactly the
|
|
15
|
+
* same result it saw before. That compatibility is the reason for `optional`
|
|
16
|
+
* rather than `required`.
|
|
17
|
+
*
|
|
18
|
+
* The store is in memory, per session. A task does not survive a restart —
|
|
19
|
+
* neither did the synchronous call it replaces, so nothing regresses, and the
|
|
20
|
+
* result outliving the request by fifteen minutes is new ground either way.
|
|
21
|
+
* When these need to survive a deploy, the store is the piece to replace: the
|
|
22
|
+
* worker already has a SQLite Durable Object under the session.
|
|
23
|
+
*
|
|
24
|
+
* The task APIs are experimental in SDK 1.30 and may change under us. They are
|
|
25
|
+
* contained here for that reason — one file to fix rather than three handlers.
|
|
26
|
+
*/
|
|
27
|
+
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
28
|
+
/** How long a finished result stays fetchable. */
|
|
29
|
+
export const TASK_TTL_MS = 900_000;
|
|
30
|
+
/**
|
|
31
|
+
* How often a client is told to poll — and, because the SDK auto-polls at this
|
|
32
|
+
* interval for a client that asked for no task, a floor on how fast those
|
|
33
|
+
* calls can now return. Measured: a plain call that would have answered
|
|
34
|
+
* instantly takes one interval. The SDK's default is 5s; these tools import
|
|
35
|
+
* media and run multimodal AI over it, so 2s is a rounding error against the
|
|
36
|
+
* work and cheap enough to poll over HTTP for the five minutes it may run.
|
|
37
|
+
* Only the three slowest tools are wrapped, so nothing quick pays this.
|
|
38
|
+
*/
|
|
39
|
+
export const TASK_POLL_MS = 2_000;
|
|
40
|
+
/**
|
|
41
|
+
* The default store: in memory, which is correct for stdio because the process
|
|
42
|
+
* *is* the session — when it exits there is nothing left to poll for.
|
|
43
|
+
*
|
|
44
|
+
* It is wrong for the Worker, where the Durable Object holding the server is
|
|
45
|
+
* restarted by eviction, hibernation and every deploy, taking every in-flight
|
|
46
|
+
* task with it. That transport passes its own durable store; see
|
|
47
|
+
* `cloudflare/src/taskStore.ts`.
|
|
48
|
+
*/
|
|
49
|
+
export function createTaskStore() {
|
|
50
|
+
return new InMemoryTaskStore();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Registers a tool whose work is slow enough to deserve a task, keeping the
|
|
54
|
+
* handler exactly as it was written for the synchronous path.
|
|
55
|
+
*/
|
|
56
|
+
export function registerSlowTool(server, name, config, run) {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
const tasks = server.experimental.tasks;
|
|
59
|
+
tasks.registerToolTask(name, { ...config, execution: { taskSupport: "optional" } }, {
|
|
60
|
+
createTask: async (args, extra) => {
|
|
61
|
+
const task = await extra.taskStore.createTask({ ttl: TASK_TTL_MS, pollInterval: TASK_POLL_MS });
|
|
62
|
+
// Deliberately not awaited: createTask must return the id now, and the
|
|
63
|
+
// work carries on past the end of this request. Nothing in these
|
|
64
|
+
// handlers reads extra.signal, and makeClient takes only plain values
|
|
65
|
+
// off extra (authInfo, requestId, arguments), so none of it goes stale
|
|
66
|
+
// when the request that started it completes.
|
|
67
|
+
void (async () => {
|
|
68
|
+
try {
|
|
69
|
+
const result = await run(args, extra);
|
|
70
|
+
await extra.taskStore.storeTaskResult(task.taskId, result?.isError ? "failed" : "completed", result);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
// A throw here has nowhere to go — the request is long gone — so
|
|
74
|
+
// it has to land in the task or the client polls a task that never
|
|
75
|
+
// finishes.
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
77
|
+
await extra.taskStore.storeTaskResult(task.taskId, "failed", {
|
|
78
|
+
content: [{ type: "text", text: `${name} failed: ${message}` }],
|
|
79
|
+
isError: true,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
return { task };
|
|
84
|
+
},
|
|
85
|
+
getTask: async (_args, extra) => extra.taskStore.getTask(extra.taskId),
|
|
86
|
+
getTaskResult: async (_args, extra) => extra.taskStore.getTaskResult(extra.taskId),
|
|
87
|
+
});
|
|
88
|
+
}
|