@nooticr/mcp 1.26.30 → 1.26.31
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/shared/evidence.js +53 -0
- package/dist/shared/jobs.js +44 -9
- package/dist/shared/tools-def.js +1 -1
- package/dist/shared/tools.js +1 -1
- package/package.json +1 -1
package/dist/shared/evidence.js
CHANGED
|
@@ -563,3 +563,56 @@ export function fetchBillingNote(tool) {
|
|
|
563
563
|
"not charge again: the idempotency key is namespaced per tool, so a retry replays the " +
|
|
564
564
|
"same debits.");
|
|
565
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* The searchable core of a complaint, with the speaker stripped out.
|
|
568
|
+
*
|
|
569
|
+
* `find_people_with_problem` widens a problem into the shapes a complaint
|
|
570
|
+
* takes. Those shapes were templated onto the raw argument, and the argument
|
|
571
|
+
* the schema asks for is a full sentence with a subject ("I waste an hour a
|
|
572
|
+
* day checking what competitors posted") — so "is there a way to " + that
|
|
573
|
+
* produced a string nobody has ever written, and sent it to a keyword search
|
|
574
|
+
* (#60).
|
|
575
|
+
*
|
|
576
|
+
* Two things happen here, and neither is intent matching — the backend is a
|
|
577
|
+
* keyword search and this does not pretend otherwise:
|
|
578
|
+
*
|
|
579
|
+
* - The first-person subject and its auxiliary come off the front of each
|
|
580
|
+
* clause. "I'm out of content ideas" is a sentence about a speaker;
|
|
581
|
+
* "out of content ideas" is what the post being looked for actually says.
|
|
582
|
+
* - The result is capped, because a long sentence matches on its commonest
|
|
583
|
+
* words. That is the tool's own advice to its caller, and it was not
|
|
584
|
+
* taking it.
|
|
585
|
+
*
|
|
586
|
+
* A caller who can do better should: `queries` overrides all of this.
|
|
587
|
+
*/
|
|
588
|
+
export function complaintCore(problem) {
|
|
589
|
+
const MAX_WORDS = 12;
|
|
590
|
+
// The pronoun always goes. The verb after it goes ONLY if it is an
|
|
591
|
+
// auxiliary — "I have no idea" is about a speaker, but "I waste an hour a
|
|
592
|
+
// day" carries "waste", and stripping it leaves "an hour a day checking",
|
|
593
|
+
// which is a worse search than the one being fixed. That distinction is the
|
|
594
|
+
// whole of this function; getting it wrong trades one bad query for another.
|
|
595
|
+
const SUBJECT = /^\s*(?:i|we)\b['’]?(?:m\b)?[\s,]*/i;
|
|
596
|
+
const AUXILIARY = /^\s*(?:am|are|is|was|were|have|has|had|do|does|did|don['’]?t|can['’]?t|cannot|been)\b[\s,]*/i;
|
|
597
|
+
// Never end on a word that was pointing at the one the cap removed.
|
|
598
|
+
const DANGLING = /(?:\s+(?:for|my|our|the|a|an|of|to|and|or|with|in|on|at|by|that|this))+$/i;
|
|
599
|
+
const cleaned = problem
|
|
600
|
+
.split(/[,;]+/)
|
|
601
|
+
.map((clause) => {
|
|
602
|
+
const withoutSubject = clause.trim().replace(SUBJECT, "");
|
|
603
|
+
// Only strip an auxiliary that actually followed a subject, so a clause
|
|
604
|
+
// opening with a real verb keeps it.
|
|
605
|
+
return (withoutSubject === clause.trim()
|
|
606
|
+
? withoutSubject
|
|
607
|
+
: withoutSubject.replace(AUXILIARY, "")).trim();
|
|
608
|
+
})
|
|
609
|
+
.filter(Boolean)
|
|
610
|
+
.join(", ");
|
|
611
|
+
const words = (cleaned || problem.trim()).split(/\s+/).filter(Boolean);
|
|
612
|
+
return words
|
|
613
|
+
.slice(0, MAX_WORDS)
|
|
614
|
+
.join(" ")
|
|
615
|
+
.replace(/[,;]+$/, "")
|
|
616
|
+
.replace(DANGLING, "")
|
|
617
|
+
.trim();
|
|
618
|
+
}
|
package/dist/shared/jobs.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
import { z } from "zod";
|
|
57
57
|
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
58
58
|
import { platformFromUrl, postSlug } from "./comment-review.js";
|
|
59
|
-
import { clamp, handleMissGuidance, ownIt, PLATFORM_ARG } from "./evidence.js";
|
|
59
|
+
import { clamp, complaintCore, handleMissGuidance, ownIt, PLATFORM_ARG } from "./evidence.js";
|
|
60
60
|
import { withEvidence } from "./evidence-digest.js";
|
|
61
61
|
import { confirmSpend, costOf, declinedResult, MAX_SPOKEN_HANDLE_CALLS, MAX_SPOKEN_TRANSCRIPTS, SPOKEN_PLATFORMS, CREDITS_PER_CREATOR, } from "./spend.js";
|
|
62
62
|
import { distributionOf, excluding, numberOf, standing, } from "./performance.js";
|
|
@@ -2112,6 +2112,14 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
2112
2112
|
.optional()
|
|
2113
2113
|
.describe("Where to look (default reddit). Each one is a separate paid search."),
|
|
2114
2114
|
limit: z.number().int().optional().describe("Posts per query shape (default 6, max 15)."),
|
|
2115
|
+
queries: z
|
|
2116
|
+
.array(z.string())
|
|
2117
|
+
.optional()
|
|
2118
|
+
.describe("Search these exact phrasings instead of the ones derived from `problem` (max 4). " +
|
|
2119
|
+
"You are better at this than a string template is: write how a person would " +
|
|
2120
|
+
"actually word the complaint on the network being searched — a Reddit title, " +
|
|
2121
|
+
"an X post — and keep each one short, because this is a keyword search and a " +
|
|
2122
|
+
"long sentence matches on its commonest words."),
|
|
2115
2123
|
})
|
|
2116
2124
|
.strict(),
|
|
2117
2125
|
}, async (args, extra) => {
|
|
@@ -2123,11 +2131,27 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
2123
2131
|
// The shapes a complaint takes, rather than the topic it is about. Kept
|
|
2124
2132
|
// to three: each is a paid call per platform, and past three they start
|
|
2125
2133
|
// returning the same posts as each other.
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2134
|
+
//
|
|
2135
|
+
// Built from a stripped core rather than the raw sentence, and the two
|
|
2136
|
+
// non-plain shapes no longer prefix a template onto whatever arrived.
|
|
2137
|
+
// The old `is there a way to ${problem}` assumed a bare verb phrase
|
|
2138
|
+
// while the schema's own example is a full sentence with a subject, so
|
|
2139
|
+
// the documented input produced "is there a way to I have no idea what
|
|
2140
|
+
// to post next, I'm out of content ideas for my page" — a string nobody
|
|
2141
|
+
// has ever written, sent to a keyword search, three times, having been
|
|
2142
|
+
// lengthened twice (#60).
|
|
2143
|
+
const core = complaintCore(problem);
|
|
2144
|
+
const shapes = args.queries?.length
|
|
2145
|
+
? args.queries.slice(0, 4).map((q, i) => ({ shape: `given:${i + 1}`, query: q.trim() }))
|
|
2146
|
+
: [
|
|
2147
|
+
{ shape: "plain", query: core },
|
|
2148
|
+
// Reddit's own idiom, and it reads as a sentence now that `core`
|
|
2149
|
+
// has had its leading subject removed.
|
|
2150
|
+
{ shape: "shared", query: `anyone else ${core}` },
|
|
2151
|
+
// A SUFFIX, not a prefix: appended keywords cannot be made
|
|
2152
|
+
// ungrammatical by whatever shape the core turned out to be.
|
|
2153
|
+
{ shape: "asking", query: `${core} any recommendations` },
|
|
2154
|
+
];
|
|
2131
2155
|
const seen = new Set();
|
|
2132
2156
|
const posts = [];
|
|
2133
2157
|
const unavailable = [];
|
|
@@ -2199,9 +2223,20 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
2199
2223
|
"and say how many you rejected and why — a shortlist with no reject count reads as if " +
|
|
2200
2224
|
"the search was precise.",
|
|
2201
2225
|
"",
|
|
2202
|
-
|
|
2203
|
-
"
|
|
2204
|
-
|
|
2226
|
+
args.queries?.length
|
|
2227
|
+
? "`foundBy` says which of your own phrasings surfaced each post, in the order you " +
|
|
2228
|
+
"gave them."
|
|
2229
|
+
: "`foundBy` says which phrasing surfaced each post: `plain` the problem itself, " +
|
|
2230
|
+
"`shared` the does-anyone-else form, `asking` the looking-for-a-tool form. The " +
|
|
2231
|
+
"last two skew towards genuine complaints and the first towards content about " +
|
|
2232
|
+
"the topic.",
|
|
2233
|
+
"",
|
|
2234
|
+
args.queries?.length
|
|
2235
|
+
? ""
|
|
2236
|
+
: `These were searched as: ${shapes.map((sh) => `"${sh.query}"`).join(", ")}. That ` +
|
|
2237
|
+
"is a keyword search, not intent matching. If those read wrong for where you are " +
|
|
2238
|
+
"looking — a Reddit title is worded differently from an X post — call this again " +
|
|
2239
|
+
"with `queries` set to phrasings you write yourself, and keep each one short.",
|
|
2205
2240
|
"",
|
|
2206
2241
|
unavailable.length
|
|
2207
2242
|
? `Note that ${unavailable.length} searches errored — tell the user which networks ` +
|
package/dist/shared/tools-def.js
CHANGED
|
@@ -394,7 +394,7 @@ export const TOOL_DEFINITIONS = [
|
|
|
394
394
|
name: "find_people_with_problem",
|
|
395
395
|
title: "Find People With A Problem",
|
|
396
396
|
description: "Find people describing a problem in their own words — prospects, early users, anyone whose complaint your product answers. Say the problem the way a person would say it (\"I spend hours checking competitors by hand\") and this searches for posts shaped like that complaint, not for the topic it is about. That difference is the point: searching the TOPIC finds marketing about it, which is what discover_social_posts is for. Widens the query into the forms a complaint takes — the plain phrasing, the does-anyone-else question, the is-there-a-tool ask — and merges what each returns. It is a wide cheap net and NOT a filter: the posts come back for you to judge, and many will be off-target. Consumes 2 nooticr credits per platform searched. Defaults to reddit, where people describe workflow pain in sentences; add twitter for volume.",
|
|
397
|
-
inputSchema: z.object({ problem: z.string().describe("The problem in a person's own words, not a topic. \"I waste an hour a day checking what competitors posted\" finds people; \"competitor analysis\" finds marketing about competitor analysis."), platforms: z.array(z.enum(["reddit", "twitter", "youtube", "tiktok", "instagram", "linkedin"])).optional().describe("Where to look (default reddit). Each one is a separate paid search."), limit: z.number().int().optional().describe("Posts per query shape (default 6, max 15).") }).strict(),
|
|
397
|
+
inputSchema: z.object({ problem: z.string().describe("The problem in a person's own words, not a topic. \"I waste an hour a day checking what competitors posted\" finds people; \"competitor analysis\" finds marketing about competitor analysis."), platforms: z.array(z.enum(["reddit", "twitter", "youtube", "tiktok", "instagram", "linkedin"])).optional().describe("Where to look (default reddit). Each one is a separate paid search."), limit: z.number().int().optional().describe("Posts per query shape (default 6, max 15)."), queries: z.array(z.string()).optional().describe("Search these exact phrasings instead of the ones derived from `problem` (max 4). You are better at this than a string template is: write how a person would actually word the complaint on the network being searched \u2014 a Reddit title, an X post \u2014 and keep each one short, because this is a keyword search and a long sentence matches on its commonest words.") }).strict(),
|
|
398
398
|
},
|
|
399
399
|
{
|
|
400
400
|
name: "search_spoken_mentions",
|
package/dist/shared/tools.js
CHANGED
|
@@ -26,7 +26,7 @@ import { loadingPlansJson } from "./loading-plans.js";
|
|
|
26
26
|
import { registerHandoff } from "./handoff.js";
|
|
27
27
|
import { registerCollabTools } from "./collab.js";
|
|
28
28
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
29
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
29
|
+
export const MCP_SERVER_VERSION = "1.26.31";
|
|
30
30
|
/** MCP Apps extension identifier */
|
|
31
31
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
32
32
|
/** MIME type for MCP Apps HTML resources */
|
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.31",
|
|
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": {
|