@heyclaude/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/CHANGELOG.md +9 -0
- package/README.md +132 -0
- package/package.json +81 -0
- package/scripts/validate-endpoint.mjs +200 -0
- package/src/cli-options.js +103 -0
- package/src/cli.js +30 -0
- package/src/endpoint-url.js +32 -0
- package/src/package-metadata.js +7 -0
- package/src/platforms.d.ts +9 -0
- package/src/platforms.js +86 -0
- package/src/registry.d.ts +97 -0
- package/src/registry.js +496 -0
- package/src/remote-proxy.d.ts +19 -0
- package/src/remote-proxy.js +151 -0
- package/src/schemas.d.ts +24 -0
- package/src/schemas.js +198 -0
- package/src/server.d.ts +10 -0
- package/src/server.js +52 -0
- package/src/submissions.d.ts +31 -0
- package/src/submissions.js +569 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @heyclaude/mcp Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 - Initial Public Package
|
|
4
|
+
|
|
5
|
+
- Add remote-first stdio bridge for the public HeyClaude MCP endpoint.
|
|
6
|
+
- Keep explicit local artifact mode for development and release validation.
|
|
7
|
+
- Expose read-only registry search, detail, compatibility, install guidance,
|
|
8
|
+
feed discovery, and submission-draft helper tools.
|
|
9
|
+
- Add package smoke validation for packed npm installs.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# HeyClaude MCP Server
|
|
2
|
+
|
|
3
|
+
Read-only Model Context Protocol server for the HeyClaude registry.
|
|
4
|
+
|
|
5
|
+
It exposes the same public registry surface used by the website and Raycast:
|
|
6
|
+
search, entry details, platform compatibility, install guidance, generated
|
|
7
|
+
adapters, feed discovery, and safe submission-draft helpers. It does not create
|
|
8
|
+
GitHub issues, open pull requests, write local files, publish content, or manage
|
|
9
|
+
accounts.
|
|
10
|
+
|
|
11
|
+
## Tools
|
|
12
|
+
|
|
13
|
+
- `search_registry` - search public registry entries by query, category, and
|
|
14
|
+
platform.
|
|
15
|
+
- `get_entry_detail` - fetch an entry detail payload by category and slug.
|
|
16
|
+
- `get_compatibility` - fetch skill platform compatibility metadata.
|
|
17
|
+
- `get_install_guidance` - fetch install commands, config, package, and platform
|
|
18
|
+
guidance.
|
|
19
|
+
- `get_platform_adapter` - fetch generated adapter content, currently Cursor
|
|
20
|
+
rule adapters for skill packages.
|
|
21
|
+
- `list_distribution_feeds` - discover public JSON, RSS, Atom, and platform
|
|
22
|
+
feeds.
|
|
23
|
+
- `get_submission_schema` - fetch category submission fields and issue template
|
|
24
|
+
metadata.
|
|
25
|
+
- `validate_submission_draft` - validate a content submission draft locally.
|
|
26
|
+
- `search_duplicate_entries` - check generated registry artifacts for likely
|
|
27
|
+
duplicates before opening a submission.
|
|
28
|
+
- `build_submission_urls` - build prefilled HeyClaude submit and GitHub issue
|
|
29
|
+
URLs for human review.
|
|
30
|
+
- `get_category_submission_guidance` - fetch category-specific contribution
|
|
31
|
+
guidance and required fields.
|
|
32
|
+
|
|
33
|
+
## Local Stdio
|
|
34
|
+
|
|
35
|
+
The published package defaults to the live HeyClaude MCP endpoint:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"mcpServers": {
|
|
40
|
+
"heyclaude": {
|
|
41
|
+
"command": "npx",
|
|
42
|
+
"args": ["-y", "@heyclaude/mcp"]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Use a custom endpoint when testing a preview/dev deployment:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"heyclaude": {
|
|
54
|
+
"command": "npx",
|
|
55
|
+
"args": [
|
|
56
|
+
"-y",
|
|
57
|
+
"@heyclaude/mcp",
|
|
58
|
+
"--url",
|
|
59
|
+
"https://heyclaude-dev.zeronode.workers.dev/api/mcp"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Local artifact mode is explicit and intended for development:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pnpm --filter @heyclaude/mcp start:local
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set `HEYCLAUDE_DATA_DIR=/absolute/path/to/data`, or pass
|
|
73
|
+
`--local --data-dir /absolute/path/to/data`, to point at a generated data
|
|
74
|
+
directory.
|
|
75
|
+
|
|
76
|
+
Example local MCP client config:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"heyclaude": {
|
|
82
|
+
"command": "pnpm",
|
|
83
|
+
"args": ["--filter", "@heyclaude/mcp", "start:local"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Remote HTTP
|
|
90
|
+
|
|
91
|
+
The web app also exposes a Streamable HTTP endpoint:
|
|
92
|
+
|
|
93
|
+
- production: `https://heyclau.de/api/mcp`
|
|
94
|
+
- dev: `https://heyclaude-dev.zeronode.workers.dev/api/mcp`
|
|
95
|
+
|
|
96
|
+
Validate a deployed endpoint with the SDK-level contract check:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
MCP_ENDPOINT_URL=https://heyclaude-dev.zeronode.workers.dev/api/mcp pnpm validate:mcp-endpoint
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
This check connects with an MCP client, lists tools, calls representative
|
|
103
|
+
registry and submission-helper tools, verifies strict argument validation, and
|
|
104
|
+
checks the HTTP guards used by the remote route.
|
|
105
|
+
|
|
106
|
+
## Security Boundary
|
|
107
|
+
|
|
108
|
+
- Read-only registry artifacts only.
|
|
109
|
+
- Submission helpers generate URLs and validation reports only.
|
|
110
|
+
- No GitHub OAuth, tokens, issue creation, PR creation, or repo writes.
|
|
111
|
+
- No local project-file writes or config mutations.
|
|
112
|
+
- Remote endpoint uses route-level rate limits and Cloudflare rate-limit bindings
|
|
113
|
+
when available.
|
|
114
|
+
|
|
115
|
+
## npm Release Prep
|
|
116
|
+
|
|
117
|
+
MCP releases are package-scoped. Website/catalog changes do not create repo-wide
|
|
118
|
+
semver releases. The initial public package version is `0.1.0`, and GitHub
|
|
119
|
+
release tags use `mcp-vX.Y.Z`.
|
|
120
|
+
|
|
121
|
+
Do not publish until the web branch has shipped, the production endpoint has
|
|
122
|
+
been verified, and the package smoke test passes. The release checklist is:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
pnpm validate:mcp-endpoint -- --url https://heyclau.de/api/mcp
|
|
126
|
+
pnpm --filter @heyclaude/mcp test
|
|
127
|
+
pnpm --filter @heyclaude/mcp pack --dry-run
|
|
128
|
+
MCP_PACKAGE_REMOTE_SMOKE_URL=https://heyclau.de/api/mcp pnpm validate:mcp-package
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Publishing should happen through the manual `Publish MCP Package` GitHub
|
|
132
|
+
workflow with npm trusted publishing/provenance enabled for `@heyclaude/mcp`.
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heyclaude/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Read-only MCP server for the HeyClaude registry.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "./src/registry.js",
|
|
8
|
+
"types": "./src/registry.d.ts",
|
|
9
|
+
"homepage": "https://heyclau.de",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/JSONbored/claudepro-directory.git",
|
|
13
|
+
"directory": "packages/mcp"
|
|
14
|
+
},
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/JSONbored/claudepro-directory/issues"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"heyclaude",
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"claude",
|
|
23
|
+
"ai-registry",
|
|
24
|
+
"agents",
|
|
25
|
+
"skills"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"README.md",
|
|
30
|
+
"scripts/**/*.mjs",
|
|
31
|
+
"src/**/*.d.ts",
|
|
32
|
+
"src/**/*.js"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./src/registry.d.ts",
|
|
37
|
+
"default": "./src/registry.js"
|
|
38
|
+
},
|
|
39
|
+
"./registry": {
|
|
40
|
+
"types": "./src/registry.d.ts",
|
|
41
|
+
"default": "./src/registry.js"
|
|
42
|
+
},
|
|
43
|
+
"./server": {
|
|
44
|
+
"types": "./src/server.d.ts",
|
|
45
|
+
"default": "./src/server.js"
|
|
46
|
+
},
|
|
47
|
+
"./remote-proxy": {
|
|
48
|
+
"types": "./src/remote-proxy.d.ts",
|
|
49
|
+
"default": "./src/remote-proxy.js"
|
|
50
|
+
},
|
|
51
|
+
"./platforms": {
|
|
52
|
+
"types": "./src/platforms.d.ts",
|
|
53
|
+
"default": "./src/platforms.js"
|
|
54
|
+
},
|
|
55
|
+
"./schemas": {
|
|
56
|
+
"types": "./src/schemas.d.ts",
|
|
57
|
+
"default": "./src/schemas.js"
|
|
58
|
+
},
|
|
59
|
+
"./submissions": {
|
|
60
|
+
"types": "./src/submissions.d.ts",
|
|
61
|
+
"default": "./src/submissions.js"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"bin": {
|
|
65
|
+
"heyclaude-mcp": "src/cli.js"
|
|
66
|
+
},
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": ">=20"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"start": "node src/cli.js",
|
|
72
|
+
"start:local": "node src/cli.js --local --data-dir ../../apps/web/public/data",
|
|
73
|
+
"validate:endpoint": "node scripts/validate-endpoint.mjs",
|
|
74
|
+
"validate:package": "node ../../scripts/validate-mcp-package.mjs",
|
|
75
|
+
"test": "pnpm --dir ../.. exec vitest run tests/mcp-server.test.ts tests/mcp-cli.test.ts"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
79
|
+
"zod": "4.4.3"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
5
|
+
|
|
6
|
+
import { normalizeEndpointUrl } from "../src/endpoint-url.js";
|
|
7
|
+
import { READ_ONLY_TOOL_NAMES } from "../src/registry.js";
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
const args = new Map();
|
|
11
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
12
|
+
const value = argv[index];
|
|
13
|
+
if (value === "--") continue;
|
|
14
|
+
if (!value.startsWith("--")) continue;
|
|
15
|
+
args.set(value.slice(2), argv[index + 1] ?? "");
|
|
16
|
+
index += 1;
|
|
17
|
+
}
|
|
18
|
+
return args;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseToolResult(result) {
|
|
22
|
+
const text = result?.content?.find((item) => item?.type === "text")?.text;
|
|
23
|
+
if (!text) throw new Error("MCP tool response did not include text content.");
|
|
24
|
+
return JSON.parse(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assert(condition, message) {
|
|
28
|
+
if (!condition) throw new Error(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function validateHttpGuards(endpointUrl) {
|
|
32
|
+
const options = await fetch(endpointUrl, { method: "OPTIONS" });
|
|
33
|
+
assert(options.status === 204, `OPTIONS returned ${options.status}`);
|
|
34
|
+
assert(
|
|
35
|
+
String(options.headers.get("access-control-allow-methods") || "").includes(
|
|
36
|
+
"POST",
|
|
37
|
+
),
|
|
38
|
+
"OPTIONS did not expose POST in access-control-allow-methods.",
|
|
39
|
+
);
|
|
40
|
+
assert(
|
|
41
|
+
options.headers.get("cache-control") === "no-store",
|
|
42
|
+
"OPTIONS did not return cache-control: no-store.",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const invalidContentType = await fetch(endpointUrl, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
accept: "application/json, text/event-stream",
|
|
49
|
+
"content-type": "text/plain",
|
|
50
|
+
},
|
|
51
|
+
body: "{}",
|
|
52
|
+
});
|
|
53
|
+
assert(
|
|
54
|
+
invalidContentType.status === 415 || invalidContentType.status === 403,
|
|
55
|
+
`text/plain POST returned ${invalidContentType.status}`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const forbiddenOrigin = await fetch(endpointUrl, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
accept: "application/json, text/event-stream",
|
|
62
|
+
"content-type": "application/json",
|
|
63
|
+
origin: "https://example.invalid",
|
|
64
|
+
},
|
|
65
|
+
body: JSON.stringify({
|
|
66
|
+
jsonrpc: "2.0",
|
|
67
|
+
id: "forbidden-origin",
|
|
68
|
+
method: "tools/list",
|
|
69
|
+
params: {},
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
assert(
|
|
73
|
+
forbiddenOrigin.status === 403,
|
|
74
|
+
`forbidden origin POST returned ${forbiddenOrigin.status}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function validateMcpTools(endpointUrl) {
|
|
79
|
+
const client = new Client({
|
|
80
|
+
name: "heyclaude-endpoint-validator",
|
|
81
|
+
version: "0.1.0",
|
|
82
|
+
});
|
|
83
|
+
const transport = new StreamableHTTPClientTransport(endpointUrl);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await client.connect(transport);
|
|
87
|
+
|
|
88
|
+
const tools = await client.listTools();
|
|
89
|
+
const toolNames = tools.tools.map((tool) => tool.name);
|
|
90
|
+
assert(
|
|
91
|
+
JSON.stringify(toolNames) === JSON.stringify(READ_ONLY_TOOL_NAMES),
|
|
92
|
+
`Unexpected tool list: ${toolNames.join(", ")}`,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const search = parseToolResult(
|
|
96
|
+
await client.callTool({
|
|
97
|
+
name: "search_registry",
|
|
98
|
+
arguments: { query: "mcp", limit: 2 },
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
assert(search.ok === true, "search_registry did not return ok: true.");
|
|
102
|
+
assert(
|
|
103
|
+
Array.isArray(search.entries) && search.entries.length > 0,
|
|
104
|
+
"search_registry did not return entries.",
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
const first = search.entries[0];
|
|
108
|
+
const detail = parseToolResult(
|
|
109
|
+
await client.callTool({
|
|
110
|
+
name: "get_entry_detail",
|
|
111
|
+
arguments: { category: first.category, slug: first.slug },
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
assert(detail.ok === true, "get_entry_detail did not return ok: true.");
|
|
115
|
+
assert(
|
|
116
|
+
detail.key === `${first.category}:${first.slug}`,
|
|
117
|
+
"get_entry_detail returned the wrong entry.",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const feeds = parseToolResult(
|
|
121
|
+
await client.callTool({
|
|
122
|
+
name: "list_distribution_feeds",
|
|
123
|
+
arguments: {},
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
assert(feeds.ok === true, "list_distribution_feeds did not return ok.");
|
|
127
|
+
assert(
|
|
128
|
+
feeds.artifacts?.directory === "/data/directory-index.json",
|
|
129
|
+
"list_distribution_feeds did not expose the directory artifact.",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const schema = parseToolResult(
|
|
133
|
+
await client.callTool({
|
|
134
|
+
name: "get_submission_schema",
|
|
135
|
+
arguments: { category: "mcp" },
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
assert(schema.ok === true, "get_submission_schema did not return ok.");
|
|
139
|
+
assert(
|
|
140
|
+
schema.issueTemplate?.template === "submit-mcp.yml",
|
|
141
|
+
"get_submission_schema did not return the MCP issue template.",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const urls = parseToolResult(
|
|
145
|
+
await client.callTool({
|
|
146
|
+
name: "build_submission_urls",
|
|
147
|
+
arguments: {
|
|
148
|
+
fields: {
|
|
149
|
+
category: "mcp",
|
|
150
|
+
name: "Endpoint Validation MCP",
|
|
151
|
+
docs_url: "https://example.com/docs",
|
|
152
|
+
description:
|
|
153
|
+
"Endpoint validation draft for the HeyClaude MCP submission helpers.",
|
|
154
|
+
install_command: "npx -y endpoint-validation-mcp",
|
|
155
|
+
usage_snippet: "Use this draft to validate MCP route behavior.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
);
|
|
160
|
+
assert(urls.ok === true, "build_submission_urls did not return ok.");
|
|
161
|
+
assert(
|
|
162
|
+
String(urls.githubIssueUrl || "").includes("template=submit-mcp.yml"),
|
|
163
|
+
"build_submission_urls did not return an MCP issue URL.",
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const invalid = parseToolResult(
|
|
167
|
+
await client.callTool({
|
|
168
|
+
name: "search_registry",
|
|
169
|
+
arguments: { limit: 100, unexpected: true },
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
assert(invalid.ok === false, "Invalid search_registry call did not fail.");
|
|
173
|
+
assert(
|
|
174
|
+
invalid.error?.code === "invalid_request",
|
|
175
|
+
"Invalid search_registry call did not return invalid_request.",
|
|
176
|
+
);
|
|
177
|
+
} finally {
|
|
178
|
+
await client.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const args = parseArgs(process.argv.slice(2));
|
|
183
|
+
const endpointUrlRaw = args.get("url") || process.env.MCP_ENDPOINT_URL;
|
|
184
|
+
const endpointUrl = endpointUrlRaw ? normalizeEndpointUrl(endpointUrlRaw) : "";
|
|
185
|
+
|
|
186
|
+
if (!endpointUrl) {
|
|
187
|
+
console.error(
|
|
188
|
+
"Missing --url or MCP_ENDPOINT_URL for MCP endpoint validation.",
|
|
189
|
+
);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
await validateHttpGuards(endpointUrl);
|
|
195
|
+
await validateMcpTools(endpointUrl);
|
|
196
|
+
console.log(`Validated HeyClaude MCP endpoint at ${endpointUrl.toString()}`);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_REMOTE_MCP_URL,
|
|
3
|
+
DEFAULT_REQUEST_TIMEOUT_MS,
|
|
4
|
+
normalizeEndpointUrl,
|
|
5
|
+
normalizeTimeoutMs,
|
|
6
|
+
} from "./endpoint-url.js";
|
|
7
|
+
import { packageName, packageVersion } from "./package-metadata.js";
|
|
8
|
+
|
|
9
|
+
const helpText = `${packageName} ${packageVersion}
|
|
10
|
+
|
|
11
|
+
Read-only stdio MCP bridge for the HeyClaude registry.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
heyclaude-mcp [--url <endpoint>] [--timeout-ms <ms>]
|
|
15
|
+
heyclaude-mcp --local --data-dir <path>
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--url <endpoint> Remote Streamable HTTP MCP endpoint.
|
|
19
|
+
Defaults to ${DEFAULT_REMOTE_MCP_URL}
|
|
20
|
+
--local Run the local artifact-backed MCP server.
|
|
21
|
+
--data-dir <path> Generated registry data directory for local mode.
|
|
22
|
+
--timeout-ms <ms> Remote request timeout. Default ${DEFAULT_REQUEST_TIMEOUT_MS}.
|
|
23
|
+
--version, -v Print package version.
|
|
24
|
+
--help, -h Print this help text.
|
|
25
|
+
|
|
26
|
+
Environment:
|
|
27
|
+
HEYCLAUDE_MCP_URL Remote MCP endpoint override.
|
|
28
|
+
HEYCLAUDE_MCP_TIMEOUT_MS Remote request timeout override.
|
|
29
|
+
HEYCLAUDE_DATA_DIR Local data directory; enables local mode.
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
function readFlagValue(argv, index, flag) {
|
|
33
|
+
const value = argv[index + 1];
|
|
34
|
+
if (!value || value.startsWith("-")) {
|
|
35
|
+
throw new Error(`${flag} requires a value.`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderHelp() {
|
|
41
|
+
return helpText;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseCliArgs(argv = [], env = process.env) {
|
|
45
|
+
const options = {
|
|
46
|
+
mode: "remote",
|
|
47
|
+
url: env.HEYCLAUDE_MCP_URL || DEFAULT_REMOTE_MCP_URL,
|
|
48
|
+
dataDir: env.HEYCLAUDE_DATA_DIR || "",
|
|
49
|
+
timeoutMs: normalizeTimeoutMs(env.HEYCLAUDE_MCP_TIMEOUT_MS),
|
|
50
|
+
help: false,
|
|
51
|
+
version: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (options.dataDir) {
|
|
55
|
+
options.mode = "local";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
59
|
+
const arg = argv[index];
|
|
60
|
+
if (arg === "--") continue;
|
|
61
|
+
if (arg === "--help" || arg === "-h") {
|
|
62
|
+
options.help = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (arg === "--version" || arg === "-v") {
|
|
66
|
+
options.version = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === "--local") {
|
|
70
|
+
options.mode = "local";
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg === "--url") {
|
|
74
|
+
options.url = readFlagValue(argv, index, arg);
|
|
75
|
+
index += 1;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--data-dir") {
|
|
79
|
+
options.dataDir = readFlagValue(argv, index, arg);
|
|
80
|
+
options.mode = "local";
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === "--timeout-ms") {
|
|
85
|
+
options.timeoutMs = normalizeTimeoutMs(readFlagValue(argv, index, arg));
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (options.help || options.version) return options;
|
|
93
|
+
|
|
94
|
+
if (options.mode === "local") {
|
|
95
|
+
if (!options.dataDir) {
|
|
96
|
+
throw new Error("Local mode requires --data-dir or HEYCLAUDE_DATA_DIR.");
|
|
97
|
+
}
|
|
98
|
+
return options;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
options.url = normalizeEndpointUrl(options.url);
|
|
102
|
+
return options;
|
|
103
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseCliArgs, renderHelp } from "./cli-options.js";
|
|
3
|
+
import { packageVersion } from "./package-metadata.js";
|
|
4
|
+
import { runRemoteStdioProxy } from "./remote-proxy.js";
|
|
5
|
+
import { runStdioServer } from "./server.js";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
const options = parseCliArgs(process.argv.slice(2), process.env);
|
|
9
|
+
if (options.help) {
|
|
10
|
+
console.log(renderHelp());
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (options.version) {
|
|
14
|
+
console.log(packageVersion);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (options.mode === "local") {
|
|
18
|
+
await runStdioServer({ dataDir: options.dataDir });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
await runRemoteStdioProxy({
|
|
22
|
+
url: options.url,
|
|
23
|
+
timeoutMs: options.timeoutMs,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch((error) => {
|
|
28
|
+
console.error(error instanceof Error ? error.message : error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export const DEFAULT_REMOTE_MCP_URL = "https://heyclau.de/api/mcp";
|
|
2
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
3
|
+
|
|
4
|
+
const localHosts = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
5
|
+
|
|
6
|
+
export function normalizeEndpointUrl(value = DEFAULT_REMOTE_MCP_URL) {
|
|
7
|
+
const raw = String(value || "").trim();
|
|
8
|
+
if (!raw) throw new Error("MCP endpoint URL is required.");
|
|
9
|
+
|
|
10
|
+
const url = new URL(raw);
|
|
11
|
+
if (url.protocol !== "https:" && !localHosts.has(url.hostname)) {
|
|
12
|
+
throw new Error("MCP endpoint URL must use HTTPS outside localhost.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
16
|
+
url.pathname = "/api/mcp";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizeTimeoutMs(
|
|
23
|
+
value,
|
|
24
|
+
fallback = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
25
|
+
) {
|
|
26
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
27
|
+
const numeric = Number(value);
|
|
28
|
+
if (!Number.isFinite(numeric) || numeric < 1000 || numeric > 300000) {
|
|
29
|
+
throw new Error("Timeout must be between 1000 and 300000 milliseconds.");
|
|
30
|
+
}
|
|
31
|
+
return Math.trunc(numeric);
|
|
32
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const packageJson = require("../package.json");
|
|
5
|
+
|
|
6
|
+
export const packageName = String(packageJson.name || "@heyclaude/mcp");
|
|
7
|
+
export const packageVersion = String(packageJson.version || "0.0.0");
|
package/src/platforms.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export const SITE_URL = "https://heyclau.de";
|
|
2
|
+
|
|
3
|
+
function slugPart(value, options = {}) {
|
|
4
|
+
const text = String(value || "")
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase();
|
|
7
|
+
let output = "";
|
|
8
|
+
let lastWasSeparator = false;
|
|
9
|
+
|
|
10
|
+
for (const char of text) {
|
|
11
|
+
const isAlphaNumeric =
|
|
12
|
+
(char >= "a" && char <= "z") || (char >= "0" && char <= "9");
|
|
13
|
+
if (isAlphaNumeric) {
|
|
14
|
+
output += char;
|
|
15
|
+
lastWasSeparator = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === "&" && options.expandAmpersand) {
|
|
19
|
+
if (output && !lastWasSeparator) output += "-";
|
|
20
|
+
output += "and";
|
|
21
|
+
lastWasSeparator = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (output && !lastWasSeparator) {
|
|
25
|
+
output += "-";
|
|
26
|
+
lastWasSeparator = true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return lastWasSeparator ? output.slice(0, -1) : output;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function platformFeedSlug(platform) {
|
|
34
|
+
return slugPart(platform, { expandAmpersand: true });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildSkillPlatformCompatibility(entry) {
|
|
38
|
+
if (entry?.category !== "skills") return [];
|
|
39
|
+
if (Array.isArray(entry.platformCompatibility)) {
|
|
40
|
+
return entry.platformCompatibility;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const slug = String(entry?.slug || "").trim();
|
|
44
|
+
return [
|
|
45
|
+
{
|
|
46
|
+
platform: "Claude",
|
|
47
|
+
support: "native-skill",
|
|
48
|
+
artifact: "SKILL.md package",
|
|
49
|
+
installHint: "Install the skill package into your Claude skills folder.",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
platform: "Codex",
|
|
53
|
+
support: "native-skill",
|
|
54
|
+
artifact: "SKILL.md package",
|
|
55
|
+
installHint: "Install the skill package into your Codex skills folder.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
platform: "Windsurf",
|
|
59
|
+
support: "native-skill",
|
|
60
|
+
artifact: "SKILL.md package",
|
|
61
|
+
installHint: "Install the skill under .windsurf/skills/<skill-name>/.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
platform: "Gemini",
|
|
65
|
+
support: "native-skill",
|
|
66
|
+
artifact: "SKILL.md package",
|
|
67
|
+
installHint:
|
|
68
|
+
"Use the skill package with Gemini CLI extension skill support.",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
platform: "Cursor",
|
|
72
|
+
support: "adapter",
|
|
73
|
+
artifact: `.cursor/rules/${slug}.mdc`,
|
|
74
|
+
adapterUrl: `/data/skill-adapters/cursor/${slug}.mdc`,
|
|
75
|
+
installHint:
|
|
76
|
+
"Use the generated Cursor rule adapter because Cursor rules are the supported reusable instruction surface.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
platform: "Generic AGENTS",
|
|
80
|
+
support: "manual-context",
|
|
81
|
+
artifact: "SKILL.md package",
|
|
82
|
+
installHint:
|
|
83
|
+
"Use SKILL.md as reusable agent context or convert it into the target tool's instruction file.",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
}
|