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 +21 -0
- package/README.md +114 -0
- package/dist/auth.d.ts +16 -0
- package/dist/auth.js +63 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +96 -0
- package/dist/client.d.ts +8 -0
- package/dist/client.js +43 -0
- package/dist/protocol.d.ts +36 -0
- package/dist/protocol.js +16 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +76 -0
- package/dist/tools.d.ts +12 -0
- package/dist/tools.js +201 -0
- package/package.json +51 -0
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
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
|
+
});
|
package/dist/client.d.ts
ADDED
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
|
+
};
|
package/dist/protocol.js
ADDED
|
@@ -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
|
+
}
|
package/dist/server.d.ts
ADDED
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
|
+
}
|
package/dist/tools.d.ts
ADDED
|
@@ -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
|
+
}
|