@bixbd/utils-mcp 0.0.0-stage → 0.1.0-beta.3
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 +94 -2
- package/package.json +65 -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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 BIXBD
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @bixbd/utils-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Connect utility portals to AI agents through the [Model Context Protocol](https://modelcontextprotocol.io). It runs on your own computer over stdio, so your credentials never leave your machine.
|
|
4
|
+
|
|
5
|
+
> **Beta.** Expect changes. Published under the `next` tag.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
You enable the connectors you want. Each connector adds a few task-shaped tools (like `desco_get_balance`) to your AI app. Only enabled connectors are loaded, so the AI's tool list stays small.
|
|
10
|
+
|
|
11
|
+
| Connector | Tools | Credentials |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `desco` ([package](https://www.npmjs.com/package/@bixbd/utils-mcp-connector-desco)) | `desco_list_accounts`, `desco_get_balance`, `desco_get_realtime_status` | `DESCO_USERNAME`, `DESCO_PASSWORD` |
|
|
14
|
+
| `example` (repo only) | `example_get_status` | none |
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
[Bun](https://bun.sh) 1.1 or newer. The server runs TypeScript directly with Bun.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
Install the server and the connectors you need into one folder. Connectors are separate packages, and the server loads them from the folder it is installed in.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
mkdir -p ~/.utils-mcp && cd ~/.utils-mcp
|
|
26
|
+
bun add @bixbd/utils-mcp@next @bixbd/utils-mcp-connector-desco@next
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Choose connectors
|
|
30
|
+
|
|
31
|
+
Create `~/.utils-mcp/config.json`. Nothing is enabled by default.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{ "enabled": ["desco"] }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Each name `<id>` loads the package `@bixbd/utils-mcp-connector-<id>`, so it must be installed in the same folder.
|
|
38
|
+
|
|
39
|
+
## Add it to your AI app
|
|
40
|
+
|
|
41
|
+
Use the full path, because most apps do not expand `~`.
|
|
42
|
+
|
|
43
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"utils": {
|
|
49
|
+
"command": "/Users/you/.utils-mcp/node_modules/.bin/utils-mcp",
|
|
50
|
+
"env": { "DESCO_USERNAME": "your-username", "DESCO_PASSWORD": "your-password" }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Claude Code:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
claude mcp add utils -e DESCO_USERNAME=your-username -e DESCO_PASSWORD=your-password -- ~/.utils-mcp/node_modules/.bin/utils-mcp
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Restart the app, then ask something like "What is my DESCO balance?"
|
|
63
|
+
|
|
64
|
+
## Multiple accounts
|
|
65
|
+
|
|
66
|
+
Add a label after two underscores and pass it as `account` in the tool call:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
DESCO_USERNAME__OFFICE=...
|
|
70
|
+
DESCO_PASSWORD__OFFICE=...
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then ask "check the office DESCO account", and the AI calls the tool with `account: "office"`. Without a label, the default account is used.
|
|
74
|
+
|
|
75
|
+
## Troubleshooting
|
|
76
|
+
|
|
77
|
+
| Message | Fix |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `no connectors enabled` | Create `~/.utils-mcp/config.json` with `{"enabled": ["desco"]}`. |
|
|
80
|
+
| Cannot find the connector package | Run `bun add @bixbd/utils-mcp-connector-<id>@next` inside `~/.utils-mcp`. |
|
|
81
|
+
| A tool says credentials are missing | Add the environment variables to your app's MCP config (the `env` block). |
|
|
82
|
+
| The portal changed | The tool returns `UPSTREAM_CHANGED`. Open an issue with the tool name. |
|
|
83
|
+
|
|
84
|
+
Set the config path with `UTILS_MCP_CONFIG=/path/to/config.json` if you keep it elsewhere. Logs go to stderr, never stdout.
|
|
85
|
+
|
|
86
|
+
## Security
|
|
87
|
+
|
|
88
|
+
- Credentials come from environment variables and are sent only to the portal over HTTPS.
|
|
89
|
+
- Sessions are kept in memory only. Nothing is written to disk.
|
|
90
|
+
- Account numbers are masked in tool output.
|
|
91
|
+
- Use only accounts you own or have permission to access. Connectors are unofficial and not affiliated with the portals.
|
|
92
|
+
|
|
93
|
+
## Links
|
|
94
|
+
|
|
95
|
+
[Source and issues](https://github.com/bixbd/utils-mcp) · [Write a connector](https://github.com/bixbd/utils-mcp/blob/main/CONTRIBUTING.md) · MIT License
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bixbd/utils-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0-beta.3",
|
|
4
|
+
"description": "Connect utility portals to AI agents through MCP. Local stdio server for Bun; credentials stay on your machine.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"ai-agents",
|
|
9
|
+
"claude",
|
|
10
|
+
"bun",
|
|
11
|
+
"utilities",
|
|
12
|
+
"portals",
|
|
13
|
+
"stdio"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "BIXBD (https://github.com/bixbd)",
|
|
17
|
+
"homepage": "https://github.com/bixbd/utils-mcp#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/bixbd/utils-mcp/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/bixbd/utils-mcp.git",
|
|
24
|
+
"directory": "packages/server"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"utils-mcp": "src/cli.ts"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"bun": ">=1.1.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "echo \"server: runs from source with Bun\"",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"test": "bun test",
|
|
45
|
+
"start": "bun src/cli.ts"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@bixbd/utils-mcp-core": "0.1.0-beta.3",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@bixbd/utils-mcp-connector-example": "0.0.0",
|
|
53
|
+
"@bixbd/utils-mcp-connector-desco": "0.1.0-beta.3"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@bixbd/utils-mcp-connector-example": "*",
|
|
57
|
+
"@bixbd/utils-mcp-connector-desco": "*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@bixbd/utils-mcp-connector-example": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@bixbd/utils-mcp-connector-desco": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
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
|
+
}
|