@ekwo-ai/mcp 0.4.0
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 +253 -0
- package/dist/backend.d.ts +24 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +105 -0
- package/dist/backend.js.map +1 -0
- package/dist/bin.d.ts +15 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +75 -0
- package/dist/bin.js.map +1 -0
- package/dist/columns.d.ts +59 -0
- package/dist/columns.d.ts.map +1 -0
- package/dist/columns.js +422 -0
- package/dist/columns.js.map +1 -0
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +102 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +49 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/postgrest.d.ts +35 -0
- package/dist/postgrest.d.ts.map +1 -0
- package/dist/postgrest.js +152 -0
- package/dist/postgrest.js.map +1 -0
- package/dist/rounding.d.ts +25 -0
- package/dist/rounding.d.ts.map +1 -0
- package/dist/rounding.js +35 -0
- package/dist/rounding.js.map +1 -0
- package/dist/schema.d.ts +28 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +51 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +521 -0
- package/dist/server.js.map +1 -0
- package/dist/sql.d.ts +36 -0
- package/dist/sql.d.ts.map +1 -0
- package/dist/sql.js +214 -0
- package/dist/sql.js.map +1 -0
- package/dist/tools/modules.d.ts +61 -0
- package/dist/tools/modules.d.ts.map +1 -0
- package/dist/tools/modules.js +335 -0
- package/dist/tools/modules.js.map +1 -0
- package/dist/tools/read.d.ts +233 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +853 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +343 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +814 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +69 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAML,KAAK,OAAO,EAKb,MAAM,cAAc,CAAC;AAEtB,gEAAgE;AAChE,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClF,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AA+CD,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AA4BD,wBAAgB,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAsH7E;AAED,qDAAqD;AACrD,wBAAsB,OAAO,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA4C1E"}
|
package/dist/sql.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The direct-Postgres backend.
|
|
3
|
+
*
|
|
4
|
+
* For a self-hosted installation where PostgREST is not in front of the
|
|
5
|
+
* database, and for the tests, which run the real schema in Postgres compiled
|
|
6
|
+
* to WebAssembly. It is the second route, not the privileged one: every call
|
|
7
|
+
* runs inside a transaction that sets `request.jwt.claims` and switches to
|
|
8
|
+
* the `authenticated` role, so row level security binds exactly as it does
|
|
9
|
+
* over the API. A connection that could bypass the policies would make this
|
|
10
|
+
* server a way round them, which is the one thing it must never be.
|
|
11
|
+
*
|
|
12
|
+
* The driver is behind `SqlClient` — thirty lines with nothing to decide — so
|
|
13
|
+
* the tests plug PGlite in and never load a network driver.
|
|
14
|
+
*/
|
|
15
|
+
import { EkwoMcpError, columnName, explain, identifier, qualified, } from './backend.js';
|
|
16
|
+
/** Builds `where` with $n placeholders, appending to `params`. */
|
|
17
|
+
function whereClause(filters, params) {
|
|
18
|
+
if (filters === undefined || filters.length === 0)
|
|
19
|
+
return '';
|
|
20
|
+
const parts = filters.map((filter) => {
|
|
21
|
+
const column = identifier(filter.column);
|
|
22
|
+
switch (filter.op) {
|
|
23
|
+
case 'is':
|
|
24
|
+
return `${column} is null`;
|
|
25
|
+
case 'in': {
|
|
26
|
+
if (filter.value.length === 0)
|
|
27
|
+
return 'false';
|
|
28
|
+
const slots = filter.value.map((value) => {
|
|
29
|
+
params.push(value);
|
|
30
|
+
return `$${params.length}`;
|
|
31
|
+
});
|
|
32
|
+
return `${column} in (${slots.join(', ')})`;
|
|
33
|
+
}
|
|
34
|
+
case 'ilike':
|
|
35
|
+
params.push(filter.value);
|
|
36
|
+
return `${column} ilike $${params.length}`;
|
|
37
|
+
default: {
|
|
38
|
+
const operator = { eq: '=', neq: '<>', gt: '>', gte: '>=', lt: '<', lte: '<=' }[filter.op];
|
|
39
|
+
params.push(filter.value);
|
|
40
|
+
return `${column} ${operator} $${params.length}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return ` where ${parts.join(' and ')}`;
|
|
45
|
+
}
|
|
46
|
+
/** `amount::text` is selected as `amount::text as amount`, so keys never shift. */
|
|
47
|
+
function selectList(columns) {
|
|
48
|
+
return columns
|
|
49
|
+
.map((column) => {
|
|
50
|
+
const cast = column.indexOf('::');
|
|
51
|
+
if (cast === -1)
|
|
52
|
+
return identifier(column);
|
|
53
|
+
const name = columnName(column);
|
|
54
|
+
const type = column.slice(cast + 2);
|
|
55
|
+
if (!/^[a-z ]+$/.test(type)) {
|
|
56
|
+
throw new EkwoMcpError(`bad_identifier: ${column} is not a column and a cast`);
|
|
57
|
+
}
|
|
58
|
+
return `${name}::${type} as ${name}`;
|
|
59
|
+
})
|
|
60
|
+
.join(', ');
|
|
61
|
+
}
|
|
62
|
+
/** `fn(p_a => $1, p_b => $2)` and the parameters in that order. */
|
|
63
|
+
/**
|
|
64
|
+
* An argument that is an object or an array is JSON, and is sent as JSON.
|
|
65
|
+
*
|
|
66
|
+
* PostgREST posts the arguments as a JSON body, so a `jsonb` parameter
|
|
67
|
+
* receives a real array on that route. A Postgres driver does not: handed a
|
|
68
|
+
* JavaScript array it builds a Postgres *array* literal, and `node-postgres`
|
|
69
|
+
* turns an array of objects into `{"[object Object]"}`. So the value is
|
|
70
|
+
* stringified here and the placeholder carries an explicit `::jsonb`, which
|
|
71
|
+
* makes the two routes agree instead of agreeing by accident on one driver.
|
|
72
|
+
*/
|
|
73
|
+
function callOf(fn, args, schema) {
|
|
74
|
+
const entries = Object.entries(args);
|
|
75
|
+
const params = [];
|
|
76
|
+
const parts = entries.map(([name, value], index) => {
|
|
77
|
+
const json = typeof value === 'object' && value !== null;
|
|
78
|
+
params.push(json ? JSON.stringify(value) : value);
|
|
79
|
+
return `${identifier(name)} => $${index + 1}${json ? '::jsonb' : ''}`;
|
|
80
|
+
});
|
|
81
|
+
return { call: `${qualified(schema, fn)}(${parts.join(', ')})`, params };
|
|
82
|
+
}
|
|
83
|
+
export function sqlBackend(db, options) {
|
|
84
|
+
const claims = JSON.stringify({
|
|
85
|
+
...(options.claims ?? {}),
|
|
86
|
+
sub: options.userId,
|
|
87
|
+
role: 'authenticated',
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* One statement, as the user.
|
|
91
|
+
*
|
|
92
|
+
* `set_config(..., true)` and `set local role` are both transaction-scoped,
|
|
93
|
+
* so nothing leaks into the next call even on a pooled connection.
|
|
94
|
+
*/
|
|
95
|
+
async function asUser(run) {
|
|
96
|
+
try {
|
|
97
|
+
return await db.transaction(async (tx) => {
|
|
98
|
+
await tx.query('select set_config($1, $2, true)', ['request.jwt.claims', claims]);
|
|
99
|
+
await tx.exec('set local role authenticated');
|
|
100
|
+
return run(tx);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
throw explain(error.message);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
mode: 'sql',
|
|
109
|
+
actingAs: options.userId,
|
|
110
|
+
async rpc(fn, args = {}, schema) {
|
|
111
|
+
const { call, params } = callOf(fn, args, schema);
|
|
112
|
+
// Aggregated as JSON so the shape matches what PostgREST returns for the
|
|
113
|
+
// same function, instead of depending on how a driver types a column.
|
|
114
|
+
const rows = await asUser((tx) => tx.query(`select coalesce(jsonb_agg(to_jsonb(f)), '[]'::jsonb) as value from ${call} f`, params));
|
|
115
|
+
return (rows[0]?.value ?? []);
|
|
116
|
+
},
|
|
117
|
+
async rpcVoid(fn, args = {}, schema) {
|
|
118
|
+
const { call, params } = callOf(fn, args, schema);
|
|
119
|
+
await asUser((tx) => tx.query(`select ${call}`, params));
|
|
120
|
+
},
|
|
121
|
+
async select(query) {
|
|
122
|
+
const params = [];
|
|
123
|
+
let sql = `select ${selectList(query.columns)} from ${qualified(query.schema, query.table)}`;
|
|
124
|
+
sql += whereClause(query.where, params);
|
|
125
|
+
if (query.order !== undefined && query.order.length > 0) {
|
|
126
|
+
const parts = query.order.map((order) => `${identifier(order.column)} ${order.ascending === false ? 'desc' : 'asc'}`);
|
|
127
|
+
sql += ` order by ${parts.join(', ')}`;
|
|
128
|
+
}
|
|
129
|
+
if (query.limit !== undefined) {
|
|
130
|
+
params.push(query.limit);
|
|
131
|
+
sql += ` limit $${params.length}`;
|
|
132
|
+
}
|
|
133
|
+
return asUser((tx) => tx.query(sql, params));
|
|
134
|
+
},
|
|
135
|
+
async insert(table, rows, returning = ['*'], schema) {
|
|
136
|
+
if (rows.length === 0)
|
|
137
|
+
return [];
|
|
138
|
+
const columns = Object.keys(rows[0]).map(identifier);
|
|
139
|
+
const params = [];
|
|
140
|
+
const tuples = rows.map((row) => {
|
|
141
|
+
const slots = columns.map((column) => {
|
|
142
|
+
params.push(row[column]);
|
|
143
|
+
return `$${params.length}`;
|
|
144
|
+
});
|
|
145
|
+
return `(${slots.join(', ')})`;
|
|
146
|
+
});
|
|
147
|
+
const sql = `insert into ${qualified(schema, table)} (${columns.join(', ')}) values ${tuples.join(', ')}` +
|
|
148
|
+
` returning ${returning[0] === '*' ? '*' : selectList(returning)}`;
|
|
149
|
+
return asUser((tx) => tx.query(sql, params));
|
|
150
|
+
},
|
|
151
|
+
async update(table, patch, where, returning = ['*'], schema) {
|
|
152
|
+
const params = [];
|
|
153
|
+
const assignments = Object.keys(patch).map((column) => {
|
|
154
|
+
params.push(patch[column]);
|
|
155
|
+
return `${identifier(column)} = $${params.length}`;
|
|
156
|
+
});
|
|
157
|
+
if (assignments.length === 0) {
|
|
158
|
+
throw new EkwoMcpError(`empty_update: nothing to change on ${table}`);
|
|
159
|
+
}
|
|
160
|
+
const sql = `update ${qualified(schema, table)} set ${assignments.join(', ')}` +
|
|
161
|
+
whereClause(where, params) +
|
|
162
|
+
` returning ${returning[0] === '*' ? '*' : selectList(returning)}`;
|
|
163
|
+
return asUser((tx) => tx.query(sql, params));
|
|
164
|
+
},
|
|
165
|
+
async remove(table, where, schema) {
|
|
166
|
+
const params = [];
|
|
167
|
+
const sql = `delete from ${qualified(schema, table)}` + whereClause(where, params);
|
|
168
|
+
await asUser((tx) => tx.query(sql, params));
|
|
169
|
+
},
|
|
170
|
+
async close() {
|
|
171
|
+
await db.close();
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Opens a connection with the `postgres` driver. */
|
|
176
|
+
export async function connect(connectionString) {
|
|
177
|
+
// Imported lazily, and declared as an optional peer dependency: the
|
|
178
|
+
// recommended route needs no Postgres driver at all, so an operator who
|
|
179
|
+
// never sets EKWO_DB_URL should not have to install one.
|
|
180
|
+
const { default: postgres } = await import('postgres').catch(() => {
|
|
181
|
+
throw new EkwoMcpError('missing_driver: EKWO_DB_URL needs the `postgres` package. Install it alongside this server, or use SUPABASE_URL with a user session instead.');
|
|
182
|
+
});
|
|
183
|
+
const sql = postgres(connectionString, {
|
|
184
|
+
max: 1,
|
|
185
|
+
prepare: false,
|
|
186
|
+
idle_timeout: 20,
|
|
187
|
+
connect_timeout: 30,
|
|
188
|
+
onnotice: () => { },
|
|
189
|
+
});
|
|
190
|
+
const wrap = (handle) => ({
|
|
191
|
+
async query(text, params = []) {
|
|
192
|
+
const result = await handle.unsafe(text, params);
|
|
193
|
+
return result;
|
|
194
|
+
},
|
|
195
|
+
async exec(text) {
|
|
196
|
+
await handle.unsafe(text).simple();
|
|
197
|
+
},
|
|
198
|
+
async transaction(fn) {
|
|
199
|
+
return (await handle.begin(async (tx) => fn(wrap(tx))));
|
|
200
|
+
},
|
|
201
|
+
async close() {
|
|
202
|
+
await sql.end({ timeout: 5 });
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
try {
|
|
206
|
+
await sql `select 1`;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
await sql.end({ timeout: 5 }).catch(() => { });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
return wrap(sql);
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=sql.js.map
|
package/dist/sql.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../src/sql.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools of the modules, and the loader that puts them on the server.
|
|
3
|
+
*
|
|
4
|
+
* A module is a schema of its own, so its tools are named after it —
|
|
5
|
+
* `assets_list`, `budgets_variance` — and a model reading a tool list can tell
|
|
6
|
+
* which module answers for what. The prefix is in `module.json`, and a test
|
|
7
|
+
* holds the tool names to it.
|
|
8
|
+
*
|
|
9
|
+
* **Nothing here decides what is installed.** `public.modules` does, and the
|
|
10
|
+
* server asks it at startup: a module whose migrations have never run has no
|
|
11
|
+
* row, so its tools are not offered, and a tool a model cannot use is worse
|
|
12
|
+
* than a tool it cannot see. What the registry below carries is the other
|
|
13
|
+
* half — the handlers this release knows how to write — and a module installed
|
|
14
|
+
* in the database that this release has no handlers for is simply not exposed.
|
|
15
|
+
*
|
|
16
|
+
* **PostgREST has to be told.** A schema other than `public` is served only
|
|
17
|
+
* once the project lists it under its exposed schemas, which no migration can
|
|
18
|
+
* set. The refusal that comes back is a 406 about a profile, which says
|
|
19
|
+
* nothing useful to a model, so every module tool turns it into the sentence
|
|
20
|
+
* that names the setting.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { type Backend } from '../backend.js';
|
|
24
|
+
/** One module's tools, as the loader sees them. */
|
|
25
|
+
export interface ModuleToolset {
|
|
26
|
+
/** The module code, and the key of `public.modules`. */
|
|
27
|
+
code: string;
|
|
28
|
+
/** Every tool of this module is `<prefix>_<verb>`. */
|
|
29
|
+
prefix: string;
|
|
30
|
+
/** The schema its objects live in, for the message when it is not exposed. */
|
|
31
|
+
schema: string;
|
|
32
|
+
tools: ModuleTool[];
|
|
33
|
+
}
|
|
34
|
+
export interface ModuleTool {
|
|
35
|
+
/** Without the prefix: `list`, `create`, `variance`. */
|
|
36
|
+
verb: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
input: z.ZodObject<z.ZodRawShape>;
|
|
40
|
+
readOnly: boolean;
|
|
41
|
+
run(backend: Backend, args: Record<string, never>): Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A schema the project has not exposed answers with a profile error, which
|
|
45
|
+
* reads like a bug in this server. It is a setting, so say which one.
|
|
46
|
+
*/
|
|
47
|
+
export declare function inSchema<T>(schema: string, run: () => Promise<T>): Promise<T>;
|
|
48
|
+
/** Every module this release knows how to expose, by module code. */
|
|
49
|
+
export declare const MODULE_TOOLSETS: readonly ModuleToolset[];
|
|
50
|
+
/** The toolsets for a set of installed module codes, in a fixed order. */
|
|
51
|
+
export declare function toolsetsFor(installed: readonly string[]): ModuleToolset[];
|
|
52
|
+
/**
|
|
53
|
+
* Which modules this installation carries.
|
|
54
|
+
*
|
|
55
|
+
* `public.modules` is the registry, and it is readable by anyone signed in —
|
|
56
|
+
* it says what exists here, not what any company holds. A database that
|
|
57
|
+
* predates the module framework has no such table, and the answer is then no
|
|
58
|
+
* modules rather than an error.
|
|
59
|
+
*/
|
|
60
|
+
export declare function installedModules(backend: Backend): Promise<string[]>;
|
|
61
|
+
//# sourceMappingURL=modules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../../src/tools/modules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAgB,KAAK,OAAO,EAAY,MAAM,eAAe,CAAC;AAIrE,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;IACf,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtE;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAcnF;AAwUD,qEAAqE;AACrE,eAAO,MAAM,eAAe,EAAE,SAAS,aAAa,EAAsB,CAAC;AAE3E,0EAA0E;AAC1E,wBAAgB,WAAW,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,EAAE,CAGzE;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAW1E"}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tools of the modules, and the loader that puts them on the server.
|
|
3
|
+
*
|
|
4
|
+
* A module is a schema of its own, so its tools are named after it —
|
|
5
|
+
* `assets_list`, `budgets_variance` — and a model reading a tool list can tell
|
|
6
|
+
* which module answers for what. The prefix is in `module.json`, and a test
|
|
7
|
+
* holds the tool names to it.
|
|
8
|
+
*
|
|
9
|
+
* **Nothing here decides what is installed.** `public.modules` does, and the
|
|
10
|
+
* server asks it at startup: a module whose migrations have never run has no
|
|
11
|
+
* row, so its tools are not offered, and a tool a model cannot use is worse
|
|
12
|
+
* than a tool it cannot see. What the registry below carries is the other
|
|
13
|
+
* half — the handlers this release knows how to write — and a module installed
|
|
14
|
+
* in the database that this release has no handlers for is simply not exposed.
|
|
15
|
+
*
|
|
16
|
+
* **PostgREST has to be told.** A schema other than `public` is served only
|
|
17
|
+
* once the project lists it under its exposed schemas, which no migration can
|
|
18
|
+
* set. The refusal that comes back is a 406 about a profile, which says
|
|
19
|
+
* nothing useful to a model, so every module tool turns it into the sentence
|
|
20
|
+
* that names the setting.
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import { EkwoMcpError } from '../backend.js';
|
|
24
|
+
import { money } from '@ekwo-ai/core';
|
|
25
|
+
import { companyId, isoDate, uuid } from './read.js';
|
|
26
|
+
/**
|
|
27
|
+
* A schema the project has not exposed answers with a profile error, which
|
|
28
|
+
* reads like a bug in this server. It is a setting, so say which one.
|
|
29
|
+
*/
|
|
30
|
+
export async function inSchema(schema, run) {
|
|
31
|
+
try {
|
|
32
|
+
return await run();
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
36
|
+
if (/profile|schema must be one of|PGRST106|acceptable/i.test(message)) {
|
|
37
|
+
throw new EkwoMcpError(`schema_not_exposed: the \`${schema}\` schema is not served by this project's API. ` +
|
|
38
|
+
`Add it in Supabase → Project Settings → API → Exposed schemas, or to \`[api] schemas\` ` +
|
|
39
|
+
`in supabase/config.toml, and try again. ${message}`);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// assets
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
const ASSETS = {
|
|
48
|
+
code: 'assets',
|
|
49
|
+
prefix: 'assets',
|
|
50
|
+
schema: 'assets',
|
|
51
|
+
tools: [
|
|
52
|
+
{
|
|
53
|
+
verb: 'list',
|
|
54
|
+
title: 'Fixed assets',
|
|
55
|
+
readOnly: true,
|
|
56
|
+
description: 'The register of fixed assets of a company at a date: what each one cost, what has been written off it and what is left. It reads what has been *booked*, so it ties back to the accumulated depreciation account on the balance sheet. An asset disposed of before that date is not in it.',
|
|
57
|
+
input: z.object({
|
|
58
|
+
company_id: companyId,
|
|
59
|
+
at: isoDate.describe('The date to read the register at.'),
|
|
60
|
+
}),
|
|
61
|
+
async run(backend, args) {
|
|
62
|
+
const at = args['at'];
|
|
63
|
+
const rows = await inSchema('assets', () => backend.rpc('register', { p_company_id: args['company_id'], p_at: at }, 'assets'));
|
|
64
|
+
return {
|
|
65
|
+
at,
|
|
66
|
+
assets: rows.map((row) => ({
|
|
67
|
+
...row,
|
|
68
|
+
cost: money(row['cost']),
|
|
69
|
+
accumulated: money(row['accumulated']),
|
|
70
|
+
net_book_value: money(row['net_book_value']),
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
verb: 'create',
|
|
77
|
+
title: 'Record a fixed asset',
|
|
78
|
+
readOnly: false,
|
|
79
|
+
description: 'Creates a fixed asset and writes its depreciation schedule in one call. Give a `category_code` from the country pack — assets_categories lists them — and the method, the duration and the coefficient come from it; anything you pass yourself wins over the category. The three accounts are the asset account, the accumulated depreciation account and the depreciation charge. Nothing is booked here: assets_run_depreciation is what posts.',
|
|
80
|
+
input: z.object({
|
|
81
|
+
company_id: companyId,
|
|
82
|
+
code: z.string().min(1).describe('Your own reference for the asset. Unique in the company.'),
|
|
83
|
+
name: z.string().min(1),
|
|
84
|
+
acquisition_date: isoDate,
|
|
85
|
+
cost: z.number().positive().describe('What it cost, excluding recoverable tax.'),
|
|
86
|
+
asset_account: z.string().describe('Account code the asset itself sits on.'),
|
|
87
|
+
depreciation_account: z.string().describe('Account code of the accumulated depreciation.'),
|
|
88
|
+
expense_account: z.string().describe('Account code the depreciation charge lands on.'),
|
|
89
|
+
category_code: z.string().optional().describe('A category of the country pack.'),
|
|
90
|
+
duration_months: z.number().int().positive().optional(),
|
|
91
|
+
method: z.enum(['straight_line', 'declining_balance']).optional(),
|
|
92
|
+
coefficient: z.number().positive().optional().describe('Required by a declining balance.'),
|
|
93
|
+
residual_value: z.number().min(0).optional(),
|
|
94
|
+
in_service_date: isoDate.optional().describe('When it started being used, if not the day it was bought.'),
|
|
95
|
+
description: z.string().optional(),
|
|
96
|
+
}),
|
|
97
|
+
async run(backend, args) {
|
|
98
|
+
// A function returning a scalar comes back as the scalar, on either
|
|
99
|
+
// backend: `opening_balance` is read the same way.
|
|
100
|
+
const created = await inSchema('assets', () => backend.rpc('create_asset', {
|
|
101
|
+
p_company_id: args['company_id'],
|
|
102
|
+
p_code: args['code'],
|
|
103
|
+
p_name: args['name'],
|
|
104
|
+
p_acquisition_date: args['acquisition_date'],
|
|
105
|
+
p_cost: args['cost'],
|
|
106
|
+
p_asset_account: args['asset_account'],
|
|
107
|
+
p_depreciation_account: args['depreciation_account'],
|
|
108
|
+
p_expense_account: args['expense_account'],
|
|
109
|
+
p_category_code: args['category_code'] ?? null,
|
|
110
|
+
p_duration_months: args['duration_months'] ?? null,
|
|
111
|
+
p_method: args['method'] ?? null,
|
|
112
|
+
p_coefficient: args['coefficient'] ?? null,
|
|
113
|
+
p_residual_value: args['residual_value'] ?? 0,
|
|
114
|
+
p_in_service_date: args['in_service_date'] ?? null,
|
|
115
|
+
p_description: args['description'] ?? null,
|
|
116
|
+
}, 'assets'));
|
|
117
|
+
return { asset_id: created[0] ?? null };
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
verb: 'schedule',
|
|
122
|
+
title: 'Depreciation schedule',
|
|
123
|
+
readOnly: true,
|
|
124
|
+
description: 'The planned depreciation of one asset, period by period, with the amount, the accumulated total and the net book value after each one, and the entry that booked it where it has been booked. The schedule always sums to exactly the cost less the residual value: the last line takes the remainder.',
|
|
125
|
+
input: z.object({ company_id: companyId, asset_id: uuid }),
|
|
126
|
+
async run(backend, args) {
|
|
127
|
+
const lines = await inSchema('assets', () => backend.select({
|
|
128
|
+
schema: 'assets',
|
|
129
|
+
table: 'depreciation_lines',
|
|
130
|
+
columns: [
|
|
131
|
+
'sequence',
|
|
132
|
+
'period_start',
|
|
133
|
+
'period_end',
|
|
134
|
+
'amount::text',
|
|
135
|
+
'accumulated::text',
|
|
136
|
+
'net_book_value::text',
|
|
137
|
+
'entry_id',
|
|
138
|
+
'posted_at',
|
|
139
|
+
],
|
|
140
|
+
where: [
|
|
141
|
+
{ column: 'company_id', op: 'eq', value: args['company_id'] },
|
|
142
|
+
{ column: 'asset_id', op: 'eq', value: args['asset_id'] },
|
|
143
|
+
],
|
|
144
|
+
order: [{ column: 'sequence' }],
|
|
145
|
+
}));
|
|
146
|
+
return { lines };
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
verb: 'run_depreciation',
|
|
151
|
+
title: 'Book the depreciation',
|
|
152
|
+
readOnly: false,
|
|
153
|
+
description: 'Books every planned period that ends on or before a date, one ledger entry per period, through the socle\'s own posting function. It is idempotent: a period already booked is skipped, and the database refuses a second entry for it anyway. A closed financial year or a locked period refuses the posting — report the refusal, do not move the date.',
|
|
154
|
+
input: z.object({
|
|
155
|
+
company_id: companyId,
|
|
156
|
+
period_end: isoDate.describe('Book everything up to and including this date.'),
|
|
157
|
+
}),
|
|
158
|
+
async run(backend, args) {
|
|
159
|
+
const result = await inSchema('assets', () => backend.rpc('run_depreciation', { p_company_id: args['company_id'], p_period_end: args['period_end'] }, 'assets'));
|
|
160
|
+
return result[0] ?? { entries: [] };
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
verb: 'dispose',
|
|
165
|
+
title: 'Dispose of a fixed asset',
|
|
166
|
+
readOnly: false,
|
|
167
|
+
description: 'Takes an asset off the books on a date: clears its cost and its accumulated depreciation, books the proceeds against the account you name, and presents the result the way the country pack says — one gain or loss line, or the value sold and the proceeds in full. Run assets_run_depreciation first: a disposal is refused while a period that has already ended is still unbooked. There is no undo.',
|
|
168
|
+
input: z.object({
|
|
169
|
+
company_id: companyId,
|
|
170
|
+
asset_id: uuid,
|
|
171
|
+
disposal_date: isoDate,
|
|
172
|
+
proceeds: z.number().min(0).default(0).describe('What it was sold for, excluding tax. Zero when it was scrapped.'),
|
|
173
|
+
counterpart_account: z
|
|
174
|
+
.string()
|
|
175
|
+
.optional()
|
|
176
|
+
.describe('Account code the proceeds are owed on. The receivable account of the company by default.'),
|
|
177
|
+
contact_id: uuid.optional().describe('Who bought it.'),
|
|
178
|
+
}),
|
|
179
|
+
async run(backend, args) {
|
|
180
|
+
const result = await inSchema('assets', () => backend.rpc('dispose_asset', {
|
|
181
|
+
p_asset_id: args['asset_id'],
|
|
182
|
+
p_date: args['disposal_date'],
|
|
183
|
+
p_proceeds: args['proceeds'] ?? 0,
|
|
184
|
+
p_counterpart_account: args['counterpart_account'] ?? null,
|
|
185
|
+
p_contact_id: args['contact_id'] ?? null,
|
|
186
|
+
}, 'assets'));
|
|
187
|
+
return { entry_id: result[0] ?? null };
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// budgets
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
const BUDGETS = {
|
|
196
|
+
code: 'budgets',
|
|
197
|
+
prefix: 'budgets',
|
|
198
|
+
schema: 'budgets',
|
|
199
|
+
tools: [
|
|
200
|
+
{
|
|
201
|
+
verb: 'list',
|
|
202
|
+
title: 'Budgets',
|
|
203
|
+
readOnly: true,
|
|
204
|
+
description: 'The budgets of a company, with the financial year each one is for and whether it is a draft, approved or closed. Start here: budgets_variance needs a budget_id.',
|
|
205
|
+
input: z.object({ company_id: companyId }),
|
|
206
|
+
async run(backend, args) {
|
|
207
|
+
const budgets = await inSchema('budgets', () => backend.select({
|
|
208
|
+
schema: 'budgets',
|
|
209
|
+
table: 'budgets',
|
|
210
|
+
columns: ['id', 'code', 'name', 'state', 'fiscal_year_id', 'notes'],
|
|
211
|
+
where: [{ column: 'company_id', op: 'eq', value: args['company_id'] }],
|
|
212
|
+
order: [{ column: 'code' }],
|
|
213
|
+
}));
|
|
214
|
+
return { budgets };
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
verb: 'upsert_lines',
|
|
219
|
+
title: 'Write budget lines',
|
|
220
|
+
readOnly: false,
|
|
221
|
+
description: 'Adds lines to a budget: one figure per account and per period. State the amount the way a business says it out loud — an income and a cost are both positive — and the comparison flips the ledger, not you. A line already there for the same account and period is replaced.',
|
|
222
|
+
input: z.object({
|
|
223
|
+
company_id: companyId,
|
|
224
|
+
budget_id: uuid,
|
|
225
|
+
lines: z
|
|
226
|
+
.array(z.object({
|
|
227
|
+
account_code: z.string(),
|
|
228
|
+
period_start: isoDate,
|
|
229
|
+
period_end: isoDate,
|
|
230
|
+
amount: z.number(),
|
|
231
|
+
note: z.string().optional(),
|
|
232
|
+
}))
|
|
233
|
+
.min(1),
|
|
234
|
+
}),
|
|
235
|
+
async run(backend, args) {
|
|
236
|
+
const companyIdValue = args['company_id'];
|
|
237
|
+
const budgetIdValue = args['budget_id'];
|
|
238
|
+
const lines = args['lines'];
|
|
239
|
+
const codes = [...new Set(lines.map((line) => line.account_code))];
|
|
240
|
+
const accounts = await backend.select({
|
|
241
|
+
table: 'accounts',
|
|
242
|
+
columns: ['id', 'code'],
|
|
243
|
+
where: [
|
|
244
|
+
{ column: 'company_id', op: 'eq', value: companyIdValue },
|
|
245
|
+
{ column: 'code', op: 'in', value: codes },
|
|
246
|
+
],
|
|
247
|
+
});
|
|
248
|
+
const byCode = new Map(accounts.map((account) => [account.code, account.id]));
|
|
249
|
+
const missing = codes.filter((code) => !byCode.has(code));
|
|
250
|
+
if (missing.length > 0) {
|
|
251
|
+
throw new EkwoMcpError(`unknown_account: ${missing.join(', ')} — not an account of this company. Use list_accounts to find the right code.`);
|
|
252
|
+
}
|
|
253
|
+
return inSchema('budgets', async () => {
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
await backend.remove('lines', [
|
|
256
|
+
{ column: 'budget_id', op: 'eq', value: budgetIdValue },
|
|
257
|
+
{ column: 'account_id', op: 'eq', value: byCode.get(line.account_code) },
|
|
258
|
+
{ column: 'period_start', op: 'eq', value: line.period_start },
|
|
259
|
+
{ column: 'period_end', op: 'eq', value: line.period_end },
|
|
260
|
+
], 'budgets');
|
|
261
|
+
}
|
|
262
|
+
const written = await backend.insert('lines', lines.map((line) => ({
|
|
263
|
+
budget_id: budgetIdValue,
|
|
264
|
+
company_id: companyIdValue,
|
|
265
|
+
account_id: byCode.get(line.account_code),
|
|
266
|
+
period_start: line.period_start,
|
|
267
|
+
period_end: line.period_end,
|
|
268
|
+
amount: line.amount,
|
|
269
|
+
note: line.note ?? null,
|
|
270
|
+
})), ['id'], 'budgets');
|
|
271
|
+
return { written: written.length };
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
verb: 'variance',
|
|
277
|
+
title: 'Budget against ledger',
|
|
278
|
+
readOnly: true,
|
|
279
|
+
description: 'What was planned against what was booked, per account, over a period. Both figures are in the sign a business states them in, and the variance is the actual less the plan — so a negative figure on an income account is revenue that did not arrive, and a positive one on a cost account is overspending. It reads posted entries of kind `normal`: a closing or appropriation entry is not what a period earned. A budget line is taken whole or not at all, so ask for a window that contains the periods the budget was written in.',
|
|
280
|
+
input: z.object({
|
|
281
|
+
company_id: companyId,
|
|
282
|
+
budget_id: uuid,
|
|
283
|
+
from: isoDate,
|
|
284
|
+
to: isoDate,
|
|
285
|
+
}),
|
|
286
|
+
async run(backend, args) {
|
|
287
|
+
const rows = await inSchema('budgets', () => backend.rpc('variance', {
|
|
288
|
+
p_company_id: args['company_id'],
|
|
289
|
+
p_budget_id: args['budget_id'],
|
|
290
|
+
p_from: args['from'],
|
|
291
|
+
p_to: args['to'],
|
|
292
|
+
}, 'budgets'));
|
|
293
|
+
return {
|
|
294
|
+
from: args['from'],
|
|
295
|
+
to: args['to'],
|
|
296
|
+
lines: rows.map((row) => ({
|
|
297
|
+
...row,
|
|
298
|
+
budget: money(row['budget']),
|
|
299
|
+
actual: money(row['actual']),
|
|
300
|
+
variance: money(row['variance']),
|
|
301
|
+
})),
|
|
302
|
+
};
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
/** Every module this release knows how to expose, by module code. */
|
|
308
|
+
export const MODULE_TOOLSETS = [ASSETS, BUDGETS];
|
|
309
|
+
/** The toolsets for a set of installed module codes, in a fixed order. */
|
|
310
|
+
export function toolsetsFor(installed) {
|
|
311
|
+
const wanted = new Set(installed);
|
|
312
|
+
return MODULE_TOOLSETS.filter((toolset) => wanted.has(toolset.code));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Which modules this installation carries.
|
|
316
|
+
*
|
|
317
|
+
* `public.modules` is the registry, and it is readable by anyone signed in —
|
|
318
|
+
* it says what exists here, not what any company holds. A database that
|
|
319
|
+
* predates the module framework has no such table, and the answer is then no
|
|
320
|
+
* modules rather than an error.
|
|
321
|
+
*/
|
|
322
|
+
export async function installedModules(backend) {
|
|
323
|
+
try {
|
|
324
|
+
const rows = await backend.select({
|
|
325
|
+
table: 'modules',
|
|
326
|
+
columns: ['code', 'status'],
|
|
327
|
+
order: [{ column: 'code' }],
|
|
328
|
+
});
|
|
329
|
+
return rows.filter((row) => row.status !== 'draft').map((row) => row.code);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
//# sourceMappingURL=modules.js.map
|