@bixbd/utils-mcp 0.0.0-stage → 0.1.0-beta.2
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/package.json +47 -4
- package/src/cli.ts +19 -0
- package/src/config.ts +26 -0
- package/src/context.ts +33 -0
- package/src/load.ts +54 -0
- package/src/server.ts +35 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bixbd/utils-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
|
+
"description": "Connect utility portals to AI agents through MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"utils-mcp": "src/cli.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "echo \"server: runs from source with Bun\"",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"start": "bun src/cli.ts"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@bixbd/utils-mcp-core": "0.1.0-beta.2",
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@bixbd/utils-mcp-connector-example": "0.0.0",
|
|
24
|
+
"@bixbd/utils-mcp-connector-desco": "0.1.0-beta.2"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@bixbd/utils-mcp-connector-example": "*",
|
|
28
|
+
"@bixbd/utils-mcp-connector-desco": "*"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"@bixbd/utils-mcp-connector-example": {
|
|
32
|
+
"optional": true
|
|
33
|
+
},
|
|
34
|
+
"@bixbd/utils-mcp-connector-desco": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/bixbd/utils-mcp.git",
|
|
44
|
+
"directory": "packages/server"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"bun": ">=1.1.0"
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { loadConfig } from "./config";
|
|
3
|
+
import { loadConnectors } from "./load";
|
|
4
|
+
import { startServer } from "./server";
|
|
5
|
+
|
|
6
|
+
// stdout is the MCP protocol channel. Log to stderr only.
|
|
7
|
+
const log = (message: string) => process.stderr.write(`utils-mcp: ${message}\n`);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const config = await loadConfig();
|
|
11
|
+
if (config.enabled.length === 0) {
|
|
12
|
+
log('no connectors enabled. Create ~/.utils-mcp/config.json with {"enabled": ["desco"]}.');
|
|
13
|
+
}
|
|
14
|
+
const connectors = await loadConnectors(config.enabled);
|
|
15
|
+
await startServer(connectors, config.enabled);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
log(error instanceof Error ? error.message : "failed to start");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type Config = { enabled: string[] };
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONFIG: Config = { enabled: [] };
|
|
7
|
+
|
|
8
|
+
export function configPath(env: Record<string, string | undefined> = process.env): string {
|
|
9
|
+
return env.UTILS_MCP_CONFIG ?? join(homedir(), ".utils-mcp", "config.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseConfig(raw: string): Config {
|
|
13
|
+
const data = JSON.parse(raw) as { enabled?: unknown };
|
|
14
|
+
if (!Array.isArray(data.enabled) || !data.enabled.every((x) => typeof x === "string")) {
|
|
15
|
+
throw new Error('Config must look like {"enabled": ["connector-id"]}');
|
|
16
|
+
}
|
|
17
|
+
return { enabled: data.enabled as string[] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadConfig(path = configPath()): Promise<Config> {
|
|
21
|
+
const file = Bun.file(path);
|
|
22
|
+
if (!(await file.exists())) {
|
|
23
|
+
return DEFAULT_CONFIG;
|
|
24
|
+
}
|
|
25
|
+
return parseConfig(await file.text());
|
|
26
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { envSecrets, type JsonSchema, type RuntimeContext } from "@bixbd/utils-mcp-core";
|
|
2
|
+
|
|
3
|
+
/** Every tool accepts an optional `account` label. The server adds it, so connectors don't have to. */
|
|
4
|
+
export function withAccountInput(schema: JsonSchema): JsonSchema {
|
|
5
|
+
return {
|
|
6
|
+
...schema,
|
|
7
|
+
properties: {
|
|
8
|
+
...schema.properties,
|
|
9
|
+
account: {
|
|
10
|
+
type: "string",
|
|
11
|
+
description: 'Account label to use. Defaults to "default".',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function splitAccount(args: Record<string, unknown>): {
|
|
18
|
+
account: string;
|
|
19
|
+
input: Record<string, unknown>;
|
|
20
|
+
} {
|
|
21
|
+
const { account, ...input } = args;
|
|
22
|
+
return {
|
|
23
|
+
account: typeof account === "string" && account !== "" ? account : "default",
|
|
24
|
+
input,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildContext(
|
|
29
|
+
account: string,
|
|
30
|
+
env: Record<string, string | undefined> = process.env,
|
|
31
|
+
): RuntimeContext {
|
|
32
|
+
return { account, fetch, secrets: envSecrets(account, env) };
|
|
33
|
+
}
|
package/src/load.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Connector } from "@bixbd/utils-mcp-core";
|
|
2
|
+
|
|
3
|
+
export type Importer = (specifier: string) => Promise<unknown>;
|
|
4
|
+
|
|
5
|
+
const ID = /^[a-z][a-z0-9-]*$/;
|
|
6
|
+
|
|
7
|
+
export const packageFor = (id: string) => `@bixbd/utils-mcp-connector-${id}`;
|
|
8
|
+
|
|
9
|
+
function isConnector(value: unknown): value is Connector {
|
|
10
|
+
if (typeof value !== "object" || value === null) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const candidate = value as Record<string, unknown>;
|
|
14
|
+
return (
|
|
15
|
+
typeof candidate.id === "string" &&
|
|
16
|
+
typeof candidate.baseUrl === "string" &&
|
|
17
|
+
Array.isArray(candidate.tools) &&
|
|
18
|
+
typeof candidate.auth === "object" &&
|
|
19
|
+
candidate.auth !== null
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Loads each enabled connector from its own package, so the server never
|
|
25
|
+
* depends on connectors directly. Each package must default-export a connector.
|
|
26
|
+
*/
|
|
27
|
+
export async function loadConnectors(
|
|
28
|
+
ids: string[],
|
|
29
|
+
importer: Importer = (specifier) => import(specifier),
|
|
30
|
+
): Promise<Connector[]> {
|
|
31
|
+
return Promise.all(
|
|
32
|
+
ids.map(async (id) => {
|
|
33
|
+
// Restrict what can be imported: only our own connector packages.
|
|
34
|
+
if (!ID.test(id)) {
|
|
35
|
+
throw new Error(`Invalid connector id "${id}". Use lowercase kebab-case.`);
|
|
36
|
+
}
|
|
37
|
+
const specifier = packageFor(id);
|
|
38
|
+
let mod: unknown;
|
|
39
|
+
try {
|
|
40
|
+
mod = await importer(specifier);
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(`Connector "${id}" is enabled but ${specifier} could not be loaded. Install it with: bun add ${specifier}`);
|
|
43
|
+
}
|
|
44
|
+
const connector = (mod as { default?: unknown }).default;
|
|
45
|
+
if (!isConnector(connector)) {
|
|
46
|
+
throw new Error(`${specifier} must default-export a connector made with defineConnector.`);
|
|
47
|
+
}
|
|
48
|
+
if (connector.id !== id) {
|
|
49
|
+
throw new Error(`${specifier} exports connector "${connector.id}", expected "${id}".`);
|
|
50
|
+
}
|
|
51
|
+
return connector;
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRegistry, type Connector } from "@bixbd/utils-mcp-core";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { buildContext, splitAccount, withAccountInput } from "./context";
|
|
6
|
+
|
|
7
|
+
export async function startServer(connectors: Connector[], enabled: string[]): Promise<void> {
|
|
8
|
+
const registry = createRegistry(connectors, enabled);
|
|
9
|
+
const server = new Server({ name: "utils-mcp", version: "0.0.0" }, { capabilities: { tools: {} } });
|
|
10
|
+
|
|
11
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
12
|
+
tools: registry.listTools().map((t) => ({
|
|
13
|
+
name: t.name,
|
|
14
|
+
description: t.description,
|
|
15
|
+
inputSchema: withAccountInput(t.inputSchema),
|
|
16
|
+
outputSchema: t.outputSchema,
|
|
17
|
+
annotations: t.annotations,
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
22
|
+
const args = (request.params.arguments ?? {}) as Record<string, unknown>;
|
|
23
|
+
const { account, input } = splitAccount(args);
|
|
24
|
+
const result = await registry.callTool(request.params.name, input, buildContext(account));
|
|
25
|
+
if (result.ok) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text", text: JSON.stringify(result.data) }],
|
|
28
|
+
structuredContent: result.data as Record<string, unknown>,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { isError: true, content: [{ type: "text", text: JSON.stringify(result.error) }] };
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await server.connect(new StdioServerTransport());
|
|
35
|
+
}
|
package/README.md
DELETED