appcrane-mcp 1.0.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 +118 -0
- package/catalog.json +1730 -0
- package/dist/index.js +134 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* appcrane-mcp — standalone MCP connector for AppCrane.
|
|
4
|
+
*
|
|
5
|
+
* AppCrane (https://glick.run/appcrane.html) is a self-hosted deployment
|
|
6
|
+
* platform for AI-built apps. Its MCP server lives at `<instance>/api/mcp` and
|
|
7
|
+
* requires an `X-API-Key`. Registries and sandboxes (e.g. Glama) need a
|
|
8
|
+
* standalone MCP server that starts in a container and answers `tools/list`
|
|
9
|
+
* introspection with NO configuration. This connector bridges the two:
|
|
10
|
+
*
|
|
11
|
+
* - tools/list: if APPCRANE_URL + APPCRANE_KEY are set, it connects to the
|
|
12
|
+
* live instance and returns its real tool list. If NOT configured, it
|
|
13
|
+
* returns the bundled static catalog (catalog.json) so introspection works
|
|
14
|
+
* offline with zero env vars.
|
|
15
|
+
* - tools/call: requires APPCRANE_URL + APPCRANE_KEY and proxies the call to
|
|
16
|
+
* the backend `/api/mcp`. Returns a clear error if unconfigured.
|
|
17
|
+
*
|
|
18
|
+
* Transport to the MCP client is stdio, so it runs via `npx appcrane-mcp`.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
24
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
26
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
27
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
// package.json version, resolved at runtime (dist/ sits next to package.json).
|
|
30
|
+
function readVersion() {
|
|
31
|
+
try {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
|
|
33
|
+
return pkg.version ?? '0.0.0';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return '0.0.0';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// The bundled offline catalog. Shipped in the package root, one level up from
|
|
40
|
+
// dist/. This is what makes zero-config introspection work.
|
|
41
|
+
function loadBundledCatalog() {
|
|
42
|
+
const path = join(__dirname, '..', 'catalog.json');
|
|
43
|
+
const raw = readFileSync(path, 'utf8');
|
|
44
|
+
const arr = JSON.parse(raw);
|
|
45
|
+
if (!Array.isArray(arr))
|
|
46
|
+
throw new Error('catalog.json is not an array');
|
|
47
|
+
return arr;
|
|
48
|
+
}
|
|
49
|
+
function readConfig() {
|
|
50
|
+
return {
|
|
51
|
+
url: process.env.APPCRANE_URL?.trim() || undefined,
|
|
52
|
+
key: process.env.APPCRANE_KEY?.trim() || undefined,
|
|
53
|
+
githubToken: process.env.APPCRANE_GITHUB_TOKEN?.trim() || undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function isConfigured(cfg) {
|
|
57
|
+
return Boolean(cfg.url && cfg.key);
|
|
58
|
+
}
|
|
59
|
+
const NOT_CONFIGURED_MESSAGE = 'AppCrane is not configured. Set APPCRANE_URL (e.g. https://crane.example.com) ' +
|
|
60
|
+
'and APPCRANE_KEY (your X-API-Key) to call tools against your instance. ' +
|
|
61
|
+
'Optionally set APPCRANE_GITHUB_TOKEN to enable github_* passthrough tools.';
|
|
62
|
+
// Lazily builds and connects a client to the backend AppCrane MCP endpoint.
|
|
63
|
+
// Reused across calls within a process; reconnects if the previous one closed.
|
|
64
|
+
async function connectBackend(cfg) {
|
|
65
|
+
const base = cfg.url.replace(/\/+$/, '');
|
|
66
|
+
const endpoint = new URL(`${base}/api/mcp`);
|
|
67
|
+
const headers = { 'X-API-Key': cfg.key };
|
|
68
|
+
if (cfg.githubToken)
|
|
69
|
+
headers['X-Github-Token'] = cfg.githubToken;
|
|
70
|
+
const transport = new StreamableHTTPClientTransport(endpoint, {
|
|
71
|
+
requestInit: { headers },
|
|
72
|
+
});
|
|
73
|
+
const client = new Client({ name: 'appcrane-mcp-connector', version: readVersion() }, { capabilities: {} });
|
|
74
|
+
await client.connect(transport);
|
|
75
|
+
return client;
|
|
76
|
+
}
|
|
77
|
+
async function main() {
|
|
78
|
+
const version = readVersion();
|
|
79
|
+
const server = new Server({ name: 'appcrane-mcp', version }, { capabilities: { tools: {} } });
|
|
80
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
81
|
+
const cfg = readConfig();
|
|
82
|
+
if (isConfigured(cfg)) {
|
|
83
|
+
// Live path: fetch the real, auth-filtered tool list from the instance.
|
|
84
|
+
try {
|
|
85
|
+
const client = await connectBackend(cfg);
|
|
86
|
+
try {
|
|
87
|
+
const result = await client.listTools();
|
|
88
|
+
return { tools: result.tools };
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await client.close().catch(() => { });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
// If the instance is unreachable, fall back to the bundled catalog so
|
|
96
|
+
// the connection still advertises its surface rather than failing.
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
process.stderr.write(`[appcrane-mcp] live tools/list failed (${msg}); serving bundled catalog\n`);
|
|
99
|
+
return { tools: loadBundledCatalog() };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Offline path (no config): serve the bundled static catalog. This is the
|
|
103
|
+
// path a registry sandbox hits during introspection.
|
|
104
|
+
return { tools: loadBundledCatalog() };
|
|
105
|
+
});
|
|
106
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
107
|
+
const cfg = readConfig();
|
|
108
|
+
if (!isConfigured(cfg)) {
|
|
109
|
+
return {
|
|
110
|
+
isError: true,
|
|
111
|
+
content: [{ type: 'text', text: NOT_CONFIGURED_MESSAGE }],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const client = await connectBackend(cfg);
|
|
115
|
+
try {
|
|
116
|
+
const result = await client.callTool({
|
|
117
|
+
name: request.params.name,
|
|
118
|
+
arguments: request.params.arguments ?? {},
|
|
119
|
+
});
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await client.close().catch(() => { });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
const transport = new StdioServerTransport();
|
|
127
|
+
await server.connect(transport);
|
|
128
|
+
process.stderr.write(`[appcrane-mcp] v${version} ready on stdio\n`);
|
|
129
|
+
}
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
const msg = err instanceof Error ? err.stack || err.message : String(err);
|
|
132
|
+
process.stderr.write(`[appcrane-mcp] fatal: ${msg}\n`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "appcrane-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"mcpName": "io.github.gitayg/appcrane",
|
|
5
|
+
"description": "Standalone MCP connector for AppCrane — the self-hosted deployment platform for AI-built apps. Serves the appcrane_* tool catalog for offline introspection and proxies real tool calls to your AppCrane instance.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "AppCrane",
|
|
9
|
+
"homepage": "https://glick.run/appcrane.html",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/gitayg/appcrane-mcp.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/gitayg/appcrane-mcp/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"appcrane",
|
|
21
|
+
"deployment",
|
|
22
|
+
"self-hosted",
|
|
23
|
+
"connector"
|
|
24
|
+
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"appcrane-mcp": "dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"catalog.json",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"gen:catalog": "node scripts/gen-catalog.mjs",
|
|
37
|
+
"check:catalog": "node scripts/gen-catalog.mjs --check",
|
|
38
|
+
"prepublishOnly": "npm run build",
|
|
39
|
+
"start": "node dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.19.1"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.19.0",
|
|
49
|
+
"typescript": "^5.6.0"
|
|
50
|
+
}
|
|
51
|
+
}
|