@alfe.ai/mcp-server 0.2.5 → 0.2.6

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/README.md CHANGED
@@ -10,6 +10,36 @@ Part of [**Alfe**](https://alfe.ai) — the operating system for AI agents: buil
10
10
  npm install @alfe.ai/mcp-server
11
11
  ```
12
12
 
13
+ ## Profiles
14
+
15
+ The executable defaults to the OpenClaw-safe integrations-only surface:
16
+
17
+ ```bash
18
+ alfe-mcp-server
19
+ ```
20
+
21
+ Claude Code has no Alfe plugins, so its explicit profile adds memory, voice,
22
+ and messaging tools:
23
+
24
+ ```bash
25
+ alfe-mcp-server --profile claude-code
26
+ ```
27
+
28
+ The default profile must remain integrations-only. OpenClaw already receives
29
+ the other capabilities from its native Alfe plugins, and exposing them here as
30
+ well would duplicate its tool surface.
31
+
32
+ The server reads credentials through `@alfe.ai/config`. Library callers may
33
+ inject a pre-bound `AgentApiClient`, its API URL, and an identity into
34
+ `createServer` for tests or alternate hosts. The main client and URL must be
35
+ provided together. A distinct `voiceApiUrl` also requires a `voiceClient`; the
36
+ package never reads the local API key and attaches it to an arbitrary injected
37
+ destination.
38
+
39
+ `main()` attaches a transport and returns the connected server without changing
40
+ process state. The packaged executable owns fatal logging, exit codes, and
41
+ SIGINT/SIGTERM shutdown.
42
+
13
43
  ## Links
14
44
 
15
45
  - 🌐 Website: <https://alfe.ai>
package/dist/bin.cjs CHANGED
@@ -1,5 +1,51 @@
1
1
  #!/usr/bin/env node
2
2
  const require_index = require("./index.cjs");
3
+ //#region src/cli.ts
4
+ const MAX_ERROR_CHARS = 4096;
5
+ /**
6
+ * Own the executable-only process policy around the reusable MCP server.
7
+ * Startup failures set a non-zero exit code after writing a redacted diagnostic;
8
+ * SIGINT/SIGTERM close the transport before terminating the child process.
9
+ */
10
+ async function runCli(argv, dependencies = {}, runtime = process) {
11
+ const start = dependencies.start ?? require_index.main;
12
+ let server;
13
+ try {
14
+ server = await start({ profile: require_index.parseProfileArg(argv) });
15
+ } catch (error) {
16
+ runtime.stderr.write(`[alfe-mcp-server] failed to start: ${safeErrorMessage(error)}\n`);
17
+ runtime.exitCode = 1;
18
+ return;
19
+ }
20
+ let shutdownPromise;
21
+ const shutdown = () => {
22
+ if (shutdownPromise) return;
23
+ runtime.off("SIGINT", shutdown);
24
+ runtime.off("SIGTERM", shutdown);
25
+ shutdownPromise = server.close().catch((error) => {
26
+ runtime.stderr.write(`[alfe-mcp-server] failed to close cleanly: ${safeErrorMessage(error)}\n`);
27
+ }).then(() => {
28
+ runtime.exit(0);
29
+ });
30
+ };
31
+ runtime.once("SIGINT", shutdown);
32
+ runtime.once("SIGTERM", shutdown);
33
+ }
34
+ function safeErrorMessage(error) {
35
+ const redacted = (error instanceof Error ? error.message : String(error)).slice(0, MAX_ERROR_CHARS).replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/giu, "$1 [REDACTED]").replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[=:]\s*[^\s,;]+/giu, "$1=[REDACTED]").replace(/\balfe_[A-Za-z0-9_-]{8,}/gu, "[REDACTED]").replace(/:\/\/[^:/@\s]+:[^@\s]+@/gu, "://[REDACTED]@");
36
+ let flattened = "";
37
+ let previousWasControl = false;
38
+ for (const character of redacted) {
39
+ const codePoint = character.codePointAt(0) ?? 0;
40
+ const isControl = codePoint < 32 || codePoint === 127;
41
+ if (isControl) {
42
+ if (!previousWasControl) flattened += " ";
43
+ } else flattened += character;
44
+ previousWasControl = isControl;
45
+ }
46
+ return flattened;
47
+ }
48
+ //#endregion
3
49
  //#region src/bin.ts
4
- require_index.main({ profile: require_index.parseProfileArg(process.argv.slice(2)) });
50
+ runCli(process.argv.slice(2));
5
51
  //#endregion
package/dist/bin.js CHANGED
@@ -1,7 +1,53 @@
1
1
  #!/usr/bin/env node
2
2
  import { main, parseProfileArg } from "./index.js";
3
+ //#region src/cli.ts
4
+ const MAX_ERROR_CHARS = 4096;
5
+ /**
6
+ * Own the executable-only process policy around the reusable MCP server.
7
+ * Startup failures set a non-zero exit code after writing a redacted diagnostic;
8
+ * SIGINT/SIGTERM close the transport before terminating the child process.
9
+ */
10
+ async function runCli(argv, dependencies = {}, runtime = process) {
11
+ const start = dependencies.start ?? main;
12
+ let server;
13
+ try {
14
+ server = await start({ profile: parseProfileArg(argv) });
15
+ } catch (error) {
16
+ runtime.stderr.write(`[alfe-mcp-server] failed to start: ${safeErrorMessage(error)}\n`);
17
+ runtime.exitCode = 1;
18
+ return;
19
+ }
20
+ let shutdownPromise;
21
+ const shutdown = () => {
22
+ if (shutdownPromise) return;
23
+ runtime.off("SIGINT", shutdown);
24
+ runtime.off("SIGTERM", shutdown);
25
+ shutdownPromise = server.close().catch((error) => {
26
+ runtime.stderr.write(`[alfe-mcp-server] failed to close cleanly: ${safeErrorMessage(error)}\n`);
27
+ }).then(() => {
28
+ runtime.exit(0);
29
+ });
30
+ };
31
+ runtime.once("SIGINT", shutdown);
32
+ runtime.once("SIGTERM", shutdown);
33
+ }
34
+ function safeErrorMessage(error) {
35
+ const redacted = (error instanceof Error ? error.message : String(error)).slice(0, MAX_ERROR_CHARS).replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/giu, "$1 [REDACTED]").replace(/\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\s*[=:]\s*[^\s,;]+/giu, "$1=[REDACTED]").replace(/\balfe_[A-Za-z0-9_-]{8,}/gu, "[REDACTED]").replace(/:\/\/[^:/@\s]+:[^@\s]+@/gu, "://[REDACTED]@");
36
+ let flattened = "";
37
+ let previousWasControl = false;
38
+ for (const character of redacted) {
39
+ const codePoint = character.codePointAt(0) ?? 0;
40
+ const isControl = codePoint < 32 || codePoint === 127;
41
+ if (isControl) {
42
+ if (!previousWasControl) flattened += " ";
43
+ } else flattened += character;
44
+ previousWasControl = isControl;
45
+ }
46
+ return flattened;
47
+ }
48
+ //#endregion
3
49
  //#region src/bin.ts
4
- main({ profile: parseProfileArg(process.argv.slice(2)) });
50
+ runCli(process.argv.slice(2));
5
51
  //#endregion
6
52
  export {};
7
53
 
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { main, parseProfileArg } from './index.js';\n\n// The OpenClaw daemon bundler launches this bin with no args → `default`\n// profile (integrations only, unchanged). The Alfe CLI launches it as\n// `node <bin> --profile claude-code` for a Claude Code session → memory +\n// voice + messaging tools on top.\nvoid main({ profile: parseProfileArg(process.argv.slice(2)) });\n"],"mappings":";;;AAOK,KAAK,EAAE,SAAS,gBAAgB,QAAQ,KAAK,MAAM,EAAE,CAAC,EAAE,CAAC"}
1
+ {"version":3,"file":"bin.js","names":[],"sources":["../src/cli.ts","../src/bin.ts"],"sourcesContent":["import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { main, parseProfileArg } from './index.js';\n\nconst MAX_ERROR_CHARS = 4096;\n\ninterface CliRuntime {\n stderr: { write(message: string): unknown };\n once(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;\n off(signal: 'SIGINT' | 'SIGTERM', listener: () => void): unknown;\n exit(code?: number): unknown;\n exitCode?: string | number | null;\n}\n\ninterface CliDependencies {\n start?: typeof main;\n}\n\n/**\n * Own the executable-only process policy around the reusable MCP server.\n * Startup failures set a non-zero exit code after writing a redacted diagnostic;\n * SIGINT/SIGTERM close the transport before terminating the child process.\n */\nexport async function runCli(\n argv: string[],\n dependencies: CliDependencies = {},\n runtime: CliRuntime = process,\n): Promise<void> {\n const start = dependencies.start ?? main;\n let server: McpServer;\n try {\n server = await start({ profile: parseProfileArg(argv) });\n } catch (error: unknown) {\n runtime.stderr.write(`[alfe-mcp-server] failed to start: ${safeErrorMessage(error)}\\n`);\n runtime.exitCode = 1;\n return;\n }\n\n let shutdownPromise: Promise<void> | undefined;\n const shutdown = (): void => {\n if (shutdownPromise) return;\n runtime.off('SIGINT', shutdown);\n runtime.off('SIGTERM', shutdown);\n shutdownPromise = server.close()\n .catch((error: unknown) => {\n runtime.stderr.write(`[alfe-mcp-server] failed to close cleanly: ${safeErrorMessage(error)}\\n`);\n })\n .then(() => {\n runtime.exit(0);\n });\n };\n runtime.once('SIGINT', shutdown);\n runtime.once('SIGTERM', shutdown);\n}\n\nexport function safeErrorMessage(error: unknown): string {\n const redacted = (error instanceof Error ? error.message : String(error))\n .slice(0, MAX_ERROR_CHARS)\n .replace(/\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/=-]+/giu, '$1 [REDACTED]')\n .replace(\n /\\b(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password)\\s*[=:]\\s*[^\\s,;]+/giu,\n '$1=[REDACTED]',\n )\n .replace(/\\balfe_[A-Za-z0-9_-]{8,}/gu, '[REDACTED]')\n .replace(/:\\/\\/[^:/@\\s]+:[^@\\s]+@/gu, '://[REDACTED]@');\n let flattened = '';\n let previousWasControl = false;\n for (const character of redacted) {\n const codePoint = character.codePointAt(0) ?? 0;\n const isControl = codePoint < 32 || codePoint === 127;\n if (isControl) {\n if (!previousWasControl) flattened += ' ';\n } else {\n flattened += character;\n }\n previousWasControl = isControl;\n }\n return flattened;\n}\n","#!/usr/bin/env node\nimport { runCli } from './cli.js';\n\n// The OpenClaw daemon bundler launches this bin with no args → `default`\n// profile (integrations only, unchanged). The Alfe CLI launches it as\n// `node <bin> --profile claude-code` for a Claude Code session → memory +\n// voice + messaging tools on top.\nvoid runCli(process.argv.slice(2));\n"],"mappings":";;;AAGA,MAAM,kBAAkB;;;;;;AAmBxB,eAAsB,OACpB,MACA,eAAgC,EAAE,EAClC,UAAsB,SACP;CACf,MAAM,QAAQ,aAAa,SAAS;CACpC,IAAI;AACJ,KAAI;AACF,WAAS,MAAM,MAAM,EAAE,SAAS,gBAAgB,KAAK,EAAE,CAAC;UACjD,OAAgB;AACvB,UAAQ,OAAO,MAAM,sCAAsC,iBAAiB,MAAM,CAAC,IAAI;AACvF,UAAQ,WAAW;AACnB;;CAGF,IAAI;CACJ,MAAM,iBAAuB;AAC3B,MAAI,gBAAiB;AACrB,UAAQ,IAAI,UAAU,SAAS;AAC/B,UAAQ,IAAI,WAAW,SAAS;AAChC,oBAAkB,OAAO,OAAO,CAC7B,OAAO,UAAmB;AACzB,WAAQ,OAAO,MAAM,8CAA8C,iBAAiB,MAAM,CAAC,IAAI;IAC/F,CACD,WAAW;AACV,WAAQ,KAAK,EAAE;IACf;;AAEN,SAAQ,KAAK,UAAU,SAAS;AAChC,SAAQ,KAAK,WAAW,SAAS;;AAGnC,SAAgB,iBAAiB,OAAwB;CACvD,MAAM,YAAY,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,EACrE,MAAM,GAAG,gBAAgB,CACzB,QAAQ,6CAA6C,gBAAgB,CACrE,QACC,2FACA,gBACD,CACA,QAAQ,8BAA8B,aAAa,CACnD,QAAQ,6BAA6B,iBAAiB;CACzD,IAAI,YAAY;CAChB,IAAI,qBAAqB;AACzB,MAAK,MAAM,aAAa,UAAU;EAChC,MAAM,YAAY,UAAU,YAAY,EAAE,IAAI;EAC9C,MAAM,YAAY,YAAY,MAAM,cAAc;AAClD,MAAI;OACE,CAAC,mBAAoB,cAAa;QAEtC,cAAa;AAEf,uBAAqB;;AAEvB,QAAO;;;;ACrEJ,OAAO,QAAQ,KAAK,MAAM,EAAE,CAAC"}
package/dist/index.cjs CHANGED
@@ -8,6 +8,8 @@ let _alfe_ai_config = require("@alfe.ai/config");
8
8
  let _alfe_ai_mcp_tools = require("@alfe.ai/mcp-tools");
9
9
  //#region src/index.ts
10
10
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
11
+ const MAX_IDENTITY_CHARS = 256;
12
+ const MAX_SERVICE_URL_CHARS = 8192;
11
13
  /**
12
14
  * Wire shape the bundler advertises this server as — must match the
13
15
  * key the CLI registers (`alfe-platform`) so namespacing is consistent
@@ -39,30 +41,44 @@ const SERVER_BIN_PATH = (0, node_url.fileURLToPath)(new URL("./bin.js", require(
39
41
  * Pure construction — does not connect a transport. Callers (the bin
40
42
  * entry, or tests) attach `StdioServerTransport` or any other transport.
41
43
  *
42
- * `resolveConfig()` is only called when neither `client` nor `apiUrl`
43
- * is provided tests can fully construct the server without touching
44
- * `~/.alfe/config.toml`.
44
+ * `resolveConfig()` is only called when the main `client`/`apiUrl` pair is
45
+ * omitted. Tests and alternate hosts can inject a complete pair without
46
+ * touching `~/.alfe/config.toml`.
45
47
  */
46
48
  async function createServer(opts = {}) {
47
- const profile = opts.profile ?? "default";
48
- let client = opts.client;
49
- let apiUrl = opts.apiUrl;
49
+ const profile = validateProfile(opts.profile ?? "default");
50
+ if (opts.client !== void 0 !== (opts.apiUrl !== void 0)) throw new Error("client and apiUrl must be provided together.");
51
+ let client;
52
+ let apiUrl;
50
53
  let voiceClient = opts.voiceClient;
51
54
  let voiceApiUrl = opts.voiceApiUrl;
52
- if (!client || !apiUrl || profile === "claude-code" && !voiceClient && !voiceApiUrl) {
53
- const cfg = (0, _alfe_ai_config.resolveConfig)();
54
- client = client ?? new _alfe_ai_agent_api_client.AgentApiClient({
55
+ let config;
56
+ const getConfig = () => {
57
+ config ??= (0, _alfe_ai_config.resolveConfig)();
58
+ return config;
59
+ };
60
+ if (opts.client !== void 0 && opts.apiUrl !== void 0) {
61
+ client = opts.client;
62
+ apiUrl = validateServiceUrl("apiUrl", opts.apiUrl);
63
+ } else {
64
+ const cfg = getConfig();
65
+ apiUrl = validateServiceUrl("apiUrl", cfg.apiUrl);
66
+ client = new _alfe_ai_agent_api_client.AgentApiClient({
55
67
  apiKey: cfg.apiKey,
56
- apiUrl: cfg.apiUrl
68
+ apiUrl
57
69
  });
58
- apiUrl = apiUrl ?? cfg.apiUrl;
59
- voiceApiUrl = voiceApiUrl ?? cfg.voiceServiceUrl;
60
- voiceClient = voiceClient ?? new _alfe_ai_agent_api_client.AgentApiClient({
61
- apiKey: cfg.apiKey,
62
- apiUrl: cfg.voiceServiceUrl
70
+ }
71
+ if (profile === "claude-code") {
72
+ const explicitVoiceUrl = voiceApiUrl !== void 0;
73
+ voiceApiUrl = validateServiceUrl("voiceApiUrl", voiceApiUrl ?? config?.voiceServiceUrl ?? apiUrl);
74
+ if (!voiceClient) if (voiceApiUrl === apiUrl) voiceClient = client;
75
+ else if (!explicitVoiceUrl) voiceClient = new _alfe_ai_agent_api_client.AgentApiClient({
76
+ apiKey: getConfig().apiKey,
77
+ apiUrl: voiceApiUrl
63
78
  });
79
+ else throw new Error("voiceClient is required when voiceApiUrl differs from apiUrl.");
64
80
  }
65
- const identity = opts.identity ?? await client.whoami();
81
+ const identity = validateIdentity(opts.identity ?? await client.whoami());
66
82
  const ctx = {
67
83
  client,
68
84
  apiUrl,
@@ -100,23 +116,57 @@ function parseProfileArg(argv) {
100
116
  return "default";
101
117
  }
102
118
  /**
103
- * Entry point boot the server on stdio. Used by `bin.ts`. Any
104
- * startup failure is fatal: log and exit non-zero so the bundler's
105
- * connection attempt surfaces a clear error rather than a hung
106
- * handshake.
119
+ * Boot the server and attach its transport. Used by `bin.ts`; tests can inject
120
+ * an in-memory transport. Process policy (logging, exit code, signals) remains
121
+ * in the executable boundary rather than this reusable library function.
107
122
  */
108
123
  async function main(opts = {}) {
124
+ const { transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport(), ...serverOptions } = opts;
125
+ const server = await createServer(serverOptions);
109
126
  try {
110
- const server = await createServer({ profile: opts.profile });
111
- const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
112
127
  await server.connect(transport);
128
+ return server;
113
129
  } catch (err) {
114
- process.stderr.write(`[alfe-mcp-server] failed to start: ${errMsg(err)}\n`);
115
- process.exit(1);
130
+ await server.close().catch(() => void 0);
131
+ throw err;
132
+ }
133
+ }
134
+ function validateProfile(value) {
135
+ if (value !== "default" && value !== "claude-code") throw new Error("profile must be \"default\" or \"claude-code\".");
136
+ return value;
137
+ }
138
+ function validateIdentity(value) {
139
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Agent identity must be an object.");
140
+ const record = value;
141
+ return {
142
+ agentId: validateIdentityPart("agentId", record.agentId),
143
+ tenantId: validateIdentityPart("tenantId", record.tenantId)
144
+ };
145
+ }
146
+ function validateIdentityPart(label, value) {
147
+ if (typeof value !== "string" || value.length < 1 || value.length > MAX_IDENTITY_CHARS || hasControlCharacters(value)) throw new Error(`${label} must contain 1 to ${String(MAX_IDENTITY_CHARS)} non-control characters.`);
148
+ return value;
149
+ }
150
+ function hasControlCharacters(value) {
151
+ return Array.from(value).some((character) => {
152
+ const codePoint = character.codePointAt(0) ?? 0;
153
+ return codePoint < 32 || codePoint === 127;
154
+ });
155
+ }
156
+ function validateServiceUrl(label, value) {
157
+ if (typeof value !== "string" || value.length < 1 || value.length > MAX_SERVICE_URL_CHARS) throw new Error(`${label} must be a bounded absolute HTTP(S) URL.`);
158
+ let parsed;
159
+ try {
160
+ parsed = new URL(value);
161
+ } catch {
162
+ throw new Error(`${label} must be a bounded absolute HTTP(S) URL.`);
116
163
  }
164
+ if (!["http:", "https:"].includes(parsed.protocol) || parsed.username !== "" || parsed.password !== "" || parsed.search !== "" || parsed.hash !== "" || parsed.protocol === "http:" && !isLoopbackHostname(parsed.hostname)) throw new Error(`${label} must use HTTPS (or loopback HTTP) without credentials, query, or fragment.`);
165
+ return parsed.href.replace(/\/$/u, "");
117
166
  }
118
- function errMsg(err) {
119
- return err instanceof Error ? err.message : String(err);
167
+ function isLoopbackHostname(hostname) {
168
+ const normalized = hostname.replace(/^\[|\]$/gu, "").toLowerCase();
169
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
120
170
  }
121
171
  //#endregion
122
172
  exports.SERVER_BIN_PATH = SERVER_BIN_PATH;