@columenlabs/lumen-ai 0.0.0-stage → 0.1.38
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/NOTICE +12 -0
- package/README.md +55 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp.d.ts +47 -0
- package/dist/mcp.js +112 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Columen Labs
|
|
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/NOTICE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Lumen
|
|
2
|
+
Copyright (c) 2026 Columen Labs. Licensed under the MIT License (see LICENSE).
|
|
3
|
+
|
|
4
|
+
Lumen draws price and market charts with TradingView Lightweight Charts™
|
|
5
|
+
(https://github.com/tradingview/lightweight-charts), Copyright (c) 2025
|
|
6
|
+
TradingView, Inc., licensed under the Apache License 2.0. Lumen turns off the
|
|
7
|
+
chart's own logo, so per that library's NOTICE, an app that shows these charts
|
|
8
|
+
should link to https://www.tradingview.com/ somewhere a user can find it (an
|
|
9
|
+
About page or footer is enough).
|
|
10
|
+
|
|
11
|
+
The components under packages/ui/src/ui are based on shadcn/ui
|
|
12
|
+
(https://ui.shadcn.com), MIT License, Copyright (c) 2023 shadcn.
|
package/README.md
CHANGED
|
@@ -1,3 +1,56 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @columenlabs/lumen-ai
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Assistant and agent parts for apps on Cloudflare Workers. Today: serving an MCP server from a Worker, keeping only the read tools for a read-only caller, and metering every tool call. A product-neutral assistant joins later.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @columenlabs/lumen-ai @columenlabs/lumen-ops @modelcontextprotocol/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Pin every `@columenlabs/lumen-*` package to the same exact version. `@modelcontextprotocol/sdk` is a peer: the app's own copy builds the server.
|
|
12
|
+
|
|
13
|
+
## Serve an MCP server
|
|
14
|
+
|
|
15
|
+
Build one server per request for the caller you have already checked, then hand it the request. Each request stands alone (stateless Streamable HTTP, JSON responses), since a Worker keeps nothing between requests.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { jsonResult, keepReadTools, meterTools, serveMcp } from '@columenlabs/lumen-ai';
|
|
19
|
+
import { setToolSink } from '@columenlabs/lumen-ops/tools';
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
21
|
+
|
|
22
|
+
setToolSink(env.TOOL_CALLS); // once per isolate; tool calls are metered only when it is set
|
|
23
|
+
|
|
24
|
+
function build(person: string) {
|
|
25
|
+
const mcp = new McpServer({ name: 'my-app', version: '1.0.0' });
|
|
26
|
+
mcp.registerTool(
|
|
27
|
+
'account_list',
|
|
28
|
+
{ description: 'Accounts and balances', annotations: { readOnlyHint: true } },
|
|
29
|
+
async () => jsonResult(await accounts(person)),
|
|
30
|
+
);
|
|
31
|
+
return mcp;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
app.all('/mcp', async (c) => {
|
|
35
|
+
const server = meterTools(build(c.get('person')), { app: 'my-app' });
|
|
36
|
+
if (c.get('viewOnly')) keepReadTools(server);
|
|
37
|
+
return serveMcp(server, c.req.raw, { onRequest: ({ tool }) => c.set('tool', tool) });
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Exports
|
|
42
|
+
|
|
43
|
+
| Export | What it does |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `serveMcp(server, request, opts?)` | Answers one MCP request and closes the server; `onRequest` hears the method and tool first |
|
|
46
|
+
| `jsonResult(value)` | A tool result holding one JSON value as text |
|
|
47
|
+
| `keepReadTools(server, isRead?)` | Removes every tool that writes; by default a tool is read-only when its `readOnlyHint` is set |
|
|
48
|
+
| `meterTools(server, opts)` | Wraps every registered tool so each call is one tool-call point: wall time, outcome, rows of a JSON list, input and output size |
|
|
49
|
+
| `registeredTools(server)` | Every tool on a server, by name |
|
|
50
|
+
| `mcpCallOf(body)` | The method and tool a JSON-RPC body asks for |
|
|
51
|
+
|
|
52
|
+
`meterTools` writes through `@columenlabs/lumen-ops/tools`; see its README for the columns.
|
|
53
|
+
|
|
54
|
+
## Versions
|
|
55
|
+
|
|
56
|
+
Every merge to `main` publishes the next patch of every Lumen package at once.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './mcp.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './mcp.js';
|
package/dist/mcp.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { McpServer, RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/** A tool result holding one JSON value as text, what most tools return. */
|
|
3
|
+
export declare function jsonResult(value: unknown): {
|
|
4
|
+
content: {
|
|
5
|
+
type: 'text';
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
};
|
|
9
|
+
/** What one JSON-RPC request asks for, for a request log: its method and, for a call, the tool. */
|
|
10
|
+
export declare function mcpCallOf(body: unknown): {
|
|
11
|
+
method?: string;
|
|
12
|
+
tool?: string;
|
|
13
|
+
};
|
|
14
|
+
export interface ServeMcpOptions {
|
|
15
|
+
/** Told what the request asks for before it runs (the method, and the tool for a call). */
|
|
16
|
+
onRequest?: (call: {
|
|
17
|
+
method?: string;
|
|
18
|
+
tool?: string;
|
|
19
|
+
}) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Answers one MCP request with `server`, built for this caller, then closes it. JSON responses, no
|
|
23
|
+
* session: each request stands alone. The caller is already checked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serveMcp(server: McpServer, request: Request, opts?: ServeMcpOptions): Promise<Response>;
|
|
26
|
+
/** Every tool registered on `server`, by name. */
|
|
27
|
+
export declare function registeredTools(server: McpServer): Record<string, RegisteredTool>;
|
|
28
|
+
/** A tool marked read-only (`readOnlyHint`), the default test for `keepReadTools`. */
|
|
29
|
+
export declare const readOnlyHint: (_name: string, tool: RegisteredTool) => boolean;
|
|
30
|
+
/** Removes every tool `isRead` says writes, for a caller who may only read. Returns `server`. */
|
|
31
|
+
export declare function keepReadTools(server: McpServer, isRead?: (name: string, tool: RegisteredTool) => boolean): McpServer;
|
|
32
|
+
export interface MeterToolsOptions {
|
|
33
|
+
/** The app, or the part of it, the server belongs to. */
|
|
34
|
+
app: string;
|
|
35
|
+
/** Who is calling: `mcp` for an outside client, or the app's own word (an assistant). */
|
|
36
|
+
source?: string;
|
|
37
|
+
/** The turn or run the calls belong to. */
|
|
38
|
+
run?: string;
|
|
39
|
+
/** A short error kind for a throw; default `error`. */
|
|
40
|
+
errorKind?: (err: unknown) => string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wraps every tool registered on `server` so each call is one tool-call point (lumen-ops/tools):
|
|
44
|
+
* its wall time, rows when the result is a JSON list, the size of its input and output, and
|
|
45
|
+
* whether it failed. Call it after the tools are registered. Returns `server`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function meterTools(server: McpServer, opts: MeterToolsOptions): McpServer;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serving an MCP server from a Worker: one stateless Streamable HTTP request at a time (a Worker
|
|
3
|
+
* keeps nothing between requests), plus the small pieces every app's server repeats: a JSON
|
|
4
|
+
* result, keeping only the read tools for a read-only caller, and metering every tool call.
|
|
5
|
+
*/
|
|
6
|
+
import { meterToolCall } from '@columenlabs/lumen-ops/tools';
|
|
7
|
+
import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
|
|
8
|
+
/** A tool result holding one JSON value as text, what most tools return. */
|
|
9
|
+
export function jsonResult(value) {
|
|
10
|
+
return { content: [{ type: 'text', text: JSON.stringify(value) }] };
|
|
11
|
+
}
|
|
12
|
+
/** What one JSON-RPC request asks for, for a request log: its method and, for a call, the tool. */
|
|
13
|
+
export function mcpCallOf(body) {
|
|
14
|
+
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
15
|
+
return {};
|
|
16
|
+
const b = body;
|
|
17
|
+
return {
|
|
18
|
+
method: typeof b.method === 'string' ? b.method : undefined,
|
|
19
|
+
tool: typeof b.params?.name === 'string' ? b.params.name : undefined,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Answers one MCP request with `server`, built for this caller, then closes it. JSON responses, no
|
|
24
|
+
* session: each request stands alone. The caller is already checked.
|
|
25
|
+
*/
|
|
26
|
+
export async function serveMcp(server, request, opts = {}) {
|
|
27
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
28
|
+
sessionIdGenerator: undefined,
|
|
29
|
+
enableJsonResponse: true,
|
|
30
|
+
});
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
try {
|
|
33
|
+
const body = await request
|
|
34
|
+
.clone()
|
|
35
|
+
.json()
|
|
36
|
+
.catch(() => undefined);
|
|
37
|
+
opts.onRequest?.(mcpCallOf(body));
|
|
38
|
+
return await transport.handleRequest(request, body === undefined ? undefined : { parsedBody: body });
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
// JSON-response mode has fully answered by now.
|
|
42
|
+
void server.close();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Every tool registered on `server`, by name. */
|
|
46
|
+
export function registeredTools(server) {
|
|
47
|
+
return server
|
|
48
|
+
._registeredTools;
|
|
49
|
+
}
|
|
50
|
+
/** A tool marked read-only (`readOnlyHint`), the default test for `keepReadTools`. */
|
|
51
|
+
export const readOnlyHint = (_name, tool) => tool.annotations?.readOnlyHint === true;
|
|
52
|
+
/** Removes every tool `isRead` says writes, for a caller who may only read. Returns `server`. */
|
|
53
|
+
export function keepReadTools(server, isRead = readOnlyHint) {
|
|
54
|
+
for (const [name, tool] of Object.entries(registeredTools(server))) {
|
|
55
|
+
if (!isRead(name, tool))
|
|
56
|
+
tool.remove();
|
|
57
|
+
}
|
|
58
|
+
return server;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wraps every tool registered on `server` so each call is one tool-call point (lumen-ops/tools):
|
|
62
|
+
* its wall time, rows when the result is a JSON list, the size of its input and output, and
|
|
63
|
+
* whether it failed. Call it after the tools are registered. Returns `server`.
|
|
64
|
+
*/
|
|
65
|
+
export function meterTools(server, opts) {
|
|
66
|
+
for (const [name, tool] of Object.entries(registeredTools(server))) {
|
|
67
|
+
const handler = tool.handler;
|
|
68
|
+
tool.handler = (async (...args) => {
|
|
69
|
+
const point = {
|
|
70
|
+
app: opts.app,
|
|
71
|
+
tool: name,
|
|
72
|
+
kind: 'call',
|
|
73
|
+
source: opts.source ?? 'mcp',
|
|
74
|
+
run: opts.run,
|
|
75
|
+
};
|
|
76
|
+
const bytesIn = args.length > 1 ? JSON.stringify(args[0] ?? null).length : 0;
|
|
77
|
+
const t0 = performance.now();
|
|
78
|
+
try {
|
|
79
|
+
const out = (await handler(...args));
|
|
80
|
+
const text = out?.content?.[0]?.text ?? '';
|
|
81
|
+
let rows;
|
|
82
|
+
try {
|
|
83
|
+
const v = JSON.parse(text);
|
|
84
|
+
if (Array.isArray(v))
|
|
85
|
+
rows = v.length;
|
|
86
|
+
}
|
|
87
|
+
catch { }
|
|
88
|
+
meterToolCall({
|
|
89
|
+
...point,
|
|
90
|
+
ok: !out?.isError,
|
|
91
|
+
ms: performance.now() - t0,
|
|
92
|
+
...(out?.isError ? { error: 'tool_error' } : {}),
|
|
93
|
+
rows,
|
|
94
|
+
bytesIn,
|
|
95
|
+
bytesOut: text.length,
|
|
96
|
+
});
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
meterToolCall({
|
|
101
|
+
...point,
|
|
102
|
+
ok: false,
|
|
103
|
+
ms: performance.now() - t0,
|
|
104
|
+
error: opts.errorKind?.(err) ?? 'error',
|
|
105
|
+
bytesIn,
|
|
106
|
+
});
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return server;
|
|
112
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@columenlabs/lumen-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.38",
|
|
4
|
+
"description": "Lumen's assistant and agent parts: serving an MCP server from a Worker, read-only tool sets and metered tool calls.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./mcp": {
|
|
15
|
+
"types": "./dist/mcp.d.ts",
|
|
16
|
+
"default": "./dist/mcp.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"NOTICE"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
27
|
+
"homepage": "https://github.com/columenlabs/lumen",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/columenlabs/lumen.git",
|
|
31
|
+
"directory": "packages/ai"
|
|
32
|
+
},
|
|
33
|
+
"bugs": "https://github.com/columenlabs/lumen/issues",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"registry": "https://registry.npmjs.org/"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@columenlabs/lumen-ops": "0.1.38"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
43
|
+
}
|
|
44
|
+
}
|