@treenity/mods 3.0.2 → 3.0.4

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.
Files changed (135) hide show
  1. package/agent/client.ts +2 -0
  2. package/agent/guardian.ts +492 -0
  3. package/agent/seed.ts +74 -0
  4. package/agent/server.ts +4 -0
  5. package/agent/service.ts +644 -0
  6. package/agent/types.ts +184 -0
  7. package/agent/view.tsx +431 -0
  8. package/dist/agent/client.d.ts +3 -0
  9. package/dist/agent/client.d.ts.map +1 -0
  10. package/dist/agent/client.js +3 -0
  11. package/dist/agent/client.js.map +1 -0
  12. package/dist/agent/guardian.d.ts +47 -0
  13. package/dist/agent/guardian.d.ts.map +1 -0
  14. package/dist/agent/guardian.js +452 -0
  15. package/dist/agent/guardian.js.map +1 -0
  16. package/dist/agent/seed.d.ts +2 -0
  17. package/dist/agent/seed.d.ts.map +1 -0
  18. package/dist/agent/seed.js +68 -0
  19. package/dist/agent/seed.js.map +1 -0
  20. package/dist/agent/server.d.ts +5 -0
  21. package/dist/agent/server.d.ts.map +1 -0
  22. package/dist/agent/server.js +5 -0
  23. package/dist/agent/server.js.map +1 -0
  24. package/dist/agent/service.d.ts +2 -0
  25. package/dist/agent/service.d.ts.map +1 -0
  26. package/dist/agent/service.js +556 -0
  27. package/dist/agent/service.js.map +1 -0
  28. package/dist/agent/types.d.ts +115 -0
  29. package/dist/agent/types.d.ts.map +1 -0
  30. package/dist/agent/types.js +168 -0
  31. package/dist/agent/types.js.map +1 -0
  32. package/dist/agent/view.d.ts +2 -0
  33. package/dist/agent/view.d.ts.map +1 -0
  34. package/dist/agent/view.js +137 -0
  35. package/dist/agent/view.js.map +1 -0
  36. package/dist/mcp/mcp-server.d.ts +16 -0
  37. package/dist/mcp/mcp-server.d.ts.map +1 -0
  38. package/dist/mcp/mcp-server.js +344 -0
  39. package/dist/mcp/mcp-server.js.map +1 -0
  40. package/dist/mcp/server.d.ts +3 -0
  41. package/dist/mcp/server.d.ts.map +1 -0
  42. package/dist/mcp/server.js +3 -0
  43. package/dist/mcp/server.js.map +1 -0
  44. package/dist/mcp/service.d.ts +2 -0
  45. package/dist/mcp/service.d.ts.map +1 -0
  46. package/dist/mcp/service.js +16 -0
  47. package/dist/mcp/service.js.map +1 -0
  48. package/dist/mcp/types.d.ts +4 -0
  49. package/dist/mcp/types.d.ts.map +1 -0
  50. package/dist/mcp/types.js +6 -0
  51. package/dist/mcp/types.js.map +1 -0
  52. package/dist/metatron/claude.d.ts +30 -0
  53. package/dist/metatron/claude.d.ts.map +1 -0
  54. package/dist/metatron/claude.js +201 -0
  55. package/dist/metatron/claude.js.map +1 -0
  56. package/dist/metatron/client.d.ts +3 -0
  57. package/dist/metatron/client.d.ts.map +1 -0
  58. package/dist/metatron/client.js +3 -0
  59. package/dist/metatron/client.js.map +1 -0
  60. package/dist/metatron/mentions.d.ts +9 -0
  61. package/dist/metatron/mentions.d.ts.map +1 -0
  62. package/dist/metatron/mentions.js +21 -0
  63. package/dist/metatron/mentions.js.map +1 -0
  64. package/dist/metatron/permissions.d.ts +16 -0
  65. package/dist/metatron/permissions.d.ts.map +1 -0
  66. package/dist/metatron/permissions.js +52 -0
  67. package/dist/metatron/permissions.js.map +1 -0
  68. package/dist/metatron/seed.d.ts +2 -0
  69. package/dist/metatron/seed.d.ts.map +1 -0
  70. package/dist/metatron/seed.js +41 -0
  71. package/dist/metatron/seed.js.map +1 -0
  72. package/dist/metatron/server.d.ts +4 -0
  73. package/dist/metatron/server.d.ts.map +1 -0
  74. package/dist/metatron/server.js +4 -0
  75. package/dist/metatron/server.js.map +1 -0
  76. package/dist/metatron/service.d.ts +2 -0
  77. package/dist/metatron/service.d.ts.map +1 -0
  78. package/dist/metatron/service.js +361 -0
  79. package/dist/metatron/service.js.map +1 -0
  80. package/dist/metatron/types.d.ts +76 -0
  81. package/dist/metatron/types.d.ts.map +1 -0
  82. package/dist/metatron/types.js +112 -0
  83. package/dist/metatron/types.js.map +1 -0
  84. package/dist/metatron/view.d.ts +4 -0
  85. package/dist/metatron/view.d.ts.map +1 -0
  86. package/dist/metatron/view.js +5 -0
  87. package/dist/metatron/view.js.map +1 -0
  88. package/dist/metatron/views/config.d.ts +2 -0
  89. package/dist/metatron/views/config.d.ts.map +1 -0
  90. package/dist/metatron/views/config.js +116 -0
  91. package/dist/metatron/views/config.js.map +1 -0
  92. package/dist/metatron/views/log.d.ts +18 -0
  93. package/dist/metatron/views/log.d.ts.map +1 -0
  94. package/dist/metatron/views/log.js +224 -0
  95. package/dist/metatron/views/log.js.map +1 -0
  96. package/dist/metatron/views/shared.d.ts +13 -0
  97. package/dist/metatron/views/shared.d.ts.map +1 -0
  98. package/dist/metatron/views/shared.js +33 -0
  99. package/dist/metatron/views/shared.js.map +1 -0
  100. package/dist/metatron/views/task.d.ts +4 -0
  101. package/dist/metatron/views/task.d.ts.map +1 -0
  102. package/dist/metatron/views/task.js +106 -0
  103. package/dist/metatron/views/task.js.map +1 -0
  104. package/dist/metatron/views/workspace.d.ts +2 -0
  105. package/dist/metatron/views/workspace.d.ts.map +1 -0
  106. package/dist/metatron/views/workspace.js +138 -0
  107. package/dist/metatron/views/workspace.js.map +1 -0
  108. package/mcp/mcp-server.ts +393 -0
  109. package/mcp/server.ts +2 -0
  110. package/mcp/service.ts +18 -0
  111. package/mcp/types.ts +6 -0
  112. package/metatron/CLAUDE.md +22 -0
  113. package/metatron/claude.ts +258 -0
  114. package/metatron/client.ts +2 -0
  115. package/metatron/mentions.ts +31 -0
  116. package/metatron/permissions.ts +76 -0
  117. package/metatron/seed.ts +50 -0
  118. package/metatron/server.ts +3 -0
  119. package/metatron/service.ts +406 -0
  120. package/metatron/types.ts +120 -0
  121. package/metatron/view.tsx +4 -0
  122. package/metatron/views/config.tsx +408 -0
  123. package/metatron/views/log.tsx +412 -0
  124. package/metatron/views/shared.tsx +40 -0
  125. package/metatron/views/task.tsx +255 -0
  126. package/metatron/views/workspace.tsx +418 -0
  127. package/package.json +6 -2
  128. package/dist/mindmap/radial-tree.d.ts +0 -14
  129. package/dist/mindmap/radial-tree.d.ts.map +0 -1
  130. package/dist/mindmap/radial-tree.js +0 -184
  131. package/dist/mindmap/radial-tree.js.map +0 -1
  132. package/dist/mindmap/use-tree-data.d.ts +0 -14
  133. package/dist/mindmap/use-tree-data.d.ts.map +0 -1
  134. package/dist/mindmap/use-tree-data.js +0 -95
  135. package/dist/mindmap/use-tree-data.js.map +0 -1
@@ -0,0 +1,393 @@
1
+ // Treenity MCP Server — exposes tree store as MCP tools
2
+ // StreamableHTTP transport, stateless, token auth via ?token= or Authorization header
3
+
4
+ import { requestApproval } from '#agent/guardian';
5
+ import { matchesAny } from '@treenity/core/glob';
6
+ import { AiPolicy } from '#agent/types';
7
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9
+ import { createNode, getComponent } from '@treenity/core';
10
+ import { verifyViewSource } from '@treenity/core/mods/uix/verify';
11
+ import { TypeCatalog } from '@treenity/core/schema/catalog';
12
+ import { executeAction } from '@treenity/core/server/actions';
13
+ import { buildClaims, resolveToken, type Session, withAcl } from '@treenity/core/server/auth';
14
+ import { deployPrefab } from '@treenity/core/server/prefab';
15
+ import type { Tree } from '@treenity/core/tree';
16
+ import { existsSync, readFileSync } from 'node:fs';
17
+ import { createServer, type Server } from 'node:http';
18
+ import { createServer as createHttpsServer } from 'node:https';
19
+ import { homedir } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { z } from 'zod/v3';
22
+
23
+ function loadDevCerts(): { key: Buffer; cert: Buffer } | null {
24
+ const home = homedir();
25
+ const keyPath = join(home, 'localhost+2-key.pem');
26
+ const certPath = join(home, 'localhost+2.pem');
27
+ if (!existsSync(keyPath) || !existsSync(certPath)) return null;
28
+ return { key: readFileSync(keyPath), cert: readFileSync(certPath) };
29
+ }
30
+
31
+ // Guardian policy check for MCP write operations
32
+ // Reads global policy from /agents/guardian, checks deny → escalate → allow
33
+ export type McpGuardianResult = { allowed: true } | { allowed: false; reason: string };
34
+
35
+
36
+ export async function checkMcpGuardian(store: Tree, toolName: string, input?: string): Promise<McpGuardianResult> {
37
+ try {
38
+ const guardianNode = await store.get('/agents/guardian');
39
+ if (!guardianNode) return { allowed: false, reason: 'no Guardian configured at /agents/guardian — all writes denied' };
40
+
41
+ const policy = getComponent(guardianNode, AiPolicy);
42
+ if (!policy || policy.$type !== 'ai.policy') return { allowed: false, reason: 'invalid Guardian policy type — writes denied' };
43
+
44
+ const allow = (policy.allow as string[]) ?? [];
45
+ const deny = (policy.deny as string[]) ?? [];
46
+ const escalate = (policy.escalate as string[]) ?? [];
47
+
48
+ if (matchesAny(deny, toolName)) return { allowed: false, reason: `denied by Guardian: ${toolName}` };
49
+
50
+ if (matchesAny(escalate, toolName)) {
51
+ const approved = await requestApproval(store, {
52
+ agentPath: '/agents/mcp',
53
+ role: 'mcp',
54
+ tool: toolName,
55
+ input: input?.slice(0, 500) ?? '',
56
+ reason: 'MCP tool requires approval',
57
+ });
58
+ return approved
59
+ ? { allowed: true }
60
+ : { allowed: false, reason: 'denied by human' };
61
+ }
62
+
63
+ if (matchesAny(allow, toolName)) return { allowed: true };
64
+
65
+ // Unknown tool via MCP → deny by default (safer than escalate for external callers)
66
+ return { allowed: false, reason: `not in Guardian allow list: ${toolName}. Use Treenity Agent Office for write operations.` };
67
+ } catch (err) {
68
+ console.error('[mcp-guardian] failed to check policy:', err);
69
+ return { allowed: false, reason: 'Guardian check failed — writes denied for safety' };
70
+ }
71
+ }
72
+
73
+ /** Compact YAML-like serializer — readable for LLMs, much less noisy than JSON */
74
+ function yaml(val: unknown, depth = 0, maxStr = 300): string {
75
+ const pad = ' '.repeat(depth);
76
+ if (val == null) return 'null';
77
+ if (typeof val === 'boolean' || typeof val === 'number') return String(val);
78
+ if (typeof val === 'string') {
79
+ if (maxStr < Infinity && val.length > maxStr) return JSON.stringify(val.slice(0, maxStr) + '…');
80
+ return /[\n\r:#\[\]{}",]/.test(val) || val === '' ? JSON.stringify(val) : val;
81
+ }
82
+ if (Array.isArray(val)) {
83
+ if (!val.length) return '[]';
84
+ if (val.every(v => typeof v !== 'object' || v == null)) {
85
+ const inline = `[${val.map(v => yaml(v, 0, maxStr)).join(', ')}]`;
86
+ if (inline.length < 80) return inline;
87
+ }
88
+ return val.map(item => {
89
+ if (typeof item !== 'object' || item == null) return `${pad}- ${yaml(item, 0, maxStr)}`;
90
+ const inner = yaml(item, depth + 1, maxStr);
91
+ const lines = inner.split('\n');
92
+ return lines.length === 1
93
+ ? `${pad}- ${lines[0].trimStart()}`
94
+ : `${pad}- ${lines[0].trimStart()}\n${lines.slice(1).map(l => `${pad} ${l.trimStart()}`).join('\n')}`;
95
+ }).join('\n');
96
+ }
97
+ if (typeof val === 'object') {
98
+ const entries = Object.entries(val as Record<string, unknown>);
99
+ if (!entries.length) return '{}';
100
+ return entries.map(([k, v]) => {
101
+ if (v != null && typeof v === 'object') {
102
+ const inner = yaml(v, depth + 1, maxStr);
103
+ if (!inner.includes('\n') && inner.length < 60) return `${pad}${k}: ${inner.trimStart()}`;
104
+ return `${pad}${k}:\n${inner}`;
105
+ }
106
+ return `${pad}${k}: ${yaml(v, 0, maxStr)}`;
107
+ }).join('\n');
108
+ }
109
+ return String(val);
110
+ }
111
+
112
+ const text = (s: string) => ({ content: [{ type: 'text' as const, text: s }] });
113
+ const catalog = new TypeCatalog();
114
+
115
+ function dataKeys(node: Record<string, unknown>) {
116
+ return Object.keys(node).filter(k => !k.startsWith('$'));
117
+ }
118
+
119
+ export async function buildMcpServer(store: Tree, session: Session, claims?: string[]) {
120
+ claims ??= await buildClaims(store, session.userId);
121
+ const aclStore = withAcl(store, session.userId, claims);
122
+
123
+ const mcp = new McpServer({ name: 'treenity', version: '1.0.0' });
124
+
125
+ mcp.registerTool(
126
+ 'get_node',
127
+ {
128
+ description: 'Read a node by path. Returns full untruncated values.',
129
+ inputSchema: { path: z.string() },
130
+ },
131
+ async ({ path }) => {
132
+ const node = await aclStore.get(path);
133
+ return text(node ? yaml(node, 0, Infinity) : `not found: ${path}`);
134
+ },
135
+ );
136
+
137
+ mcp.registerTool(
138
+ 'list_children',
139
+ {
140
+ description: 'List children of a node. Long string values may be truncated — use get_node for full data.',
141
+ inputSchema: {
142
+ path: z.string(),
143
+ depth: z.number().optional(),
144
+ detail: z.boolean().optional().describe('Show first-level fields and component types'),
145
+ full: z.boolean().optional().describe('Return complete YAML of each node'),
146
+ },
147
+ },
148
+ async ({ path, depth, detail, full }) => {
149
+ const ctx = { queryContextPath: path, userId: session.userId };
150
+ const result = await aclStore.getChildren(path, { depth }, ctx);
151
+ const { items, total, truncated } = result;
152
+ const truncNote = truncated ? '\n⚠️ Results truncated — ACL scan limit reached. Use query mounts for large collections.' : '';
153
+
154
+ if (full) return text(yaml({ total, truncated, items }));
155
+
156
+ if (detail) {
157
+ const lines = items.map(n => {
158
+ const name = n.$path.split('/').at(-1);
159
+ const keys = dataKeys(n);
160
+ const header = n.$type === 'dir' ? `${name}/` : `${name}: ${n.$type} [${keys.length}]`;
161
+ const fields = keys.map(k => {
162
+ const v = (n as Record<string, unknown>)[k];
163
+ if (v && typeof v === 'object' && '$type' in (v as object))
164
+ return ` ${k}: ${(v as Record<string, unknown>).$type}`;
165
+ if (Array.isArray(v)) return ` ${k}: [${v.length}]`;
166
+ const s = String(v);
167
+ return ` ${k}: ${s.length > 60 ? s.slice(0, 60) + '…' : s}`;
168
+ });
169
+ return header + (fields.length ? '\n' + fields.join('\n') : '');
170
+ });
171
+ return text(lines.join('\n') + `\n(${total} total)` + truncNote);
172
+ }
173
+
174
+ const lines = items.map(n => {
175
+ const name = n.$path.split('/').at(-1);
176
+ if (n.$type === 'dir') return `${name}/`;
177
+ return `${name} ${n.$type} [${dataKeys(n).length}]`;
178
+ });
179
+ return text(lines.join('\n') + (total > items.length ? `\n(${total} total)` : '') + truncNote);
180
+ },
181
+ );
182
+
183
+ mcp.registerTool(
184
+ 'set_node',
185
+ {
186
+ description: 'Create or update a node. May require Guardian approval.',
187
+ inputSchema: {
188
+ path: z.string(),
189
+ type: z.string(),
190
+ components: z.record(z.unknown()).optional(),
191
+ acl: z.array(z.object({ g: z.string(), p: z.number() })).optional(),
192
+ owner: z.string().optional(),
193
+ },
194
+ },
195
+ async ({ path, type, components, acl, owner }) => {
196
+ const guard = await checkMcpGuardian(store, 'mcp__treenity__set_node', JSON.stringify({ path, type }));
197
+ if (!guard.allowed) return text(`🛑 Guardian: ${guard.reason}`);
198
+ const existing = await aclStore.get(path);
199
+ const node = existing ?? createNode(path, type);
200
+ if (!existing) node.$type = type;
201
+ if (components) {
202
+ for (const [k, v] of Object.entries(components)) {
203
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
204
+ const comp = v as Record<string, unknown> | null;
205
+ if (comp && typeof comp === 'object' && comp.$type === type) {
206
+ for (const [fk, fv] of Object.entries(comp)) {
207
+ if (fk !== '$type') node[fk] = fv;
208
+ }
209
+ continue;
210
+ }
211
+ node[k] = v;
212
+ }
213
+ }
214
+ if (acl) node.$acl = acl;
215
+ if (owner) node.$owner = owner;
216
+ await aclStore.set(node);
217
+ return text(yaml(node));
218
+ },
219
+ );
220
+
221
+ mcp.registerTool(
222
+ 'execute',
223
+ {
224
+ description: 'Execute an action on a node or component. Actions are methods registered on types. May require Guardian approval.',
225
+ inputSchema: {
226
+ path: z.string(),
227
+ action: z.string(),
228
+ type: z.string().optional(),
229
+ key: z.string().optional(),
230
+ data: z.record(z.unknown()).optional(),
231
+ },
232
+ },
233
+ async ({ path, action, type, key, data }) => {
234
+ const guard = await checkMcpGuardian(store, 'mcp__treenity__execute', JSON.stringify({ path, action }));
235
+ if (!guard.allowed) return text(`🛑 Guardian: ${guard.reason}`);
236
+ // Action-level Guardian check — allows operators to deny/escalate specific actions
237
+ // e.g. deny "mcp__treenity__execute:run" to block flow execution
238
+ const actionGuard = await checkMcpGuardian(store, `mcp__treenity__execute:${action}`, JSON.stringify({ path }));
239
+ if (!actionGuard.allowed) return text(`🛑 Guardian: ${actionGuard.reason}`);
240
+ const result = await executeAction(aclStore, path, type, key, action, data);
241
+ return text(yaml(result ?? { ok: true }));
242
+ },
243
+ );
244
+
245
+ mcp.registerTool(
246
+ 'deploy_prefab',
247
+ {
248
+ description: 'Deploy a module prefab (node tree template) to a target path. Idempotent: skips existing nodes. Browse available prefabs via list_children /sys/mods.',
249
+ inputSchema: {
250
+ source: z.string().describe('Prefab path: /sys/mods/{mod}/prefabs/{name}'),
251
+ target: z.string().describe('Target path where nodes will be created'),
252
+ allowAbsolute: z.boolean().optional().describe('Allow prefab to write outside target (e.g. /sys/autostart refs). Default: false'),
253
+ },
254
+ },
255
+ async ({ source, target, allowAbsolute }) => {
256
+ const guard = await checkMcpGuardian(store, 'mcp__treenity__deploy_prefab', JSON.stringify({ source, target }));
257
+ if (!guard.allowed) return text(`🛑 Guardian: ${guard.reason}`);
258
+ const result = await deployPrefab(aclStore, source, target, { allowAbsolute });
259
+ return text(yaml(result));
260
+ },
261
+ );
262
+
263
+ mcp.registerTool(
264
+ 'compile_view',
265
+ {
266
+ description: 'Verify that a UIX view source compiles correctly. Pass path to check an existing type node, or source to verify before writing.',
267
+ inputSchema: {
268
+ path: z.string().optional().describe('Path to type node (e.g. /sys/types/cosmos/system) — reads view.source'),
269
+ source: z.string().optional().describe('Raw JSX/TSX source to verify directly'),
270
+ },
271
+ },
272
+ async ({ path, source }) => {
273
+ let code = source;
274
+ if (!code) {
275
+ if (!path) return text('error: provide path or source');
276
+ const node = await aclStore.get(path);
277
+ if (!node) return text(`not found: ${path}`);
278
+ code = (node as any)?.view?.source;
279
+ if (!code || typeof code !== 'string') return text(`no view.source on ${path}`);
280
+ }
281
+ const result = verifyViewSource(code);
282
+ return text(yaml(result));
283
+ },
284
+ );
285
+
286
+ mcp.registerTool(
287
+ 'remove_node',
288
+ {
289
+ description: 'Remove a node by path. May be denied by Guardian.',
290
+ inputSchema: { path: z.string() },
291
+ },
292
+ async ({ path }) => {
293
+ const guard = await checkMcpGuardian(store, 'mcp__treenity__remove_node', path);
294
+ if (!guard.allowed) return text(`🛑 Guardian: ${guard.reason}`);
295
+ const ok = await aclStore.remove(path);
296
+ return text(ok ? `removed: ${path}` : `not found: ${path}`);
297
+ },
298
+ );
299
+
300
+ // ── Discovery tools — powered by TypeCatalog ──
301
+
302
+ mcp.registerTool(
303
+ 'catalog',
304
+ {
305
+ description: 'List all registered types with title, properties, and action names. Use this first to discover what types exist.',
306
+ inputSchema: {},
307
+ },
308
+ async () => text(yaml(catalog.list())),
309
+ );
310
+
311
+ mcp.registerTool(
312
+ 'describe_type',
313
+ {
314
+ description: 'Get full schema of a type: properties, actions with argument types, and cross-references to other types. Use after catalog to understand a specific type deeply.',
315
+ inputSchema: { type: z.string().describe('Type name, e.g. "cafe.contact" or "board.task"') },
316
+ },
317
+ async ({ type: typeName }) => {
318
+ const desc = catalog.describe(typeName);
319
+ return text(desc ? yaml(desc) : `type not found: ${typeName}`);
320
+ },
321
+ );
322
+
323
+ mcp.registerTool(
324
+ 'search_types',
325
+ {
326
+ description: 'Search types by keyword across names, titles, property names, and action names. Use to find types relevant to a task.',
327
+ inputSchema: { query: z.string().describe('Search keyword, e.g. "order", "mail", "contact"') },
328
+ },
329
+ async ({ query }) => text(yaml(catalog.search(query))),
330
+ );
331
+
332
+ return mcp;
333
+ }
334
+
335
+ export function extractToken(req: import('node:http').IncomingMessage): string | null {
336
+ const auth = req.headers.authorization;
337
+ if (typeof auth === 'string' && auth.startsWith('Bearer ')) return auth.slice(7);
338
+
339
+ const qs = (req.url ?? '').split('?')[1];
340
+ if (qs) {
341
+ const match = qs.match(/(?:^|&)token=([^&]+)/);
342
+ if (match) return decodeURIComponent(match[1]);
343
+ }
344
+ return null;
345
+ }
346
+
347
+ /** Create MCP HTTP server. Returns server handle for lifecycle management. */
348
+ export function createMcpHttpServer(store: Tree, port: number): Server {
349
+ const tls = loadDevCerts();
350
+
351
+ const handler = async (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => {
352
+ res.setHeader('Access-Control-Allow-Origin', '*');
353
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
354
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, mcp-session-id');
355
+ if (req.method === 'OPTIONS') {
356
+ res.writeHead(204);
357
+ res.end();
358
+ return;
359
+ }
360
+
361
+ const url = (req.url ?? '/').split('?')[0];
362
+ if (url !== '/mcp') {
363
+ res.writeHead(404);
364
+ res.end('not found');
365
+ return;
366
+ }
367
+
368
+ const token = extractToken(req);
369
+ let session: Session | null = null;
370
+ let devClaims: string[] | undefined;
371
+ if (token) {
372
+ session = await resolveToken(store, token);
373
+ } else if (!process.env.TENANT) {
374
+ console.warn('[mcp] ⚠️ DEV MODE: no TENANT set — all MCP requests have ADMIN access. Set TENANT env for production.');
375
+ session = { userId: 'mcp-dev' } as Session;
376
+ devClaims = ['u:mcp-dev', 'authenticated', 'admins'];
377
+ }
378
+ if (!session) {
379
+ res.writeHead(401, { 'Content-Type': 'text/plain' });
380
+ res.end('token required (?token= or Authorization: Bearer)');
381
+ return;
382
+ }
383
+
384
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
385
+ const mcp = await buildMcpServer(store, session, devClaims);
386
+ await mcp.connect(transport);
387
+ await transport.handleRequest(req, res);
388
+ };
389
+
390
+ const server = tls ? createHttpsServer(tls, handler) : createServer(handler);
391
+ server.listen(port, '127.0.0.1', () => console.log(`treenity mcp ${tls ? 'https' : 'http'}://localhost:${port}/mcp`));
392
+ return server;
393
+ }
package/mcp/server.ts ADDED
@@ -0,0 +1,2 @@
1
+ import './types';
2
+ import './service';
package/mcp/service.ts ADDED
@@ -0,0 +1,18 @@
1
+ // MCP autostart service — starts/stops the MCP HTTP server via tree lifecycle
2
+
3
+ import { getComponent, register } from '@treenity/core';
4
+ import { createMcpHttpServer } from './mcp-server';
5
+ import { McpConfig } from './types';
6
+
7
+ register('mcp.server', 'service', async (node, ctx) => {
8
+ const config = getComponent(node, McpConfig);
9
+ const port = config?.port ?? (Number(process.env.MCP_PORT) || 3212);
10
+ const server = createMcpHttpServer(ctx.tree, port);
11
+
12
+ return {
13
+ stop: async () => {
14
+ server.close();
15
+ console.log(`[mcp] stopped :${port}`);
16
+ },
17
+ };
18
+ });
package/mcp/types.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { registerType } from '@treenity/core/comp';
2
+
3
+ export class McpConfig {
4
+ port = 3212;
5
+ }
6
+ registerType('mcp.server', McpConfig);
@@ -0,0 +1,22 @@
1
+ # metatron
2
+
3
+ AI-оркестратор. Хранит Claude-диалоги как `metatron.task` ноды со структурированным `LogBlock[]` логом. Сервис следит за `running` тасками, собирает промпт из skills/memory/history, гонит Claude-turns. Параллельная обработка через `active: Set<string>`.
4
+
5
+ ## Types
6
+ - `metatron.config` — model, systemPrompt, service
7
+ - `metatron.task` — prompt, status, log[], actions: task/reply/approve
8
+ - `metatron.skill` — named prompt fragment
9
+ - `metatron.memory` — persistent context
10
+
11
+ ## Smart Permissions
12
+ - `ALLOWED_TOOLS` — generous whitelist, SDK auto-approves (never calls canUseTool)
13
+ - `NEEDS_APPROVAL` — only `remove_node` needs interactive user approval
14
+ - `canUseTool` — auto-allows anything not in NEEDS_APPROVAL; session memory auto-approves after first allow
15
+ - `metatron.permission` LogBlock — id, tool, input, status (pending/approved/denied)
16
+ - `approve` action on task — sets block status + resolves waiting canUseTool Promise
17
+ - Permission blocks flush immediately (bypass 2s debounce) for instant UI visibility
18
+
19
+ ## Parallel Execution
20
+ - `active: Set<string>` tracks running task paths (replaces `processing` boolean)
21
+ - `processRunning()` finds ALL running tasks not in active set, fires each independently (no await)
22
+ - Each `processTask()` self-contained: add to active → run → delete from active → recheck