recommended-by-mcp 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 simple bytes
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 ADDED
@@ -0,0 +1,114 @@
1
+ # recommended-by-mcp
2
+
3
+ Stdio MCP server for managing your recommended.by lists from agents like OpenClaw, Codex, Claude Desktop, Claude Code, Cursor, and Windsurf.
4
+
5
+ The package talks to the recommended.by REST API and exposes MCP tools over stdio. This is useful for clients that do not support remote HTTP MCP headers consistently: the agent launches this package locally, and the package handles recommended.by authentication.
6
+
7
+ ## Quick Start
8
+
9
+ Create an API key in `Dashboard -> Profile & URL -> API Keys`, then authenticate the package once:
10
+
11
+ ```bash
12
+ npx recommended-by-mcp@latest login rb_live_your_key_here
13
+ ```
14
+
15
+ Then configure your MCP client to run:
16
+
17
+ ```bash
18
+ npx -y recommended-by-mcp@latest
19
+ ```
20
+
21
+ You can also avoid local storage and pass the key through an environment variable:
22
+
23
+ ```bash
24
+ RECOMMENDED_BY_API_KEY=rb_live_your_key_here npx -y recommended-by-mcp@latest
25
+ ```
26
+
27
+ ## Authentication
28
+
29
+ The server resolves auth in this order:
30
+
31
+ 1. `--api-key rb_live_...`
32
+ 2. `RECOMMENDED_BY_API_KEY`
33
+ 3. saved config from `recommended-by-mcp login`
34
+
35
+ The saved config lives at:
36
+
37
+ - macOS: `~/Library/Application Support/recommended-by-mcp/config.json`
38
+ - Linux: `~/.config/recommended-by-mcp/config.json`
39
+ - Windows: `%APPDATA%\recommended-by-mcp\config.json`
40
+
41
+ Remove the saved key with:
42
+
43
+ ```bash
44
+ npx recommended-by-mcp@latest logout
45
+ ```
46
+
47
+ Check which account the key can access:
48
+
49
+ ```bash
50
+ npx recommended-by-mcp@latest whoami
51
+ ```
52
+
53
+ ## MCP Tools
54
+
55
+ - `recommended_list_lists`
56
+ - `recommended_create_list`
57
+ - `recommended_get_list`
58
+ - `recommended_update_list`
59
+ - `recommended_delete_list`
60
+ - `recommended_list_items`
61
+ - `recommended_add_item`
62
+ - `recommended_update_item`
63
+ - `recommended_delete_item`
64
+
65
+ ## Client Examples
66
+
67
+ ### Codex
68
+
69
+ Add this to `~/.codex/config.toml`:
70
+
71
+ ```toml
72
+ [mcp_servers.recommended_by]
73
+ command = "npx"
74
+ args = ["-y", "recommended-by-mcp@latest"]
75
+ env = { RECOMMENDED_BY_API_KEY = "rb_live_your_key_here" }
76
+ ```
77
+
78
+ ### OpenClaw
79
+
80
+ ```bash
81
+ openclaw mcp add recommended-by \
82
+ --command npx \
83
+ --arg -y \
84
+ --arg recommended-by-mcp@latest \
85
+ --env RECOMMENDED_BY_API_KEY=rb_live_your_key_here
86
+
87
+ openclaw mcp doctor recommended-by --probe
88
+ ```
89
+
90
+ ### Claude Desktop
91
+
92
+ Add this to `claude_desktop_config.json`:
93
+
94
+ ```json
95
+ {
96
+ "mcpServers": {
97
+ "recommended-by": {
98
+ "command": "npx",
99
+ "args": ["-y", "recommended-by-mcp@latest"],
100
+ "env": {
101
+ "RECOMMENDED_BY_API_KEY": "rb_live_your_key_here"
102
+ }
103
+ }
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## Development
109
+
110
+ ```bash
111
+ pnpm install
112
+ pnpm --filter recommended-by-mcp build
113
+ pnpm --filter recommended-by-mcp test
114
+ ```
package/dist/auth.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type StoredConfig = {
2
+ apiKey?: string;
3
+ apiBaseUrl?: string;
4
+ };
5
+ export declare function getConfigPath(): string;
6
+ export declare function loadStoredConfig(): Promise<StoredConfig>;
7
+ export declare function saveApiKey(apiKey: string, apiBaseUrl?: string): Promise<string>;
8
+ export declare function clearStoredConfig(): Promise<string>;
9
+ export declare function assertApiKey(apiKey: string): void;
10
+ export declare function resolveAuth(options: {
11
+ apiKey?: string;
12
+ apiBaseUrl?: string;
13
+ }): Promise<{
14
+ apiKey: string;
15
+ apiBaseUrl: string;
16
+ }>;
package/dist/auth.js ADDED
@@ -0,0 +1,63 @@
1
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { homedir, platform } from "node:os";
4
+ const configFileName = "config.json";
5
+ export function getConfigPath() {
6
+ const appDir = "recommended-by-mcp";
7
+ if (process.env.RECOMMENDED_BY_MCP_CONFIG) {
8
+ return process.env.RECOMMENDED_BY_MCP_CONFIG;
9
+ }
10
+ if (platform() === "darwin") {
11
+ return join(homedir(), "Library", "Application Support", appDir, configFileName);
12
+ }
13
+ if (platform() === "win32") {
14
+ return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), appDir, configFileName);
15
+ }
16
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), appDir, configFileName);
17
+ }
18
+ export async function loadStoredConfig() {
19
+ try {
20
+ const raw = await readFile(getConfigPath(), "utf8");
21
+ const parsed = JSON.parse(raw);
22
+ return {
23
+ apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : undefined,
24
+ apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : undefined,
25
+ };
26
+ }
27
+ catch {
28
+ return {};
29
+ }
30
+ }
31
+ export async function saveApiKey(apiKey, apiBaseUrl) {
32
+ assertApiKey(apiKey);
33
+ const configPath = getConfigPath();
34
+ await mkdir(dirname(configPath), { recursive: true });
35
+ await writeFile(configPath, `${JSON.stringify({ apiKey, apiBaseUrl }, null, 2)}\n`, { mode: 0o600 });
36
+ return configPath;
37
+ }
38
+ export async function clearStoredConfig() {
39
+ const configPath = getConfigPath();
40
+ await rm(configPath, { force: true });
41
+ return configPath;
42
+ }
43
+ export function assertApiKey(apiKey) {
44
+ if (!apiKey.startsWith("rb_live_") && !apiKey.startsWith("rb_test_")) {
45
+ throw new Error("recommended.by API keys must start with rb_live_ or rb_test_");
46
+ }
47
+ }
48
+ export async function resolveAuth(options) {
49
+ const stored = await loadStoredConfig();
50
+ const apiKey = options.apiKey ?? process.env.RECOMMENDED_BY_API_KEY ?? stored.apiKey;
51
+ const apiBaseUrl = options.apiBaseUrl ??
52
+ process.env.RECOMMENDED_BY_API_BASE_URL ??
53
+ stored.apiBaseUrl ??
54
+ "https://api.recommended.by/api/v1";
55
+ if (!apiKey) {
56
+ throw new Error("Missing recommended.by API key. Run `recommended-by-mcp login rb_live_...` or set RECOMMENDED_BY_API_KEY.");
57
+ }
58
+ assertApiKey(apiKey);
59
+ return {
60
+ apiKey,
61
+ apiBaseUrl: apiBaseUrl.replace(/\/+$/, ""),
62
+ };
63
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ import { clearStoredConfig, resolveAuth, saveApiKey } from "./auth.js";
3
+ import { RecommendedByClient } from "./client.js";
4
+ import { runMcpServer } from "./server.js";
5
+ async function main() {
6
+ const args = parseArgs(process.argv.slice(2));
7
+ if (args.help || args.command === "help") {
8
+ printHelp();
9
+ return;
10
+ }
11
+ if (args.command === "login") {
12
+ const apiKey = args.apiKey ?? args.rest[0];
13
+ if (!apiKey) {
14
+ throw new Error("Usage: recommended-by-mcp login rb_live_...");
15
+ }
16
+ const configPath = await saveApiKey(apiKey, args.apiBaseUrl);
17
+ process.stdout.write(`Saved recommended.by API key to ${configPath}\n`);
18
+ return;
19
+ }
20
+ if (args.command === "logout") {
21
+ const configPath = await clearStoredConfig();
22
+ process.stdout.write(`Removed recommended.by MCP config at ${configPath}\n`);
23
+ return;
24
+ }
25
+ if (args.command === "whoami") {
26
+ const auth = await resolveAuth(args);
27
+ const client = new RecommendedByClient(auth);
28
+ const lists = await client.request("GET", "/lists");
29
+ process.stdout.write(`${JSON.stringify({ ok: true, apiBaseUrl: auth.apiBaseUrl, lists }, null, 2)}\n`);
30
+ return;
31
+ }
32
+ if (args.command === "config-path") {
33
+ const { getConfigPath } = await import("./auth.js");
34
+ process.stdout.write(`${getConfigPath()}\n`);
35
+ return;
36
+ }
37
+ if (args.command && args.command !== "serve") {
38
+ throw new Error(`Unknown command: ${args.command}`);
39
+ }
40
+ const auth = await resolveAuth(args);
41
+ await runMcpServer(auth);
42
+ }
43
+ function parseArgs(argv) {
44
+ const result = { rest: [] };
45
+ const commands = new Set(["serve", "login", "logout", "whoami", "config-path", "help"]);
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const value = argv[index];
48
+ if (value === "--help" || value === "-h") {
49
+ result.help = true;
50
+ }
51
+ else if (value === "--api-key") {
52
+ result.apiKey = requireNext(argv, ++index, "--api-key");
53
+ }
54
+ else if (value === "--api-base-url") {
55
+ result.apiBaseUrl = requireNext(argv, ++index, "--api-base-url");
56
+ }
57
+ else if (!result.command && commands.has(value)) {
58
+ result.command = value;
59
+ }
60
+ else {
61
+ result.rest.push(value);
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+ function requireNext(argv, index, flag) {
67
+ const value = argv[index];
68
+ if (!value) {
69
+ throw new Error(`${flag} requires a value`);
70
+ }
71
+ return value;
72
+ }
73
+ function printHelp() {
74
+ process.stdout.write(`recommended-by-mcp
75
+
76
+ Usage:
77
+ recommended-by-mcp Start the stdio MCP server
78
+ recommended-by-mcp serve Start the stdio MCP server
79
+ recommended-by-mcp login <api-key> Save an API key locally
80
+ recommended-by-mcp logout Remove the saved API key
81
+ recommended-by-mcp whoami Validate auth by listing your lists
82
+ recommended-by-mcp config-path Print the local config path
83
+
84
+ Options:
85
+ --api-key <key> Use a key for this invocation
86
+ --api-base-url <url> Override the API base URL
87
+
88
+ Environment:
89
+ RECOMMENDED_BY_API_KEY API key used by the MCP server
90
+ RECOMMENDED_BY_API_BASE_URL Defaults to https://api.recommended.by/api/v1
91
+ `);
92
+ }
93
+ main().catch((error) => {
94
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
95
+ process.exitCode = 1;
96
+ });
@@ -0,0 +1,8 @@
1
+ export declare class RecommendedByClient {
2
+ private readonly options;
3
+ constructor(options: {
4
+ apiKey: string;
5
+ apiBaseUrl: string;
6
+ });
7
+ request(method: string, path: string, body?: unknown): Promise<unknown>;
8
+ }
package/dist/client.js ADDED
@@ -0,0 +1,43 @@
1
+ export class RecommendedByClient {
2
+ options;
3
+ constructor(options) {
4
+ this.options = options;
5
+ }
6
+ async request(method, path, body) {
7
+ const response = await fetch(`${this.options.apiBaseUrl}${path}`, {
8
+ method,
9
+ headers: {
10
+ Authorization: `Bearer ${this.options.apiKey}`,
11
+ "Content-Type": "application/json",
12
+ "User-Agent": "recommended-by-mcp/0.1.0",
13
+ },
14
+ body: body === undefined ? undefined : JSON.stringify(body),
15
+ });
16
+ const text = await response.text();
17
+ const data = text ? parseJson(text) : null;
18
+ if (!response.ok) {
19
+ throw new Error(formatApiError(response.status, data));
20
+ }
21
+ return data;
22
+ }
23
+ }
24
+ function parseJson(text) {
25
+ try {
26
+ return JSON.parse(text);
27
+ }
28
+ catch {
29
+ return text;
30
+ }
31
+ }
32
+ function formatApiError(status, data) {
33
+ if (data && typeof data === "object" && "error" in data) {
34
+ const error = data.error;
35
+ if (typeof error === "string") {
36
+ return `recommended.by API error ${status}: ${error}`;
37
+ }
38
+ }
39
+ if (typeof data === "string" && data) {
40
+ return `recommended.by API error ${status}: ${data}`;
41
+ }
42
+ return `recommended.by API error ${status}`;
43
+ }
@@ -0,0 +1,36 @@
1
+ export type JsonRpcId = string | number | null;
2
+ export type JsonRpcRequest = {
3
+ jsonrpc?: string;
4
+ id?: JsonRpcId;
5
+ method?: string;
6
+ params?: unknown;
7
+ };
8
+ export type ToolDefinition = {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: {
12
+ type: "object";
13
+ properties: Record<string, unknown>;
14
+ required?: string[];
15
+ additionalProperties?: boolean;
16
+ };
17
+ };
18
+ export declare function jsonRpcResult(id: JsonRpcId | undefined, result: unknown): {
19
+ jsonrpc: string;
20
+ id: string | number | null;
21
+ result: unknown;
22
+ };
23
+ export declare function jsonRpcError(id: JsonRpcId | undefined, code: number, message: string): {
24
+ jsonrpc: string;
25
+ id: string | number | null;
26
+ error: {
27
+ code: number;
28
+ message: string;
29
+ };
30
+ };
31
+ export declare function textResult(value: unknown): {
32
+ content: {
33
+ type: string;
34
+ text: string;
35
+ }[];
36
+ };
@@ -0,0 +1,16 @@
1
+ export function jsonRpcResult(id, result) {
2
+ return { jsonrpc: "2.0", id: id ?? null, result };
3
+ }
4
+ export function jsonRpcError(id, code, message) {
5
+ return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
6
+ }
7
+ export function textResult(value) {
8
+ return {
9
+ content: [
10
+ {
11
+ type: "text",
12
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2),
13
+ },
14
+ ],
15
+ };
16
+ }
@@ -0,0 +1,7 @@
1
+ export declare function runMcpServer(options: {
2
+ apiKey: string;
3
+ apiBaseUrl: string;
4
+ stdin?: NodeJS.ReadableStream;
5
+ stdout?: NodeJS.WritableStream;
6
+ stderr?: NodeJS.WritableStream;
7
+ }): Promise<void>;
package/dist/server.js ADDED
@@ -0,0 +1,76 @@
1
+ import { createInterface } from "node:readline";
2
+ import { RecommendedByClient } from "./client.js";
3
+ import { jsonRpcError, jsonRpcResult } from "./protocol.js";
4
+ import { callTool, tools } from "./tools.js";
5
+ export async function runMcpServer(options) {
6
+ const client = new RecommendedByClient({
7
+ apiKey: options.apiKey,
8
+ apiBaseUrl: options.apiBaseUrl,
9
+ });
10
+ const input = createInterface({
11
+ input: options.stdin ?? process.stdin,
12
+ crlfDelay: Infinity,
13
+ });
14
+ const output = options.stdout ?? process.stdout;
15
+ const stderr = options.stderr ?? process.stderr;
16
+ for await (const line of input) {
17
+ if (!line.trim())
18
+ continue;
19
+ let request;
20
+ try {
21
+ request = JSON.parse(line);
22
+ }
23
+ catch {
24
+ write(output, jsonRpcError(null, -32700, "Parse error"));
25
+ continue;
26
+ }
27
+ if (request.jsonrpc !== "2.0" || typeof request.method !== "string") {
28
+ write(output, jsonRpcError(request.id, -32600, "Invalid Request"));
29
+ continue;
30
+ }
31
+ if (request.method.startsWith("notifications/")) {
32
+ continue;
33
+ }
34
+ try {
35
+ const result = await handleRequest(client, request);
36
+ write(output, jsonRpcResult(request.id, result));
37
+ }
38
+ catch (error) {
39
+ stderr.write(`${error instanceof Error ? error.message : "Tool call failed"}\n`);
40
+ write(output, jsonRpcError(request.id, -32000, error instanceof Error ? error.message : "Tool call failed"));
41
+ }
42
+ }
43
+ }
44
+ async function handleRequest(client, request) {
45
+ if (request.method === "initialize") {
46
+ return {
47
+ protocolVersion: "2024-11-05",
48
+ capabilities: { tools: {} },
49
+ serverInfo: {
50
+ name: "recommended.by",
51
+ version: "0.1.0",
52
+ },
53
+ };
54
+ }
55
+ if (request.method === "tools/list") {
56
+ return { tools };
57
+ }
58
+ if (request.method === "tools/call") {
59
+ const params = asRecord(request.params);
60
+ const name = typeof params.name === "string" ? params.name : undefined;
61
+ if (!name) {
62
+ throw new Error("Tool name is required");
63
+ }
64
+ return callTool(client, name, params.arguments);
65
+ }
66
+ throw new Error(`Method not found: ${request.method}`);
67
+ }
68
+ function asRecord(value) {
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
70
+ return {};
71
+ }
72
+ return value;
73
+ }
74
+ function write(output, payload) {
75
+ output.write(`${JSON.stringify(payload)}\n`);
76
+ }
@@ -0,0 +1,12 @@
1
+ import { type ToolDefinition } from "./protocol.js";
2
+ export declare const tools: ToolDefinition[];
3
+ type ApiClient = {
4
+ request: (method: string, path: string, body?: unknown) => Promise<unknown>;
5
+ };
6
+ export declare function callTool(client: ApiClient, name: string, args: unknown): Promise<{
7
+ content: {
8
+ type: string;
9
+ text: string;
10
+ }[];
11
+ }>;
12
+ export {};
package/dist/tools.js ADDED
@@ -0,0 +1,201 @@
1
+ import { textResult } from "./protocol.js";
2
+ const categories = [
3
+ "movies",
4
+ "tv-shows",
5
+ "books",
6
+ "music",
7
+ "podcasts",
8
+ "travel",
9
+ "restaurants",
10
+ "products",
11
+ "tools",
12
+ "courses",
13
+ "other",
14
+ ];
15
+ const visibilities = [
16
+ "public",
17
+ "private",
18
+ "paid_subscription",
19
+ "paid_one_time",
20
+ ];
21
+ const listInputProperties = {
22
+ title: { type: "string", description: "List title." },
23
+ description: { type: "string", description: "Optional list description." },
24
+ thumbnailUrl: {
25
+ type: "string",
26
+ description: "Optional cover image URL.",
27
+ },
28
+ category: { type: "string", enum: categories },
29
+ visibility: {
30
+ type: "string",
31
+ enum: visibilities,
32
+ default: "public",
33
+ },
34
+ priceInCents: {
35
+ type: "number",
36
+ description: "Required for paid list visibility.",
37
+ },
38
+ isRanked: { type: "boolean", default: false },
39
+ published: { type: "boolean", default: true },
40
+ };
41
+ const itemInputProperties = {
42
+ title: { type: "string", description: "Recommendation title." },
43
+ subtitle: { type: "string", description: "Optional subtitle or author." },
44
+ description: { type: "string", description: "Optional recommendation note." },
45
+ url: { type: "string", description: "Optional destination URL." },
46
+ imageUrl: { type: "string", description: "Optional image URL." },
47
+ metadata: {
48
+ type: "object",
49
+ description: "Optional structured metadata for the item.",
50
+ },
51
+ };
52
+ export const tools = [
53
+ {
54
+ name: "recommended_list_lists",
55
+ description: "List all lists owned by the API key profile, including items.",
56
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
57
+ },
58
+ {
59
+ name: "recommended_create_list",
60
+ description: "Create a recommendation list for the API key profile.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: listInputProperties,
64
+ required: ["title", "category"],
65
+ additionalProperties: false,
66
+ },
67
+ },
68
+ {
69
+ name: "recommended_get_list",
70
+ description: "Fetch one owned list by id.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: { listId: { type: "string" } },
74
+ required: ["listId"],
75
+ additionalProperties: false,
76
+ },
77
+ },
78
+ {
79
+ name: "recommended_update_list",
80
+ description: "Update list metadata, visibility, pricing, or publish state.",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: { listId: { type: "string" }, ...listInputProperties },
84
+ required: ["listId"],
85
+ additionalProperties: false,
86
+ },
87
+ },
88
+ {
89
+ name: "recommended_delete_list",
90
+ description: "Delete an owned list.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: { listId: { type: "string" } },
94
+ required: ["listId"],
95
+ additionalProperties: false,
96
+ },
97
+ },
98
+ {
99
+ name: "recommended_list_items",
100
+ description: "List items in an owned list.",
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: { listId: { type: "string" } },
104
+ required: ["listId"],
105
+ additionalProperties: false,
106
+ },
107
+ },
108
+ {
109
+ name: "recommended_add_item",
110
+ description: "Add an item to a list. Missing URL or image can be auto-enriched from the list category.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: { listId: { type: "string" }, ...itemInputProperties },
114
+ required: ["listId", "title"],
115
+ additionalProperties: false,
116
+ },
117
+ },
118
+ {
119
+ name: "recommended_update_item",
120
+ description: "Update an item in an owned list.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: {
124
+ listId: { type: "string" },
125
+ itemId: { type: "string" },
126
+ ...itemInputProperties,
127
+ },
128
+ required: ["listId", "itemId"],
129
+ additionalProperties: false,
130
+ },
131
+ },
132
+ {
133
+ name: "recommended_delete_item",
134
+ description: "Delete an item from an owned list.",
135
+ inputSchema: {
136
+ type: "object",
137
+ properties: {
138
+ listId: { type: "string" },
139
+ itemId: { type: "string" },
140
+ },
141
+ required: ["listId", "itemId"],
142
+ additionalProperties: false,
143
+ },
144
+ },
145
+ ];
146
+ export async function callTool(client, name, args) {
147
+ const input = asRecord(args);
148
+ if (name === "recommended_list_lists") {
149
+ return textResult(await client.request("GET", "/lists"));
150
+ }
151
+ if (name === "recommended_create_list") {
152
+ return textResult(await client.request("POST", "/lists", withoutKeys(input)));
153
+ }
154
+ if (name === "recommended_get_list") {
155
+ return textResult(await client.request("GET", `/lists/${encodeURIComponent(getRequiredString(input, "listId"))}`));
156
+ }
157
+ if (name === "recommended_update_list") {
158
+ const listId = getRequiredString(input, "listId");
159
+ return textResult(await client.request("PATCH", `/lists/${encodeURIComponent(listId)}`, withoutKeys(input, "listId")));
160
+ }
161
+ if (name === "recommended_delete_list") {
162
+ const listId = getRequiredString(input, "listId");
163
+ return textResult(await client.request("DELETE", `/lists/${encodeURIComponent(listId)}`));
164
+ }
165
+ if (name === "recommended_list_items") {
166
+ const listId = getRequiredString(input, "listId");
167
+ return textResult(await client.request("GET", `/lists/${encodeURIComponent(listId)}/items`));
168
+ }
169
+ if (name === "recommended_add_item") {
170
+ const listId = getRequiredString(input, "listId");
171
+ return textResult(await client.request("POST", `/lists/${encodeURIComponent(listId)}/items`, withoutKeys(input, "listId")));
172
+ }
173
+ if (name === "recommended_update_item") {
174
+ const listId = getRequiredString(input, "listId");
175
+ const itemId = getRequiredString(input, "itemId");
176
+ return textResult(await client.request("PATCH", `/lists/${encodeURIComponent(listId)}/items/${encodeURIComponent(itemId)}`, withoutKeys(input, "listId", "itemId")));
177
+ }
178
+ if (name === "recommended_delete_item") {
179
+ const listId = getRequiredString(input, "listId");
180
+ const itemId = getRequiredString(input, "itemId");
181
+ return textResult(await client.request("DELETE", `/lists/${encodeURIComponent(listId)}/items/${encodeURIComponent(itemId)}`));
182
+ }
183
+ throw new Error(`Unknown tool: ${name}`);
184
+ }
185
+ function asRecord(value) {
186
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
187
+ return {};
188
+ }
189
+ return value;
190
+ }
191
+ function getRequiredString(input, key) {
192
+ const value = input[key];
193
+ if (typeof value !== "string" || !value) {
194
+ throw new Error(`${key} is required`);
195
+ }
196
+ return value;
197
+ }
198
+ function withoutKeys(input, ...keys) {
199
+ const blocked = new Set(keys);
200
+ return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined).filter(([key]) => !blocked.has(key)));
201
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "recommended-by-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Stdio MCP server for managing recommended.by lists from agents like OpenClaw, Codex, Claude, and Cursor.",
5
+ "type": "module",
6
+ "bin": {
7
+ "recommended-by-mcp": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "!dist/*.test.js",
12
+ "!dist/*.test.d.ts",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.json",
18
+ "prepack": "pnpm build",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "test": "node --test dist/*.test.js"
21
+ },
22
+ "keywords": [
23
+ "recommended.by",
24
+ "mcp",
25
+ "openclaw",
26
+ "codex",
27
+ "claude",
28
+ "cursor",
29
+ "recommendations"
30
+ ],
31
+ "author": "simple bytes",
32
+ "license": "MIT",
33
+ "homepage": "https://recommended.by/guides",
34
+ "bugs": {
35
+ "url": "https://github.com/simplebytes-com/recommended-by-mcp/issues"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/simplebytes-com/recommended-by-mcp.git"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^20",
46
+ "typescript": "^5"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }