@nooticr/mcp 1.26.75 → 1.26.77
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/dist/auth.js +25 -7
- package/dist/index.js +8 -13
- package/dist/shared/tools.js +17 -96
- package/dist/shared/ui-template.js +5 -2
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -225,17 +225,35 @@ export function createManagementTokenProvider(auth) {
|
|
|
225
225
|
/**
|
|
226
226
|
* Builds a TokenProvider for one caller's own API key — the headless path
|
|
227
227
|
* through HTTP mode, where the bearer the client presented *is* the nooticr
|
|
228
|
-
* credential.
|
|
229
|
-
*
|
|
230
|
-
* tenant's call with another's account.
|
|
228
|
+
* credential. Never the operator's environment or credentials file, which would
|
|
229
|
+
* answer one tenant's call with another's account.
|
|
231
230
|
*/
|
|
232
231
|
export function createApiKeyTokenProvider(apiKey) {
|
|
233
232
|
return { getAccessToken: async () => apiKey };
|
|
234
233
|
}
|
|
235
|
-
/**
|
|
236
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Builds a TokenProvider for one OAuth session in HTTP mode: that session's
|
|
236
|
+
* nooticr tokens and nothing else. The environment and the credentials file
|
|
237
|
+
* belong to whoever runs the server; resolving them here answered every
|
|
238
|
+
* signed-in user as the operator. A refresh updates the session in place, so
|
|
239
|
+
* the next request from the same login carries the new token, and nothing is
|
|
240
|
+
* written to the operator's credentials file.
|
|
241
|
+
*/
|
|
242
|
+
export function createSessionTokenProvider(baseUrl, session) {
|
|
237
243
|
return {
|
|
238
|
-
getAccessToken: async () =>
|
|
239
|
-
onUnauthorized: async () =>
|
|
244
|
+
getAccessToken: async () => session.nooticrAccessToken || undefined,
|
|
245
|
+
onUnauthorized: async () => {
|
|
246
|
+
if (!session.nooticrRefreshToken)
|
|
247
|
+
return false;
|
|
248
|
+
try {
|
|
249
|
+
const next = await NooticrClient.refreshSession(baseUrl, session.nooticrRefreshToken);
|
|
250
|
+
session.nooticrAccessToken = next.accessToken;
|
|
251
|
+
session.nooticrRefreshToken = next.refreshToken ?? session.nooticrRefreshToken;
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
},
|
|
240
258
|
};
|
|
241
259
|
}
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { randomUUID } from "node:crypto";
|
|
|
14
14
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
15
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
16
16
|
import { getBaseUrl, getPublicUrl, getPort, getCredentialsFile, getTransportMode, DEFAULT_PORT, } from "./config.js";
|
|
17
|
-
import { AuthManager, NooticrAuthError, createApiKeyTokenProvider,
|
|
17
|
+
import { AuthManager, NooticrAuthError, createApiKeyTokenProvider, createSessionTokenProvider, createManagementTokenProvider, createStdioTokenProvider, } from "./auth.js";
|
|
18
18
|
import { forwardedHeaders, forwardedSession, looksLikeApiKey } from "./shared/api-key.js";
|
|
19
19
|
import { NooticrClient, NooticrError } from "./nooticr.js";
|
|
20
20
|
import { OAuthManager, SCOPES } from "./oauth.js";
|
|
@@ -227,9 +227,6 @@ export async function runHttp(port, publicUrl) {
|
|
|
227
227
|
client: new NooticrClient(baseUrl, {
|
|
228
228
|
getAccessToken: async () => undefined,
|
|
229
229
|
}),
|
|
230
|
-
onSession: async (session) => {
|
|
231
|
-
await auth.persistSession(session);
|
|
232
|
-
},
|
|
233
230
|
});
|
|
234
231
|
const state = {
|
|
235
232
|
oauth,
|
|
@@ -239,9 +236,9 @@ export async function runHttp(port, publicUrl) {
|
|
|
239
236
|
serverFactory: () => createMcpServer((extra) => {
|
|
240
237
|
const bearer = extra.authInfo?.token;
|
|
241
238
|
// A caller who presented their own API key is answered as themselves.
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
239
|
+
// No branch below falls back to the *operator's* environment or
|
|
240
|
+
// credentials file: that would serve one tenant's request from
|
|
241
|
+
// another's account.
|
|
245
242
|
if (looksLikeApiKey(bearer)) {
|
|
246
243
|
return new NooticrClient(baseUrl, createApiKeyTokenProvider(bearer));
|
|
247
244
|
}
|
|
@@ -250,15 +247,13 @@ export async function runHttp(port, publicUrl) {
|
|
|
250
247
|
if (forwarded) {
|
|
251
248
|
return new NooticrClient(baseUrl, createApiKeyTokenProvider(forwarded.token), undefined, forwardedHeaders(forwarded));
|
|
252
249
|
}
|
|
250
|
+
// An OAuth login of this server: only that session's own tokens.
|
|
253
251
|
const session = bearer
|
|
254
252
|
? oauth.verifyToken(bearer)
|
|
255
253
|
: undefined;
|
|
256
|
-
return new NooticrClient(baseUrl,
|
|
257
|
-
?
|
|
258
|
-
|
|
259
|
-
refreshToken: session.nooticrRefreshToken,
|
|
260
|
-
}
|
|
261
|
-
: undefined));
|
|
254
|
+
return new NooticrClient(baseUrl, session
|
|
255
|
+
? createSessionTokenProvider(baseUrl, session)
|
|
256
|
+
: { getAccessToken: async () => undefined });
|
|
262
257
|
}),
|
|
263
258
|
};
|
|
264
259
|
const server = http.createServer((req, res) => {
|
package/dist/shared/tools.js
CHANGED
|
@@ -36,7 +36,7 @@ import { registerAmazonTools } from "./amazon.js";
|
|
|
36
36
|
import { registerMarketplaceTools } from "./marketplace.js";
|
|
37
37
|
import { DEFAULT_NOOTICR_SETTINGS, DEFAULT_SOCIAL_PLATFORMS, MemorySettingsStore, SETTINGS_READ_TOOL, SETTINGS_UPDATE_TOOL, settingsOwnerFromAuth, } from "./settings.js";
|
|
38
38
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
39
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
39
|
+
export const MCP_SERVER_VERSION = "1.26.77";
|
|
40
40
|
/** MCP Apps extension identifier */
|
|
41
41
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
42
42
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -179,91 +179,21 @@ export function argumentsDigest(value) {
|
|
|
179
179
|
return hash.toString(16);
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
182
|
-
*
|
|
182
|
+
* Why nothing here rewrites a URL (#242).
|
|
183
183
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* - `embedUrl` is an <iframe> src on the platform's own player origin.
|
|
190
|
-
* - the `*FallbackUrl` fields are already our own signed `/media/resolve`
|
|
191
|
-
* capability links; wrapping one in `/media/proxy` proxies our own
|
|
192
|
-
* resolver and invalidates nothing but the caller's playback.
|
|
193
|
-
*/
|
|
194
|
-
const RAW_URL_KEYS = new Set([
|
|
195
|
-
"externalUrl",
|
|
196
|
-
"embedUrl",
|
|
197
|
-
"videoFallbackUrl",
|
|
198
|
-
"thumbnailFallbackUrl",
|
|
199
|
-
"musicFallbackUrl",
|
|
200
|
-
]);
|
|
201
|
-
/**
|
|
202
|
-
* Whether a URL already points at one of our own media endpoints.
|
|
184
|
+
* This worker used to wrap every external URL in a result as
|
|
185
|
+
* `${server}/media/proxy?url=...`, so a card's images would load inside
|
|
186
|
+
* ChatGPT's and Claude's sandboxed frames. Those links were unsigned, and
|
|
187
|
+
* `/media/proxy` fetches from the internet for whoever calls it, so the server
|
|
188
|
+
* could not start refusing unsigned links without blanking every card.
|
|
203
189
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// way. It was missing here, so whether it got wrapped a second time depended
|
|
211
|
-
// on the host string matching NOOTICR_BASE_URL exactly: "localhost:8080" and
|
|
212
|
-
// "127.0.0.1:8080" are the same server and different strings, and the proxy
|
|
213
|
-
// then refused its own URL as an SSRF attempt. Recognising the path shape is
|
|
214
|
-
// sturdier than trusting two spellings of a host to agree.
|
|
215
|
-
return (url.includes("/media/proxy") ||
|
|
216
|
-
url.includes("/media/resolve") ||
|
|
217
|
-
url.includes("/media/files"));
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Rewrite external image URLs to go through the nooticr proxy so they
|
|
221
|
-
* work inside ChatGPT's sandboxed iframe (CORS + CSP restrictions).
|
|
190
|
+
* nooticr-server#893 moved the rewrite to where the signing key is: the HTTP
|
|
191
|
+
* `tools/call` route now hands back every external URL as a signed proxy link
|
|
192
|
+
* (`&sig=`), with this worker's old rule ported word for word (permalinks,
|
|
193
|
+
* embeds and our own resolve/files/copy links stay as they are). So a result
|
|
194
|
+
* reaches the host exactly as the server wrote it. Minting a link here again,
|
|
195
|
+
* for any field, is the bug this note exists to prevent: it would be unsigned.
|
|
222
196
|
*/
|
|
223
|
-
function proxyImageUrl(url) {
|
|
224
|
-
if (!url || url.startsWith("data:"))
|
|
225
|
-
return url;
|
|
226
|
-
try {
|
|
227
|
-
const u = new URL(url);
|
|
228
|
-
// Only proxy external HTTP(S) URLs — skip our own proxy and data URIs
|
|
229
|
-
if (u.protocol === "http:" || u.protocol === "https:") {
|
|
230
|
-
const serverUrl = process.env.NOOTICR_API_URL || process.env.NOOTICR_BASE_URL || "";
|
|
231
|
-
if (serverUrl && !url.startsWith(serverUrl) && !isOwnMediaUrl(url)) {
|
|
232
|
-
return `${serverUrl.replace(/\/+$/, "")}/media/proxy?url=${encodeURIComponent(url)}`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
catch { }
|
|
237
|
-
return url;
|
|
238
|
-
}
|
|
239
|
-
/** Recursively rewrite URL fields in a structured object for the proxy. */
|
|
240
|
-
export function proxyUrls(obj) {
|
|
241
|
-
if (typeof obj === "string") {
|
|
242
|
-
// Check if it looks like a URL
|
|
243
|
-
if (/^https?:\/\//.test(obj) && !isOwnMediaUrl(obj)) {
|
|
244
|
-
return proxyImageUrl(obj);
|
|
245
|
-
}
|
|
246
|
-
return obj;
|
|
247
|
-
}
|
|
248
|
-
if (Array.isArray(obj))
|
|
249
|
-
return obj.map(proxyUrls);
|
|
250
|
-
if (obj && typeof obj === "object") {
|
|
251
|
-
const out = {};
|
|
252
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
253
|
-
if (RAW_URL_KEYS.has(k)) {
|
|
254
|
-
out[k] = v;
|
|
255
|
-
}
|
|
256
|
-
else if (["thumbnailUrl", "preview_url", "coverUrl", "avatarUrl", "avatar_thumb", "image_url", "videoUrl", "video_url", "url"].includes(k) && typeof v === "string") {
|
|
257
|
-
out[k] = proxyImageUrl(v);
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
out[k] = proxyUrls(v);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return out;
|
|
264
|
-
}
|
|
265
|
-
return obj;
|
|
266
|
-
}
|
|
267
197
|
const structuredOf = (res) => (res.structured ?? {});
|
|
268
198
|
/** A structured result this server computed rather than proxied. */
|
|
269
199
|
function toStructured(value) {
|
|
@@ -312,9 +242,8 @@ async function toToolResult(proxy, guidance) {
|
|
|
312
242
|
const htmlPrefix = rawText.startsWith("<")
|
|
313
243
|
? rawText.substring(0, rawText.indexOf("\n\n{")).trimEnd()
|
|
314
244
|
: "";
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const proxied = structured ? proxyUrls(structured) : {};
|
|
245
|
+
// URLs arrive already signed by the server; see the note above structuredOf.
|
|
246
|
+
const structured = proxy.structured ?? {};
|
|
318
247
|
// `_htmlCards` is the rendered card HTML, and the text block already carries
|
|
319
248
|
// it as htmlPrefix below. Leaving it in the structured payload sent the same
|
|
320
249
|
// HTML a second time to the model and a third time into the widget: measured
|
|
@@ -322,23 +251,15 @@ async function toToolResult(proxy, guidance) {
|
|
|
322
251
|
// template reads (zero references in either embedded copy - views render from
|
|
323
252
|
// `posts`). Dropped from both the JSON the model sees and the structured
|
|
324
253
|
// content the widget receives; the prefix keeps the one copy that is used.
|
|
325
|
-
const { _htmlCards: _renderedSeparately, ...forHosts } =
|
|
254
|
+
const { _htmlCards: _renderedSeparately, ...forHosts } = structured;
|
|
326
255
|
const textJson = JSON.stringify(forHosts, null, 2);
|
|
327
|
-
|
|
328
|
-
const proxiedHtml = htmlPrefix ? proxyImageUrlsInHtml(htmlPrefix) : "";
|
|
329
|
-
const body = proxiedHtml ? `${proxiedHtml}\n\n${textJson}` : textJson;
|
|
256
|
+
const body = htmlPrefix ? `${htmlPrefix}\n\n${textJson}` : textJson;
|
|
330
257
|
const text = guidance ? `${guidance}\n\n${body}` : body;
|
|
331
258
|
return {
|
|
332
259
|
content: [{ type: "text", text }],
|
|
333
260
|
structuredContent: guidance ? { ...forHosts, guidance } : forHosts,
|
|
334
261
|
};
|
|
335
262
|
}
|
|
336
|
-
/** Replace external image/video src, poster and href URLs in HTML with proxied versions. */
|
|
337
|
-
function proxyImageUrlsInHtml(html) {
|
|
338
|
-
return html.replace(/(src|href|poster)="(https?:\/\/[^"']+?)"/g, (_match, attr, url) => {
|
|
339
|
-
return `${attr}="${proxyImageUrl(url)}"`;
|
|
340
|
-
});
|
|
341
|
-
}
|
|
342
263
|
/**
|
|
343
264
|
* An expired session, however the API happens to phrase it.
|
|
344
265
|
*
|
|
@@ -4213,8 +4213,11 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4213
4213
|
+(coverSrc?'<img class="sound-cover" data-avatar src="'+esc(coverSrc)+'" alt="" loading="lazy"/>':"")+"</span>";
|
|
4214
4214
|
// The raw CDN URL is signed and cross-origin — only the proxied one
|
|
4215
4215
|
// actually plays in this sandboxed frame. A bare link never played
|
|
4216
|
-
// anything in-place, which is why sound cards were silent.
|
|
4217
|
-
|
|
4216
|
+
// anything in-place, which is why sound cards were silent. So the
|
|
4217
|
+
// server's signed playProxyUrl comes first: playUrl is not a media key
|
|
4218
|
+
// the server proxies (nooticr-server#912), and this <audio> has no
|
|
4219
|
+
// retry to fall back on (#256).
|
|
4220
|
+
var play=s.playProxyUrl||s.playUrl||s.url||"";
|
|
4218
4221
|
var secs=Number(s.duration)||0;
|
|
4219
4222
|
var meta=[s.artist||s.author||"",secs>0?fmtTime(secs):""].filter(Boolean).join(" · ");
|
|
4220
4223
|
var player=play
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nooticr/mcp",
|
|
3
3
|
"mcpName": "com.nooticr/mcp",
|
|
4
|
-
"version": "1.26.
|
|
4
|
+
"version": "1.26.77",
|
|
5
5
|
"description": "MCP server for nooticr - fetch, discover and understand TikTok/Instagram/YouTube/X/Reddit/LinkedIn/Weibo posts with AI (media metadata, niche discovery, brand monitoring, hook & viral analysis)",
|
|
6
6
|
"homepage": "https://mcp.nooticr.com",
|
|
7
7
|
"repository": {
|