@recomention/mcp 0.0.0-stage → 0.1.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Astrael OÜ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,153 @@
1
- # Temporary Holding Version
1
+ # @recomention/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ An MCP server for [recomention](https://www.recomention.com): ask Claude Code, Cursor or Codex
4
+ *"is my product recommended by ChatGPT for meeting scheduling?"* and have it start a report, wait
5
+ for it, and read the result — every engine's "named in n of m answers", who is recommended instead,
6
+ the sources the engines read, and the fix list.
7
+
8
+ It is a thin stdio client for the public JSON API v1 at `https://api.recomention.com/v1`. It holds no state.
9
+ It runs locally, as a process your MCP client starts. For ChatGPT, Claude.ai and other clients that
10
+ connect to a remote server, use the hosted connector at `https://mcp.recomention.com/mcp` instead: it
11
+ signs in with your recomention account (OAuth), no API key needed. Setup for each client:
12
+ <https://docs.recomention.com/mcp/>.
13
+
14
+ Requires Node.js 20 or newer (`npx` comes with it).
15
+
16
+ ## Environment
17
+
18
+ | Variable | | |
19
+ | --- | --- | --- |
20
+ | `RECOMENTION_API_KEY` | required for reports | Create one at `/account` → Developers. Shown once. Without it, public rankings and suggested questions still work. |
21
+ | `RECOMENTION_BASE_URL` | optional | Defaults to `https://api.recomention.com`. An older `https://www.recomention.com` (or the apex, a web preview, or a local Next dev server on port 3000–3099) still works: it reaches the site's deprecated `/api/v1` alias. Any other bare origin, such as the API preview Worker or a local API Worker (`http://localhost:8790`), uses `/v1`. |
22
+ | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `PERPLEXITY_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `DATAFORSEO_API_KEY` (Google AI Overview, `login:password`) | optional | Only for `funding: "byok"` (Bring Your Own Key), which needs an active Agent plan on the account. Any subset works and engines without a key are reported as skipped, but one of OpenAI, Anthropic or Gemini is required. Read from this server's environment, never from a tool argument, so your keys never pass through the model's context. |
23
+
24
+ ## Install
25
+
26
+ ### Claude Code
27
+
28
+ ```bash
29
+ claude mcp add recomention \
30
+ --env RECOMENTION_API_KEY=YOUR_API_KEY \
31
+ --env RECOMENTION_BASE_URL=https://api.recomention.com \
32
+ -- npx -y @recomention/mcp
33
+ ```
34
+
35
+ Replace `YOUR_API_KEY` with your key. Add `--scope user` to use it in every project. Check it with `claude mcp list`, or `/mcp` inside a session.
36
+
37
+ ### Cursor (`~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project) and other JSON-configured clients
38
+
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "recomention": {
43
+ "command": "npx",
44
+ "args": ["-y", "@recomention/mcp"],
45
+ "env": { "RECOMENTION_API_KEY": "<key>", "RECOMENTION_BASE_URL": "https://api.recomention.com" }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ Keep a project-level `.cursor/mcp.json` that holds a key out of version control.
52
+
53
+ ### Codex
54
+
55
+ ```bash
56
+ codex mcp add recomention \
57
+ --env RECOMENTION_API_KEY=YOUR_API_KEY \
58
+ --env RECOMENTION_BASE_URL=https://api.recomention.com \
59
+ -- npx -y @recomention/mcp
60
+ ```
61
+
62
+ or in `~/.codex/config.toml`:
63
+
64
+ ```toml
65
+ [mcp_servers.recomention]
66
+ command = "npx"
67
+ args = ["-y", "@recomention/mcp"]
68
+ env = { RECOMENTION_API_KEY = "<key>", RECOMENTION_BASE_URL = "https://api.recomention.com" }
69
+ ```
70
+
71
+ ## Tools
72
+
73
+ | Tool | Cost | |
74
+ | --- | --- | --- |
75
+ | `recomention_rankings_list` | free | Public AI rankings. No key needed. |
76
+ | `recomention_rankings_get(slug)` | free | One ranking with every answer it counted. |
77
+ | `recomention_suggest_prompts({domain, category, description})` | free | Buyer questions from fixed templates. No model call. Never names the product. |
78
+ | `recomention_report_start({domain, description, category, plan, prompts?, engines?, funding})` | **spends** | `funding: "credits"` uses one agency credit (plan `FULL`); `funding: "byok"` runs on your provider keys and needs an active Agent plan; a refusal names the plan limit it hit. |
79
+ | `recomention_report_status(slug)` | free | PENDING / RUNNING / COMPLETE / FAILED, with progress. |
80
+ | `recomention_report_get(slug)` | free | The full report JSON. |
81
+ | `recomention_distribution_kit(slug)` | free | Markdown files built from a finished report: product, competitors, AI visibility, where to get mentioned, channel plan. |
82
+ | `recomention_reports_list` | free | Your reports, newest first. |
83
+
84
+ Reading results honestly: an engine in `enginesSkipped` did not run (for example *"provider rejected
85
+ the key"*) — it is never an engine that left the product out. A report with `simulated: true` is test
86
+ data.
87
+
88
+ ## Setup prompt
89
+
90
+ The account page's **Copy setup prompt** button copies this (with the site's own URL). Paste it into
91
+ your agent:
92
+
93
+ ```text
94
+ Set up the recomention MCP server so you can check whether AI engines (ChatGPT, Claude, Perplexity, Gemini, Grok) recommend a product.
95
+
96
+ 1. Add this MCP server to your MCP configuration (Claude Code: `claude mcp add`; Cursor: ~/.cursor/mcp.json; Codex: ~/.codex/config.toml):
97
+ command: npx -y @recomention/mcp
98
+ env: RECOMENTION_API_KEY=<ask me for my API key; never write it into a repository>
99
+ RECOMENTION_BASE_URL=https://api.recomention.com
100
+ Claude Code one-liner: claude mcp add recomention --env RECOMENTION_API_KEY=<key> --env RECOMENTION_BASE_URL=https://api.recomention.com -- npx -y @recomention/mcp
101
+ 2. Restart or reload MCP servers, then call recomention_rankings_list to confirm it works.
102
+
103
+ How to use it:
104
+ - To check a product, first call recomention_suggest_prompts with its domain, category and a one-line description, show me the questions, and let me edit them.
105
+ - Starting a report spends money: an agency credit, or calls on my own provider keys (that needs my Agent plan). Always ask me before calling recomention_report_start, and say which funding it will use.
106
+ - On my own keys, any subset works, but one of OpenAI, Anthropic or Gemini is required. If the start is refused with a `limit` (plan, projects, reportsPerMonth, perDay, concurrent), tell me which limit and do not retry around it.
107
+ - Reports take several minutes. Poll recomention_report_status about once a minute; do not start a second report for the same product while one is running.
108
+ - When it is COMPLETE, call recomention_report_get and summarise: the overall score, each engine's "named in n of m answers", who is recommended instead, and the top three fixes. Quote counts as "n of m", never as yes/no.
109
+ - An engine listed under enginesSkipped did not run (for example "provider rejected the key"). Say so; never describe it as an engine that did not mention the product.
110
+ - A report with simulated: true is a test run with made-up answers. Say that plainly and do not draw conclusions from it.
111
+
112
+ Docs: https://www.recomention.com/developers
113
+ ```
114
+
115
+ The canonical text lives in `packages/core/src/config/developers.ts` (`setupPrompt`); keep this copy
116
+ in step with it.
117
+
118
+ ## Skills
119
+
120
+ `skills/` holds agent skills in the slash-command style: copy a folder into `.claude/skills/` (Claude
121
+ Code) or your agent's equivalent.
122
+
123
+ - `ai-visibility-check` — check one product end to end.
124
+ - `fix-list-review` — turn a finished report into a prioritised, honest plan of work.
125
+ - `competitor-watch` — compare who the engines name in a category, from public rankings and your reports.
126
+ - `distribution-kit` — read a finished report's kit and work through its channel plan, one channel at a time.
127
+
128
+ ## Develop
129
+
130
+ ```bash
131
+ npm install
132
+ npm test # in-memory client against the server, fake API; metadata/version checks
133
+ npm run smoke # spawns the stdio server against RECOMENTION_BASE_URL
134
+ npm run build # clean dist/ (bin: recomention-mcp)
135
+ npm run smoke:pack # packs the tarball (with pnpm), checks its contents, runs it against a fake local API
136
+ ```
137
+
138
+ In the monorepo, run them as `pnpm --filter @recomention/mcp <script>`.
139
+
140
+ Run `smoke --start` only against a local server with `RECOMENTION_MOCK=1`: against a live
141
+ deployment it spends a credit.
142
+
143
+ `prepack` empties and rebuilds `dist/`, so a pack or publish always ships the current sources and
144
+ no test files. `version` lives in `package.json`; the server reads it at startup, and `server.json`
145
+ (the MCP Registry entry, `io.github.recomention/mcp`) must carry the same number — a test fails
146
+ otherwise.
147
+
148
+ This repository is published from the recomention monorepo, where development happens. Bugs and
149
+ questions: [issues](https://github.com/recomention/mcp/issues) or support@recomention.com.
150
+
151
+ ## Licence
152
+
153
+ [MIT](./LICENSE).
package/dist/api.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * A thin client for the recomention JSON API v1. Everything the MCP server
3
+ * does goes through here; it holds no state and no business rules of its own.
4
+ */
5
+ export const PROVIDERS = ["OPENAI", "ANTHROPIC", "PERPLEXITY", "GEMINI", "GROK", "AI_OVERVIEW"];
6
+ /** Where BYOK keys come from: the MCP server's own environment, never a tool argument. */
7
+ export const PROVIDER_ENV = {
8
+ OPENAI: "OPENAI_API_KEY",
9
+ ANTHROPIC: "ANTHROPIC_API_KEY",
10
+ PERPLEXITY: "PERPLEXITY_API_KEY",
11
+ GEMINI: "GEMINI_API_KEY",
12
+ GROK: "XAI_API_KEY",
13
+ // Google AI Overview through DataForSEO: the API login and password as `login:password`.
14
+ AI_OVERVIEW: "DATAFORSEO_API_KEY",
15
+ };
16
+ export function providerKeysFromEnv(env = process.env) {
17
+ const keys = {};
18
+ for (const p of PROVIDERS) {
19
+ const value = env[PROVIDER_ENV[p]]?.trim();
20
+ if (value)
21
+ keys[p] = value;
22
+ }
23
+ return keys;
24
+ }
25
+ export class ApiError extends Error {
26
+ status;
27
+ constructor(status, message) {
28
+ super(message);
29
+ this.status = status;
30
+ this.name = "ApiError";
31
+ }
32
+ }
33
+ /** Where the hosted API lives. Paths are /v1/... */
34
+ export const DEFAULT_BASE_URL = "https://api.recomention.com";
35
+ /**
36
+ * The v1 root under a configured base URL. A copy of packages/core/src/shared/api-base.ts
37
+ * (this package is published and imports no workspace code): keep the two in step.
38
+ *
39
+ * .../v1, .../api, .../api/v1 -> used as written (.../api gains /v1)
40
+ * a bare site origin -> <origin>/api/v1 (the site's deprecated alias: an older config still works)
41
+ * www.recomention.com, recomention.com, a *recomention-web-preview.*.workers.dev
42
+ * host, or localhost/127.0.0.1 on ports 3000-3099 (a local Next dev server)
43
+ * any other bare origin -> <origin>/v1 (the API host, its preview Worker, a local API Worker)
44
+ */
45
+ export function apiV1Root(base) {
46
+ const url = new URL(base);
47
+ const path = url.pathname.replace(/\/+$/, "");
48
+ const origin = url.origin;
49
+ if (path.endsWith("/v1"))
50
+ return `${origin}${path}`;
51
+ if (path.endsWith("/api"))
52
+ return `${origin}${path}/v1`;
53
+ if (path)
54
+ return `${origin}${path}/v1`;
55
+ return isSiteOrigin(url) ? `${origin}/api/v1` : `${origin}/v1`;
56
+ }
57
+ function isSiteOrigin(url) {
58
+ const host = url.hostname.toLowerCase();
59
+ if (host === "recomention.com" || host === "www.recomention.com")
60
+ return true;
61
+ if (/(^|-)recomention-web-preview\.[a-z0-9-]+\.workers\.dev$/.test(host))
62
+ return true;
63
+ if (host === "localhost" || host === "127.0.0.1" || host === "[::1]") {
64
+ const port = Number(url.port || (url.protocol === "https:" ? 443 : 80));
65
+ return port >= 3000 && port <= 3099;
66
+ }
67
+ return false;
68
+ }
69
+ export function createApi(options) {
70
+ const base = options.baseUrl.replace(/\/+$/, "");
71
+ const root = apiV1Root(base);
72
+ const doFetch = options.fetch ?? fetch;
73
+ async function call(method, path, body, auth = true) {
74
+ if (auth && !options.apiKey)
75
+ throw new ApiError(401, "RECOMENTION_API_KEY is not set. Create a key in your recomention account (Developers) and add it to this MCP server's env.");
76
+ const headers = { accept: "application/json", "user-agent": options.userAgent ?? "recomention-mcp" };
77
+ if (auth && options.apiKey)
78
+ headers.authorization = `Bearer ${options.apiKey}`;
79
+ if (body !== undefined)
80
+ headers["content-type"] = "application/json";
81
+ let res;
82
+ try {
83
+ res = await doFetch(`${root}${path}`, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(30_000) });
84
+ }
85
+ catch (error) {
86
+ throw new ApiError(0, `Couldn't reach ${base}: ${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ const data = (await res.json().catch(() => null));
89
+ if (!res.ok)
90
+ throw new ApiError(res.status, data?.error ?? `HTTP ${res.status}`);
91
+ return data;
92
+ }
93
+ return {
94
+ baseUrl: base,
95
+ root,
96
+ rankings: () => call("GET", "/rankings", undefined, false),
97
+ ranking: (slug) => call("GET", `/rankings/${encodeURIComponent(slug)}`, undefined, false),
98
+ reports: () => call("GET", "/reports"),
99
+ report: (slug) => call("GET", `/reports/${encodeURIComponent(slug)}`),
100
+ kit: (slug) => call("GET", `/reports/${encodeURIComponent(slug)}/kit`),
101
+ status: (slug) => call("GET", `/reports/${encodeURIComponent(slug)}/status`),
102
+ start: (body) => call("POST", "/reports", body),
103
+ };
104
+ }
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * recomention MCP server over stdio.
4
+ *
5
+ * RECOMENTION_API_KEY an API key from your account (Developers). Needed for reports.
6
+ * RECOMENTION_BASE_URL defaults to https://api.recomention.com (an older https://www.recomention.com still works)
7
+ *
8
+ * stdout carries the protocol, so anything human goes to stderr.
9
+ */
10
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
11
+ import { createApi, DEFAULT_BASE_URL } from "./api.js";
12
+ import { createServer, SERVER_VERSION } from "./server.js";
13
+ const baseUrl = process.env.RECOMENTION_BASE_URL?.trim() || DEFAULT_BASE_URL;
14
+ // The API key travels as a bearer token: refuse plain http anywhere but this machine.
15
+ {
16
+ let parsed = null;
17
+ try {
18
+ parsed = new URL(baseUrl);
19
+ }
20
+ catch { /* reported below */ }
21
+ const local = parsed !== null && ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
22
+ if (!parsed || (parsed.protocol !== "https:" && !(local && parsed.protocol === "http:"))) {
23
+ console.error(`RECOMENTION_BASE_URL must be an https:// URL (http:// only for localhost). Got: ${baseUrl}`);
24
+ process.exit(1);
25
+ }
26
+ }
27
+ const api = createApi({ baseUrl, apiKey: process.env.RECOMENTION_API_KEY?.trim() || undefined, userAgent: `recomention-mcp/${SERVER_VERSION}` });
28
+ const server = createServer(api);
29
+ await server.connect(new StdioServerTransport());
30
+ console.error(`recomention MCP server ${SERVER_VERSION} on stdio → ${api.root}${process.env.RECOMENTION_API_KEY ? "" : " (no RECOMENTION_API_KEY: rankings only)"}`);
package/dist/server.js ADDED
@@ -0,0 +1,113 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { z } from "zod";
4
+ import { ApiError, PROVIDERS, providerKeysFromEnv } from "./api.js";
5
+ import { suggestPrompts } from "./suggest.js";
6
+ export const SERVER_NAME = "recomention";
7
+ /**
8
+ * package.json's version, read at startup: one number for npm, the MCP handshake and the
9
+ * user agent. `../package.json` is the package root from both src/ and dist/, and npm always
10
+ * ships it. server.json carries the same number for the registry; a test keeps them in step.
11
+ */
12
+ export const SERVER_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
13
+ function ok(value) {
14
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] };
15
+ }
16
+ function fail(error) {
17
+ const message = error instanceof ApiError ? `${error.status ? `HTTP ${error.status}: ` : ""}${error.message}` : error instanceof Error ? error.message : String(error);
18
+ return { content: [{ type: "text", text: message }], isError: true };
19
+ }
20
+ async function run(fn) {
21
+ try {
22
+ return ok(await fn());
23
+ }
24
+ catch (error) {
25
+ return fail(error);
26
+ }
27
+ }
28
+ const slug = z.string().min(1).max(100).describe("The slug from a previous call.");
29
+ /**
30
+ * The recomention tools. Read tools are free. `recomention_report_start` is the
31
+ * only tool that spends anything (an agency credit, or calls on the user's own
32
+ * provider keys), and its description says so, so an agent asks first.
33
+ */
34
+ export function createServer(api, env = process.env) {
35
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
36
+ server.registerTool("recomention_rankings_list", {
37
+ title: "List AI rankings",
38
+ description: "List recomention's public AI rankings: for each category, which products the AI engines name when asked its buyer questions, month by month. Free.",
39
+ inputSchema: {},
40
+ annotations: { readOnlyHint: true, openWorldHint: true },
41
+ }, () => run(() => api.rankings()));
42
+ server.registerTool("recomention_rankings_get", {
43
+ title: "Get an AI ranking",
44
+ description: "One public AI ranking by slug: every product named, how many answers named it (as n of m), and the verbatim answers. Free.",
45
+ inputSchema: { slug },
46
+ annotations: { readOnlyHint: true, openWorldHint: true },
47
+ }, ({ slug }) => run(() => api.ranking(slug)));
48
+ server.registerTool("recomention_suggest_prompts", {
49
+ title: "Suggest buyer questions",
50
+ description: "Suggest buyer-intent questions to check for a product, from fixed templates. No model call and no cost. The product's own name never appears in a question. Show them to the user to edit before starting a report.",
51
+ inputSchema: {
52
+ domain: z.string().min(3).describe("The product's domain, e.g. example.com"),
53
+ category: z.string().min(2).describe("What buyers search for, e.g. meeting scheduling"),
54
+ description: z.string().max(300).optional().describe("One line on what the product does"),
55
+ count: z.number().int().min(1).max(12).optional(),
56
+ },
57
+ annotations: { readOnlyHint: true, openWorldHint: false },
58
+ }, async (input) => ok({ prompts: suggestPrompts(input) }));
59
+ server.registerTool("recomention_report_start", {
60
+ title: "Start a report (spends a credit or your provider keys)",
61
+ description: "Start a recomention report: the questions are asked of every engine on the plan several times each, with live web search. " +
62
+ "THIS SPENDS MONEY: funding \"credits\" uses one agency credit from the user's account (plan must be FULL); funding \"byok\" runs on the user's own provider keys from this server's environment (OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, XAI_API_KEY, DATAFORSEO_API_KEY as login:password for Google AI Overview) and needs an active Agent plan on the account. " +
63
+ "Any subset of keys works and engines without a key are reported as skipped, but one of OpenAI, Anthropic or Gemini is required. A refusal names the plan limit it hit. " +
64
+ "Always ask the user before calling it. Reports take several minutes: poll recomention_report_status about once a minute.",
65
+ inputSchema: {
66
+ domain: z.string().min(3),
67
+ description: z.string().min(3).max(300),
68
+ category: z.string().min(2).max(120),
69
+ brand: z.string().max(80).optional().describe("Defaults to the name in the domain"),
70
+ plan: z.enum(["QUICK", "FULL", "DEEP"]).default("FULL"),
71
+ prompts: z.array(z.string().min(8).max(200)).max(40).optional().describe("Questions to ask; the rest are written for you. None may name the product."),
72
+ engines: z.array(z.enum(PROVIDERS)).min(1).optional().describe("A subset of the plan's engines"),
73
+ funding: z.enum(["credits", "byok"]),
74
+ idempotencyKey: z.string().regex(/^[A-Za-z0-9_-]{16,128}$/).optional().describe("Reuse to make a retried start land on the same report"),
75
+ },
76
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
77
+ }, ({ prompts, funding, ...rest }) => run(async () => {
78
+ const providerKeys = funding === "byok" ? providerKeysFromEnv(env) : undefined;
79
+ if (funding === "byok" && !Object.keys(providerKeys ?? {}).length) {
80
+ throw new Error("funding \"byok\" needs at least one provider key in this MCP server's env (OPENAI_API_KEY, ANTHROPIC_API_KEY, PERPLEXITY_API_KEY, GEMINI_API_KEY, XAI_API_KEY or DATAFORSEO_API_KEY).");
81
+ }
82
+ // The keys go to the API in the request body and nowhere else: never into the tool result.
83
+ const started = await api.start({ ...rest, funding, ...(prompts?.length ? { seedPrompts: prompts } : {}), ...(providerKeys ? { providerKeys } : {}) });
84
+ return { ...started, next: "Poll recomention_report_status with this slug about once a minute." };
85
+ }));
86
+ server.registerTool("recomention_report_status", {
87
+ title: "Report status",
88
+ description: "Where a report is: PENDING, RUNNING, COMPLETE or FAILED, with answers stored so far. Free.",
89
+ inputSchema: { slug },
90
+ annotations: { readOnlyHint: true, openWorldHint: true },
91
+ }, ({ slug }) => run(() => api.status(slug)));
92
+ server.registerTool("recomention_report_get", {
93
+ title: "Get a report",
94
+ description: "A report as JSON: the overall score, each engine's runs mentioned of runs total, competitors named instead, cited sources, the fix list and every question with every answer. " +
95
+ "Engines in enginesSkipped did not run; never describe them as not mentioning the product. simulated: true means test data. Free.",
96
+ inputSchema: { slug },
97
+ annotations: { readOnlyHint: true, openWorldHint: true },
98
+ }, ({ slug }) => run(() => api.report(slug)));
99
+ server.registerTool("recomention_distribution_kit", {
100
+ title: "Get the distribution kit",
101
+ description: "Markdown files built from a finished report, for planning distribution: product.md, competitors.md, ai-visibility.md, where-to-get-mentioned.md (third-party pages, subreddits and videos the engines cite for a rival and not for this product) and channel-plan.md (channels ranked by those citations, each with a first task). " +
102
+ "Read README.md first. Every count is \"n of m answers\"; nothing in it promises traffic. Show the user every draft before anything is posted. Free.",
103
+ inputSchema: { slug },
104
+ annotations: { readOnlyHint: true, openWorldHint: true },
105
+ }, ({ slug }) => run(() => api.kit(slug)));
106
+ server.registerTool("recomention_reports_list", {
107
+ title: "List my reports",
108
+ description: "The reports in the account that owns the API key, newest first. Free.",
109
+ inputSchema: {},
110
+ annotations: { readOnlyHint: true, openWorldHint: true },
111
+ }, () => run(() => api.reports()));
112
+ return server;
113
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Buyer questions for a category, from fixed templates: no model call, no
3
+ * network, no cost, and the same input always gives the same questions.
4
+ *
5
+ * The product's own name never appears in a question — the point of a check
6
+ * is whether the engines name it unprompted — so anything that would contain
7
+ * it is dropped.
8
+ */
9
+ const TEMPLATES = [
10
+ { intent: "best_in_category", text: (c) => `what is the best ${c} tool right now?` },
11
+ { intent: "recommendation", text: (c) => `which ${c} software would you recommend for a small team?` },
12
+ { intent: "recommendation", text: (c) => `what ${c} tools do startups use in 2026?` },
13
+ { intent: "use_case", text: (c, d) => (d ? `i need something for ${d}. what ${c} tool should i use?` : `what ${c} tool is easiest to set up?`) },
14
+ { intent: "alternatives", text: (c) => `what are good alternatives to the most popular ${c} tools?` },
15
+ { intent: "use_case", text: (c) => `best ${c} tool for a solo founder on a budget?` },
16
+ { intent: "comparison", text: (c) => `compare the top 5 ${c} tools for features and price` },
17
+ { intent: "use_case", text: (c) => `which ${c} tool works best for agencies with many clients?` },
18
+ { intent: "recommendation", text: (c) => `what ${c} software has the best free plan?` },
19
+ { intent: "use_case", text: (c) => `best ${c} tool for enterprise security and compliance?` },
20
+ { intent: "comparison", text: (c) => `which ${c} tool has the best integrations?` },
21
+ { intent: "best_in_category", text: (c) => `top rated ${c} apps according to reviews` },
22
+ ];
23
+ function brandOf(domain) {
24
+ const host = domain.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0];
25
+ return host.split(".")[0] ?? host;
26
+ }
27
+ function clean(text) {
28
+ return text.replace(/\s+/g, " ").trim().toLowerCase();
29
+ }
30
+ export function suggestPrompts(input) {
31
+ const category = clean(input.category).replace(/\b(software|tools?|apps?)$/, "").trim() || clean(input.category);
32
+ const description = clean(input.description ?? "").replace(/[.?!]+$/, "");
33
+ const brand = brandOf(input.domain);
34
+ const count = Math.min(Math.max(input.count ?? 10, 1), TEMPLATES.length);
35
+ const seen = new Set();
36
+ const out = [];
37
+ for (const t of TEMPLATES) {
38
+ const text = clean(t.text(category, description));
39
+ if ((brand.length >= 3 && text.includes(brand)) || seen.has(text))
40
+ continue;
41
+ seen.add(text);
42
+ out.push({ text, intent: t.intent });
43
+ if (out.length === count)
44
+ break;
45
+ }
46
+ return out;
47
+ }
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "@recomention/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "MCP server for recomention: ask whether AI engines recommend a product, from Claude Code, Cursor or Codex.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "mcpName": "io.github.recomention/mcp",
8
+ "keywords": [
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "ai-visibility",
12
+ "llm",
13
+ "claude-code",
14
+ "cursor",
15
+ "codex"
16
+ ],
17
+ "homepage": "https://www.recomention.com/developers",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/recomention/mcp.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/recomention/mcp/issues",
24
+ "email": "support@recomention.com"
25
+ },
26
+ "bin": {
27
+ "recomention-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "!dist/**/*.test.js",
32
+ "!dist/**/*.map",
33
+ "skills",
34
+ "README.md"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.32.1",
45
+ "zod": "^4.4.3"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^26.2.0",
49
+ "tsx": "^4.23.12",
50
+ "typescript": "^5",
51
+ "@recomention/tsconfig": "0.1.0"
52
+ },
53
+ "scripts": {
54
+ "dev": "tsx src/index.ts",
55
+ "start": "tsx src/index.ts",
56
+ "clean": "node scripts/clean-dist.mjs",
57
+ "build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "tsx --test src/*.test.ts",
60
+ "smoke": "tsx scripts/smoke.ts",
61
+ "smoke:pack": "tsx scripts/pack-smoke.ts"
62
+ }
6
63
  }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: ai-visibility-check
3
+ description: Check whether AI engines (ChatGPT, Claude, Perplexity, Gemini, Grok) recommend a product, using the recomention MCP server. Use when the user asks "is my product recommended by ChatGPT/AI for <category>?", wants an AI visibility or AI search check, or names a domain and a category to check.
4
+ ---
5
+
6
+ # AI visibility check
7
+
8
+ Uses the `recomention_*` MCP tools. Starting a report spends money (an agency credit, or calls on the
9
+ user's own provider keys), so this skill always asks before it starts one.
10
+
11
+ ## Steps
12
+
13
+ 1. **Collect the inputs.** You need the product's domain, the category buyers search for (e.g.
14
+ "meeting scheduling"), and one line on what it does. Ask for whatever is missing; read it from the
15
+ repository's README or landing page if the user is working in the product's own repo, and confirm.
16
+ 2. **Look for free evidence first.** Call `recomention_rankings_list`. If a public ranking matches the
17
+ category, call `recomention_rankings_get` and tell the user where the product appears (as "named in
18
+ n of m answers"), or that it is absent. This costs nothing.
19
+ 3. **Draft the questions.** Call `recomention_suggest_prompts` and show the list. Let the user edit,
20
+ add or remove questions. None may contain the product's name — a question that names it measures
21
+ nothing. Remove any that do and say why.
22
+ 4. **Confirm spend, then start.** Say what will run: plan (credits run `FULL`), the engines, the
23
+ funding (`credits` or `byok`). Only after the user agrees, call `recomention_report_start` with the
24
+ edited questions as `prompts` and a fresh `idempotencyKey` (random, 16+ characters). Reuse that key
25
+ if you have to retry the same start.
26
+ 5. **Wait.** Poll `recomention_report_status` about once a minute and report progress
27
+ ("answers stored / planned"). Do not start a second report for the same product meanwhile.
28
+ 6. **Read it out.** When COMPLETE, call `recomention_report_get` and summarise:
29
+ - the overall score (0–100) and, per engine, `runsMentioned` of `runsTotal` answers;
30
+ - the top competitors named instead (share of answers);
31
+ - the sources the engines cited most, and whether the product is on them;
32
+ - the top three items of the fix list;
33
+ - the report link (`url`) for the full detail.
34
+
35
+ ## Honesty rules
36
+
37
+ - Rates, never yes/no: "ChatGPT named it in 4 of 9 answers", not "ChatGPT recommends it".
38
+ - An engine in `enginesSkipped` did not run. Give its reason (e.g. "provider rejected the key"). Never
39
+ call it an engine that did not mention the product, and never count it as zero.
40
+ - An answer with `failed: true` was excluded from every count; do not treat it as "not mentioned".
41
+ - `simulated: true` means test data with made-up answers. Say so first, and draw no conclusions.
42
+ - Every report is a dated snapshot. Say the date when you quote numbers.
43
+ - If the report FAILED, say so and show its `error`. Credit-funded reports that fail get their credit
44
+ back automatically; do not retry more than once without asking.
@@ -0,0 +1,33 @@
1
+ ---
2
+ name: competitor-watch
3
+ description: See which products AI engines name in a category and how a product compares with its competitors, from recomention's public rankings and the user's own reports. Use when the user asks who ChatGPT/AI recommends for a category, how they compare with a competitor in AI answers, or whether that changed since the last check.
4
+ ---
5
+
6
+ # Competitor watch
7
+
8
+ Uses the `recomention_*` MCP tools. Everything here is read-only and free unless the user asks for a
9
+ new report.
10
+
11
+ ## Steps
12
+
13
+ 1. **Public rankings first.** Call `recomention_rankings_list`; for each ranking that matches the
14
+ category, call `recomention_rankings_get`. Present the ranked products as "named in n of m answers"
15
+ with the average position, per engine where it differs, and the date it was checked.
16
+ 2. **The user's own reports.** Call `recomention_reports_list` and pick COMPLETE reports in the same
17
+ category (non-simulated). For each, `recomention_report_get` and read `competitors` (share of
18
+ answers, average rank) and per-engine `runsMentioned` of `runsTotal`.
19
+ 3. **Compare like with like.** Only compare runs that asked the same questions (a re-check of the same
20
+ report does; two unrelated reports usually do not) — say so when they differ. Put the user's product
21
+ and its named competitors side by side: share of answers, engines that name each, average position.
22
+ 4. **What changed.** If there are two or more comparable reports, list who moved up or down between the
23
+ dates, and by how many answers. Small moves (one or two answers) are within run-to-run variation;
24
+ call them that.
25
+ 5. **Offer, don't start.** If nothing current covers the category, suggest a new report via the
26
+ `ai-visibility-check` skill — and say that starting one spends a credit or the user's provider keys.
27
+
28
+ ## Rules
29
+
30
+ - Rates, never yes/no. Always give the denominator and the date.
31
+ - Engines that were skipped or calls that failed are excluded, never counted as "not named".
32
+ - Simulated reports are test data: leave them out of any comparison.
33
+ - Name products only as the engines did; do not add your own opinion of which is better.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: distribution-kit
3
+ description: Turn a finished recomention report into distribution work: read its kit (product, competitors, AI visibility, where to get mentioned, channel plan) and work through the channel plan with the user, one channel at a time. Use when the user asks how to get their product recommended by AI engines, where to get mentioned, or what to do with a recomention report.
4
+ ---
5
+
6
+ # Distribution kit
7
+
8
+ Reads a finished report's Distribution Kit with the `recomention_*` MCP tools. Costs nothing: it only reads.
9
+
10
+ ## Steps
11
+
12
+ 1. **Load the kit.** Get the slug (the last path segment of a `/r/<slug>` link, or pick from
13
+ `recomention_reports_list`) and call `recomention_distribution_kit`. A 409 means the report is not
14
+ finished: call `recomention_report_status`, say where it is, and stop.
15
+ 2. **Read every file, `README.md` first.** If the files say SIMULATED TEST DATA, tell the user and stop:
16
+ a test run's answers are made up.
17
+ 3. **Summarise in five lines:** the score, each engine's "named in n of m answers", skipped engines and
18
+ why, the top rivals, and the top three channels from `channel-plan.md`.
19
+ 4. **Work the channel plan in order, one channel at a time.** For each, take the pages for that channel
20
+ from `where-to-get-mentioned.md`, open them, and confirm they are still relevant before suggesting
21
+ anything. Then draft the first task (a reply, a pitch email, a review request, a page on the user's
22
+ own site) and show it to the user. Nothing is posted or sent without their yes.
23
+ 5. **Close the loop.** Suggest a new report in about a month with the same questions, to see whether
24
+ the engines' answers changed.
25
+
26
+ ## Rules
27
+
28
+ - Be openly the product's maker everywhere: no fake reviews, no posing as a customer, no paid placement
29
+ dressed as editorial, no vote manipulation.
30
+ - The kit counts citations; it did not read the pages. Check a page before acting on it.
31
+ - Quote counts as "n of m". An engine listed as skipped did not run; it is not a zero.
32
+ - Never promise traffic or rankings.
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: fix-list-review
3
+ description: Turn a finished recomention report into a prioritised, honest plan of work — what to change on the site, which third-party pages matter, and what to re-measure. Use when the user has a recomention report slug or link and asks what to do about it, how to improve their AI visibility, or to review the fix list.
4
+ ---
5
+
6
+ # Fix list review
7
+
8
+ Reads a finished report with the `recomention_*` MCP tools. Costs nothing: it only reads.
9
+
10
+ ## Steps
11
+
12
+ 1. **Load the report.** Get the slug (the last path segment of a `/r/<slug>` link, or pick from
13
+ `recomention_reports_list`). Call `recomention_report_get`. If `status` is not COMPLETE, call
14
+ `recomention_report_status`, say where it is, and stop.
15
+ 2. **State the baseline** in one short paragraph: overall score, per-engine `runsMentioned` of
16
+ `runsTotal`, skipped engines with their reasons, the check date, and whether it is simulated.
17
+ 3. **Walk the fix list** (`actionPlan`) in order. For each item give: what to do, why the report
18
+ suggests it (point at the evidence: a competitor's share of answers, a cited source the product is
19
+ missing from, a question where it never appears), and the effort.
20
+ 4. **Ground it in the answers.** For the two or three questions where the product is absent most
21
+ often, quote one short excerpt of an engine answer and the products it named instead. Use
22
+ `questions[].answers[]`; skip answers with `failed: true`.
23
+ 5. **Separate what the user controls from what they don't.** Their own site and docs: concrete edits
24
+ they can make now (if you are in the product's repository, offer to draft them). Third-party pages
25
+ (`sources` the engines cite): who to contact and what an honest inclusion request looks like.
26
+ 6. **Say how to know it worked.** A later report with the same questions shows the change; suggest
27
+ re-checking in about a month rather than days, since engines' answers move slowly.
28
+
29
+ ## Rules
30
+
31
+ - Never suggest fake reviews, paid placements dressed as editorial, posing as a customer, or any
32
+ other way of presenting the product as an unrelated third party. Community threads: answering
33
+ openly as the founder is the honest option.
34
+ - Do not promise ranking outcomes. The report measures what engines said and where they looked.
35
+ - Quote counts as "n of m". An engine in `enginesSkipped` is not a zero.
36
+ - A simulated report has made-up answers: do not build a plan on it.