@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,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* URL validation + the analyze_post workflow: start the job, then poll until
|
|
3
|
+
* the analysis is done (or fails).
|
|
4
|
+
*/
|
|
5
|
+
export const POLL_INTERVAL_MS = 2000;
|
|
6
|
+
export const POLL_TIMEOUT_MS = 300_000;
|
|
7
|
+
const SUPPORTED_HOSTS_VIDEO = new Set([
|
|
8
|
+
"tiktok.com",
|
|
9
|
+
"vm.tiktok.com",
|
|
10
|
+
"m.tiktok.com",
|
|
11
|
+
"instagram.com",
|
|
12
|
+
"instagr.am",
|
|
13
|
+
"youtube.com",
|
|
14
|
+
"youtu.be",
|
|
15
|
+
"m.youtube.com",
|
|
16
|
+
"youtube-nocookie.com",
|
|
17
|
+
"douyin.com",
|
|
18
|
+
"xiaohongshu.com",
|
|
19
|
+
"xhslink.com",
|
|
20
|
+
"bilibili.com",
|
|
21
|
+
"m.bilibili.com",
|
|
22
|
+
"b23.tv",
|
|
23
|
+
]);
|
|
24
|
+
const SUPPORTED_HOSTS_POST = new Set([
|
|
25
|
+
...SUPPORTED_HOSTS_VIDEO,
|
|
26
|
+
"x.com",
|
|
27
|
+
"twitter.com",
|
|
28
|
+
"mobile.twitter.com",
|
|
29
|
+
]);
|
|
30
|
+
function validateUrl(rawUrl, allowed) {
|
|
31
|
+
if (typeof rawUrl !== "string" || rawUrl.trim() === "") {
|
|
32
|
+
return { ok: false, error: "url must be a non-empty string." };
|
|
33
|
+
}
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
parsed = new URL(rawUrl.trim());
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return { ok: false, error: "url is not a valid URL." };
|
|
40
|
+
}
|
|
41
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
42
|
+
return { ok: false, error: "url must use http or https." };
|
|
43
|
+
}
|
|
44
|
+
let host = parsed.hostname.toLowerCase();
|
|
45
|
+
if (host.startsWith("www."))
|
|
46
|
+
host = host.slice(4);
|
|
47
|
+
if (host === "youtube.com" || host === "youtu.be") {
|
|
48
|
+
// accept all youtube.com paths, incl. /shorts/<id>
|
|
49
|
+
return { ok: true, url: parsed.toString() };
|
|
50
|
+
}
|
|
51
|
+
if (allowed.has(host)) {
|
|
52
|
+
return { ok: true, url: parsed.toString() };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
ok: false,
|
|
56
|
+
error: "url host is not supported. Supported: tiktok.com, instagram.com, youtube.com (and /shorts), x.com, twitter.com, douyin.com, xiaohongshu.com, xhslink.com, bilibili.com, b23.tv.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function validateVideoUrl(rawUrl) {
|
|
60
|
+
return validateUrl(rawUrl, SUPPORTED_HOSTS_VIDEO);
|
|
61
|
+
}
|
|
62
|
+
export function validatePostUrl(rawUrl) {
|
|
63
|
+
return validateUrl(rawUrl, SUPPORTED_HOSTS_POST);
|
|
64
|
+
}
|
|
65
|
+
export class JobTimeoutError extends Error {
|
|
66
|
+
constructor(jobId, elapsedMs, lastStatus) {
|
|
67
|
+
super(`Timed out after ${Math.round(elapsedMs / 1000)}s waiting for analysis job ${jobId} to finish.` +
|
|
68
|
+
(lastStatus?.contentPreview ? ` Partial content: ${lastStatus.contentPreview}` : ""));
|
|
69
|
+
this.name = "JobTimeoutError";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Polls a job until state is "done" or "error".
|
|
74
|
+
*/
|
|
75
|
+
export async function pollUntilDone(client, jobId, opts = {}) {
|
|
76
|
+
const pollIntervalMs = opts.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
77
|
+
const timeoutMs = opts.timeoutMs ?? POLL_TIMEOUT_MS;
|
|
78
|
+
const startedAt = Date.now();
|
|
79
|
+
let last;
|
|
80
|
+
for (;;) {
|
|
81
|
+
const elapsedMs = Date.now() - startedAt;
|
|
82
|
+
if (elapsedMs >= timeoutMs) {
|
|
83
|
+
throw new JobTimeoutError(jobId, elapsedMs, last);
|
|
84
|
+
}
|
|
85
|
+
const status = await client.getJob(jobId);
|
|
86
|
+
last = status;
|
|
87
|
+
opts.onPoll?.(status);
|
|
88
|
+
if (status.state === "done")
|
|
89
|
+
return status;
|
|
90
|
+
if (status.state === "error")
|
|
91
|
+
return status;
|
|
92
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Full tool workflow: start the analysis, poll until done, return a
|
|
97
|
+
* JSON-serializable result. Throws NooticrError on API-level failures
|
|
98
|
+
* (e.g. 402 paywall).
|
|
99
|
+
*/
|
|
100
|
+
export async function runVideoAnalysis(client, url, opts = {}) {
|
|
101
|
+
const job = await client.startVideoAnalysis(url, opts.appId);
|
|
102
|
+
const status = await pollUntilDone(client, job.jobId, opts);
|
|
103
|
+
const result = {
|
|
104
|
+
ok: status.state === "done",
|
|
105
|
+
jobId: status.jobId,
|
|
106
|
+
state: status.state,
|
|
107
|
+
platform: job.platform,
|
|
108
|
+
provider: status.provider ?? job.provider,
|
|
109
|
+
analysis: status.analysis,
|
|
110
|
+
contentPreview: status.contentPreview,
|
|
111
|
+
error: status.error,
|
|
112
|
+
elapsedMs: status.elapsedMs,
|
|
113
|
+
// Mirror the Rust `/mcp` understand_social_post shape: expose the
|
|
114
|
+
// imported post + inline thumbnails at the top level so the analysis
|
|
115
|
+
// tool renders the thumbnail and the full post, not just the analysis.
|
|
116
|
+
post: job.post,
|
|
117
|
+
inlineImages: job.inlineImages,
|
|
118
|
+
};
|
|
119
|
+
if (result.ok) {
|
|
120
|
+
// Internal accounting stays internal. appId and workspaceId mean nothing
|
|
121
|
+
// to a caller, and `cost` is the workspace-credit price of the analysis
|
|
122
|
+
// job - a different currency from the MCP credits the tool is billed in.
|
|
123
|
+
// Surfacing it made the tool look like it charged 10 when it charges 6.
|
|
124
|
+
result.job = {
|
|
125
|
+
ok: job.ok,
|
|
126
|
+
jobId: job.jobId,
|
|
127
|
+
state: job.state,
|
|
128
|
+
platform: job.platform,
|
|
129
|
+
provider: job.provider,
|
|
130
|
+
post: job.post,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
export function formatPaywallError(err) {
|
|
136
|
+
const p = err.paywall;
|
|
137
|
+
const parts = [];
|
|
138
|
+
if (p?.reason)
|
|
139
|
+
parts.push(`reason: ${p.reason}`);
|
|
140
|
+
if (p?.used !== undefined && p?.max !== undefined) {
|
|
141
|
+
parts.push(`credits used: ${p.used}/${p.max}`);
|
|
142
|
+
}
|
|
143
|
+
else if (p?.used !== undefined) {
|
|
144
|
+
parts.push(`credits used: ${p.used}`);
|
|
145
|
+
}
|
|
146
|
+
if (p?.cost !== undefined)
|
|
147
|
+
parts.push(`cost: ${p.cost}`);
|
|
148
|
+
const detail = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
149
|
+
return (`Your nooticr account has no credits left for this analysis${detail}. ` +
|
|
150
|
+
`Top up or check your usage in the nooticr dashboard, then try again. ` +
|
|
151
|
+
`Note: the first analysis is covered by the free grant.`);
|
|
152
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `_meta` block that gives a tool its card, in one place.
|
|
3
|
+
*
|
|
4
|
+
* Every registrar module outside `tools.ts` needs these three keys and none of
|
|
5
|
+
* them can import `tools.ts` to get them: `tools.ts` imports the registrars, so
|
|
6
|
+
* reaching back for its `uiResource` closes a cycle. The answer up to now was a
|
|
7
|
+
* local copy per module with a comment explaining why — which worked, and then
|
|
8
|
+
* there were three of them, each a place the URI could drift from the resource
|
|
9
|
+
* `tools.ts` actually registers under.
|
|
10
|
+
*
|
|
11
|
+
* Both halves of that contract are checked in CI's `Host contract` step: every
|
|
12
|
+
* tool not in `NO_APP` must declare `ui/resourceUri` at the MCP Apps mime type
|
|
13
|
+
* with a `.html` sibling for ChatGPT. A URI that drifts from the registered
|
|
14
|
+
* resource fails there rather than at runtime, but only after a push — so the
|
|
15
|
+
* cheaper fix is for there to be one string.
|
|
16
|
+
*/
|
|
17
|
+
/** The app resource a tool's card is served from. */
|
|
18
|
+
export function uiResourceUri(tool) {
|
|
19
|
+
return `ui://nooticr/${tool}`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A view for a tool, at the URIs both hosts read.
|
|
23
|
+
*
|
|
24
|
+
* `ui/resourceUri` is the MCP Apps spec key that Claude reads; the
|
|
25
|
+
* `.html`-suffixed `openai/outputTemplate` is ChatGPT's, which does not speak
|
|
26
|
+
* that spec. The duplicated `ui.resourceUri` is the pre-standard nesting some
|
|
27
|
+
* hosts still look for. See `CLAUDE.md`'s dual-mime section for why serving
|
|
28
|
+
* both is deliberate rather than a bug.
|
|
29
|
+
*/
|
|
30
|
+
export const viewMeta = (tool) => ({
|
|
31
|
+
ui: { resourceUri: uiResourceUri(tool) },
|
|
32
|
+
"ui/resourceUri": uiResourceUri(tool),
|
|
33
|
+
"openai/outputTemplate": `${uiResourceUri(tool)}.html`,
|
|
34
|
+
});
|