Skip to content

Commit 887dae4

Browse files
Add NPX
1 parent b5692ff commit 887dae4

28 files changed

Lines changed: 2793 additions & 41 deletions

Packages/AdaEngine

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/Users/vlad-prusakov/Developer/AdaEngine

Packages/adamcp/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 AdaEngine
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.

Packages/adamcp/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# adamcp
2+
3+
`adamcp` is a stdio MCP router for live AdaMCP HTTP servers.
4+
5+
```json
6+
{
7+
"mcpServers": {
8+
"adamcp": {
9+
"command": "npx",
10+
"args": ["-y", "adamcp@latest"]
11+
}
12+
}
13+
}
14+
```
15+
16+
The CLI discovers running AdaEngine apps through AdaMCP discovery manifests. You can override discovery with:
17+
18+
- `ADAMCP_URL` or `--url` for a direct HTTP MCP endpoint.
19+
- `ADAMCP_SERVER` or `--server` for a manifest `id` or unique `serverName`.
20+
- `ADAMCP_DISCOVERY_DIR` or `--discovery-dir` for a custom manifest directory.
21+
- `ADAMCP_STDIO_COMMAND` or `--stdio-command` to launch an AdaEngine app/server as a child stdio MCP process.
22+
23+
When multiple live apps are found, use `adamcp.list_servers` and `adamcp.select_server` from the MCP client.

Packages/adamcp/bin/adamcp.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
import { main } from "../src/cli.js";
3+
4+
main().catch((error) => {
5+
console.error(`adamcp: ${error?.message ?? error}`);
6+
process.exitCode = 1;
7+
});

Packages/adamcp/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "adamcp",
3+
"version": "0.1.0",
4+
"description": "stdio MCP router for live AdaMCP HTTP servers",
5+
"type": "module",
6+
"bin": {
7+
"adamcp": "./bin/adamcp.js"
8+
},
9+
"engines": {
10+
"node": ">=18"
11+
},
12+
"files": [
13+
"bin",
14+
"src",
15+
"README.md",
16+
"LICENSE"
17+
],
18+
"scripts": {
19+
"test": "node --test"
20+
},
21+
"license": "MIT"
22+
}

Packages/adamcp/src/cli.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { spawn } from "node:child_process";
2+
import { AdamcpRouter } from "./router.js";
3+
import { readFrames, writeFrame } from "./stdio.js";
4+
5+
export function parseArgs(argv = process.argv.slice(2), env = process.env) {
6+
const options = {
7+
url: env.ADAMCP_URL,
8+
server: env.ADAMCP_SERVER,
9+
discoveryDir: env.ADAMCP_DISCOVERY_DIR,
10+
stdioCommand: env.ADAMCP_STDIO_COMMAND
11+
};
12+
13+
for (let index = 0; index < argv.length; index += 1) {
14+
const arg = argv[index];
15+
if (arg === "--url") {
16+
options.url = argv[++index];
17+
} else if (arg === "--server") {
18+
options.server = argv[++index];
19+
} else if (arg === "--discovery-dir") {
20+
options.discoveryDir = argv[++index];
21+
} else if (arg === "--stdio-command") {
22+
options.stdioCommand = argv[++index];
23+
} else if (arg === "--help" || arg === "-h") {
24+
options.help = true;
25+
} else {
26+
throw new Error(`Unknown argument: ${arg}`);
27+
}
28+
}
29+
30+
return options;
31+
}
32+
33+
export async function main(argv = process.argv.slice(2), streams = process) {
34+
const options = parseArgs(argv);
35+
if (options.help) {
36+
streams.stdout.write(helpText());
37+
return;
38+
}
39+
40+
if (options.stdioCommand) {
41+
await runChildStdioProxy(options.stdioCommand, streams);
42+
return;
43+
}
44+
45+
const router = new AdamcpRouter(options);
46+
const shutdown = async () => {
47+
await router.close();
48+
};
49+
process.once("beforeExit", shutdown);
50+
process.once("SIGINT", async () => {
51+
await shutdown();
52+
process.exit(130);
53+
});
54+
process.once("SIGTERM", async () => {
55+
await shutdown();
56+
process.exit(143);
57+
});
58+
59+
for await (const frame of readFrames(streams.stdin)) {
60+
let request;
61+
try {
62+
request = JSON.parse(frame.toString("utf8"));
63+
} catch {
64+
writeFrame(streams.stdout, {
65+
jsonrpc: "2.0",
66+
id: null,
67+
error: { code: -32700, message: "Parse error" }
68+
});
69+
continue;
70+
}
71+
72+
const response = await router.handle(request);
73+
if (response) {
74+
writeFrame(streams.stdout, response);
75+
}
76+
}
77+
78+
await shutdown();
79+
}
80+
81+
export async function runChildStdioProxy(command, streams = process) {
82+
const useProcessGroup = process.platform !== "win32";
83+
const child = spawn(command, {
84+
shell: true,
85+
stdio: ["pipe", "pipe", "pipe"],
86+
detached: useProcessGroup
87+
});
88+
89+
streams.stdin.pipe(child.stdin);
90+
child.stdout.pipe(streams.stdout);
91+
child.stderr.pipe(streams.stderr);
92+
93+
const terminateChild = () => {
94+
if (child.killed) {
95+
return;
96+
}
97+
try {
98+
if (useProcessGroup) {
99+
process.kill(-child.pid, "SIGTERM");
100+
} else {
101+
child.kill("SIGTERM");
102+
}
103+
} catch {
104+
// The process may have exited between the check and signal.
105+
}
106+
};
107+
process.once("exit", terminateChild);
108+
109+
await new Promise((resolve, reject) => {
110+
child.once("error", (error) => {
111+
process.off("exit", terminateChild);
112+
reject(error);
113+
});
114+
child.once("exit", (code, signal) => {
115+
process.off("exit", terminateChild);
116+
if (signal) {
117+
resolve();
118+
} else if (code === 0 || code === null) {
119+
resolve();
120+
} else {
121+
reject(new Error(`stdio command exited with code ${code}`));
122+
}
123+
});
124+
});
125+
}
126+
127+
function helpText() {
128+
return `Usage: adamcp [--url URL] [--server ID_OR_NAME] [--discovery-dir DIR]\n` +
129+
` adamcp --stdio-command COMMAND\n\n` +
130+
`Environment: ADAMCP_URL, ADAMCP_SERVER, ADAMCP_DISCOVERY_DIR, ADAMCP_STDIO_COMMAND\n`;
131+
}

Packages/adamcp/src/discovery.js

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import fs from "node:fs/promises";
2+
import os from "node:os";
3+
import path from "node:path";
4+
5+
export function defaultDiscoveryDir(env = process.env) {
6+
if (env.ADAMCP_DISCOVERY_DIR) {
7+
return env.ADAMCP_DISCOVERY_DIR;
8+
}
9+
10+
if (process.platform === "darwin") {
11+
return path.join(os.homedir(), "Library", "Application Support", "AdaMCP", "discovery");
12+
}
13+
14+
if (env.XDG_RUNTIME_DIR) {
15+
return path.join(env.XDG_RUNTIME_DIR, "AdaMCP", "discovery");
16+
}
17+
18+
return path.join(os.tmpdir(), "AdaMCP", "discovery");
19+
}
20+
21+
export async function scanManifests({ discoveryDir, now = Date.now(), removeStale = true } = {}) {
22+
const directory = discoveryDir ?? defaultDiscoveryDir();
23+
let entries;
24+
try {
25+
entries = await fs.readdir(directory, { withFileTypes: true });
26+
} catch (error) {
27+
if (error.code === "ENOENT") {
28+
return [];
29+
}
30+
throw error;
31+
}
32+
33+
const manifests = [];
34+
await Promise.all(entries.map(async (entry) => {
35+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
36+
return;
37+
}
38+
39+
const filePath = path.join(directory, entry.name);
40+
let manifest;
41+
try {
42+
manifest = JSON.parse(await fs.readFile(filePath, "utf8"));
43+
} catch {
44+
if (removeStale) {
45+
await fs.rm(filePath, { force: true });
46+
}
47+
return;
48+
}
49+
50+
if (isLiveManifest(manifest, now)) {
51+
manifests.push(normalizeManifest(manifest));
52+
} else if (removeStale) {
53+
await fs.rm(filePath, { force: true });
54+
}
55+
}));
56+
57+
return manifests.sort((lhs, rhs) => {
58+
const updatedDelta = Date.parse(rhs.updatedAt) - Date.parse(lhs.updatedAt);
59+
return updatedDelta === 0 ? lhs.id.localeCompare(rhs.id) : updatedDelta;
60+
});
61+
}
62+
63+
export function resolveServer(manifests, selector) {
64+
if (!selector) {
65+
if (manifests.length === 1) {
66+
return manifests[0];
67+
}
68+
return null;
69+
}
70+
71+
const byID = manifests.find((manifest) => manifest.id === selector);
72+
if (byID) {
73+
return byID;
74+
}
75+
76+
const byName = manifests.filter((manifest) => manifest.serverName === selector);
77+
if (byName.length === 1) {
78+
return byName[0];
79+
}
80+
if (byName.length > 1) {
81+
const error = new Error(`Multiple AdaMCP servers named '${selector}' are live; select by id.`);
82+
error.code = "AMBIGUOUS_SERVER";
83+
throw error;
84+
}
85+
86+
const error = new Error(`AdaMCP server '${selector}' was not found.`);
87+
error.code = "SERVER_NOT_FOUND";
88+
throw error;
89+
}
90+
91+
function isLiveManifest(manifest, now) {
92+
if (!manifest || manifest.schemaVersion !== 1 || !manifest.id || !manifest.endpointURL) {
93+
return false;
94+
}
95+
const updatedAt = Date.parse(manifest.updatedAt);
96+
if (!Number.isFinite(updatedAt)) {
97+
return false;
98+
}
99+
const ttl = Number(manifest.ttlSeconds);
100+
return Number.isFinite(ttl) && ttl > 0 && now - updatedAt <= ttl * 1000;
101+
}
102+
103+
function normalizeManifest(manifest) {
104+
return {
105+
id: String(manifest.id),
106+
pid: manifest.pid,
107+
processName: manifest.processName ?? "",
108+
serverName: manifest.serverName ?? "",
109+
serverVersion: manifest.serverVersion ?? "",
110+
endpointURL: String(manifest.endpointURL),
111+
startedAt: manifest.startedAt,
112+
updatedAt: manifest.updatedAt,
113+
ttlSeconds: Number(manifest.ttlSeconds),
114+
metadata: manifest.metadata ?? {}
115+
};
116+
}

0 commit comments

Comments
 (0)