@amalgm/tools 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PURPOSE.md +7 -0
- package/README.md +8 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/notifications-http.d.ts +12 -0
- package/dist/notifications-http.js +38 -0
- package/dist/notifications.d.ts +42 -0
- package/dist/notifications.js +108 -0
- package/dist/proxy-email.d.ts +9 -0
- package/dist/proxy-email.js +62 -0
- package/package.json +1 -1
package/PURPOSE.md
CHANGED
|
@@ -28,6 +28,8 @@ registry or execution implementation.
|
|
|
28
28
|
- A **driver** executes actions for one tool type.
|
|
29
29
|
- A **system tool** is an embedder-owned definition projected into the catalog;
|
|
30
30
|
it is not user state.
|
|
31
|
+
- A **notification** is one request to inform the current user; a channel
|
|
32
|
+
adapter turns that intent into delivery.
|
|
31
33
|
|
|
32
34
|
## Axioms
|
|
33
35
|
|
|
@@ -74,6 +76,11 @@ registry or execution implementation.
|
|
|
74
76
|
18. Tools may store opaque references owned by another product, but neither
|
|
75
77
|
Tools nor a Core adapter silently rewrites that product. A cross-product
|
|
76
78
|
workflow has one explicit product owner.
|
|
79
|
+
19. `notifications.notify_user` addresses only the current user. Callers may
|
|
80
|
+
supply content and severity, never a recipient or delivery credential.
|
|
81
|
+
20. Notification channels are host capabilities. Tools owns validation and
|
|
82
|
+
result semantics; an injected channel adapter owns formatting, recipient
|
|
83
|
+
lookup, credentials, and delivery.
|
|
77
84
|
|
|
78
85
|
## Predictable behavior
|
|
79
86
|
|
package/README.md
CHANGED
|
@@ -65,6 +65,14 @@ The read adapter serves the Engine-compatible `GET /toolbox`,
|
|
|
65
65
|
the same live Toolbox used by CLI and MCP consumers. Mutation routes return
|
|
66
66
|
405 until their legacy record fields can be represented without data loss.
|
|
67
67
|
|
|
68
|
+
## Notifications
|
|
69
|
+
|
|
70
|
+
`Notifications` projects the first-party `notifications.notify_user` action
|
|
71
|
+
into the Toolbox. The action validates one channel-neutral request and hands
|
|
72
|
+
it to an injected email delivery adapter; recipient lookup and credentials
|
|
73
|
+
never enter the tool record. `createNotificationsHttpServer` exposes
|
|
74
|
+
`GET /email` as the channel capability document used by the shell.
|
|
75
|
+
|
|
68
76
|
## Tool types
|
|
69
77
|
|
|
70
78
|
- `cli`: executed directly with `spawn`; a shell is never involved.
|
package/dist/http.d.ts
CHANGED
|
@@ -29,3 +29,5 @@ declare function createToolboxHttpServer(options?: ToolboxOptions & {
|
|
|
29
29
|
}): ToolboxHttpServer;
|
|
30
30
|
export { catalogDocument, createToolboxHttpServer };
|
|
31
31
|
export type { ToolboxHttpServer };
|
|
32
|
+
export { createNotificationsHttpServer } from './notifications-http.js';
|
|
33
|
+
export type { NotificationsHttpServer } from './notifications-http.js';
|
package/dist/http.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,9 @@ export type { ToolboxHttpServer } from './http.js';
|
|
|
7
7
|
export { actionId, actionName, id, mcpName } from './ids.js';
|
|
8
8
|
export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, } from './mcp.js';
|
|
9
9
|
export { createMcpServer } from './mcp-server.js';
|
|
10
|
+
export { Notifications, createNotificationsDriver, notificationToolDefinition, } from './notifications.js';
|
|
11
|
+
export type { EmailDelivery, EmailDeliveryReceipt, EmailNotificationRequest, NotificationLevel, NotificationsOptions, } from './notifications.js';
|
|
12
|
+
export { createProxyEmailDelivery } from './proxy-email.js';
|
|
13
|
+
export type { ProxyEmailDeliveryOptions } from './proxy-email.js';
|
|
10
14
|
export { Toolbox } from './toolbox.js';
|
|
11
15
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,6 @@ export { catalogDocument, createToolboxHttpServer } from './http.js';
|
|
|
5
5
|
export { actionId, actionName, id, mcpName } from './ids.js';
|
|
6
6
|
export { actionTools, callMcpTool, createMcpTools, findMcpTool, managementTools, } from './mcp.js';
|
|
7
7
|
export { createMcpServer } from './mcp-server.js';
|
|
8
|
+
export { Notifications, createNotificationsDriver, notificationToolDefinition, } from './notifications.js';
|
|
9
|
+
export { createProxyEmailDelivery } from './proxy-email.js';
|
|
8
10
|
export { Toolbox } from './toolbox.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Notifications } from './notifications.js';
|
|
2
|
+
interface NotificationsHttpServer {
|
|
3
|
+
listen(port?: number, host?: string): Promise<{
|
|
4
|
+
port: number;
|
|
5
|
+
}>;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
declare function createNotificationsHttpServer(options: {
|
|
9
|
+
notifications: Notifications;
|
|
10
|
+
}): NotificationsHttpServer;
|
|
11
|
+
export { createNotificationsHttpServer };
|
|
12
|
+
export type { NotificationsHttpServer };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
function createNotificationsHttpServer(options) {
|
|
3
|
+
const server = createServer((request, response) => {
|
|
4
|
+
const url = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
5
|
+
const send = (status, body) => {
|
|
6
|
+
response.writeHead(status, { 'content-type': 'application/json' });
|
|
7
|
+
response.end(JSON.stringify(body));
|
|
8
|
+
};
|
|
9
|
+
if (request.method === 'GET' && url.pathname === '/email') {
|
|
10
|
+
send(200, options.notifications.document());
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
send(404, { error: 'not found' });
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
listen(port = 0, host = '127.0.0.1') {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
server.once('error', reject);
|
|
19
|
+
server.listen(port, host, () => {
|
|
20
|
+
server.off('error', reject);
|
|
21
|
+
const address = server.address();
|
|
22
|
+
if (!address || typeof address === 'string')
|
|
23
|
+
throw new Error('Notifications HTTP server has no port');
|
|
24
|
+
resolve({ port: address.port });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
close() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
server.close((error) => { if (error)
|
|
31
|
+
reject(error);
|
|
32
|
+
else
|
|
33
|
+
resolve(); });
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export { createNotificationsHttpServer };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ToolDefinition, ToolDriver, ToolResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Outbound notification law ported from Engine
|
|
4
|
+
* `runtime/scripts/amalgm-mcp/notify/index.js` and its canonical Toolbox ids
|
|
5
|
+
* in `runtime/scripts/amalgm-mcp/server/core-tools.js`.
|
|
6
|
+
*/
|
|
7
|
+
type NotificationLevel = 'info' | 'warning' | 'error' | 'success';
|
|
8
|
+
interface EmailNotificationRequest {
|
|
9
|
+
subject: string;
|
|
10
|
+
message: string;
|
|
11
|
+
level: NotificationLevel;
|
|
12
|
+
link?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
}
|
|
15
|
+
interface EmailDeliveryReceipt {
|
|
16
|
+
email: string;
|
|
17
|
+
replyTo?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
}
|
|
20
|
+
interface EmailDelivery {
|
|
21
|
+
send(request: EmailNotificationRequest): Promise<EmailDeliveryReceipt>;
|
|
22
|
+
}
|
|
23
|
+
interface NotificationsOptions {
|
|
24
|
+
email?: EmailDelivery;
|
|
25
|
+
}
|
|
26
|
+
declare function notificationToolDefinition(): ToolDefinition;
|
|
27
|
+
declare class Notifications {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(options?: NotificationsOptions);
|
|
30
|
+
document(): {
|
|
31
|
+
service: string;
|
|
32
|
+
actions: string[];
|
|
33
|
+
channels: {
|
|
34
|
+
id: string;
|
|
35
|
+
configured: boolean;
|
|
36
|
+
}[];
|
|
37
|
+
};
|
|
38
|
+
notify(input: Record<string, unknown>, context?: unknown): Promise<ToolResult>;
|
|
39
|
+
}
|
|
40
|
+
declare function createNotificationsDriver(notifications: Notifications): ToolDriver;
|
|
41
|
+
export { Notifications, createNotificationsDriver, notificationToolDefinition };
|
|
42
|
+
export type { EmailDelivery, EmailDeliveryReceipt, EmailNotificationRequest, NotificationLevel, NotificationsOptions, };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { errorResult } from './results.js';
|
|
2
|
+
const LEVELS = new Set(['info', 'warning', 'error', 'success']);
|
|
3
|
+
function nonempty(value, label) {
|
|
4
|
+
const result = typeof value === 'string' ? value.trim() : '';
|
|
5
|
+
if (!result)
|
|
6
|
+
throw new Error(`${label} is required`);
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
function optional(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
11
|
+
}
|
|
12
|
+
function contextSessionId(value) {
|
|
13
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
14
|
+
return undefined;
|
|
15
|
+
const context = value;
|
|
16
|
+
return optional(context.callerSessionId) || optional(context.sessionId);
|
|
17
|
+
}
|
|
18
|
+
function notificationToolDefinition() {
|
|
19
|
+
return {
|
|
20
|
+
id: 'notifications',
|
|
21
|
+
name: 'Notifications',
|
|
22
|
+
description: 'Notify the current user about important results, completions, and failures.',
|
|
23
|
+
owner: 'amalgm',
|
|
24
|
+
origin: 'system',
|
|
25
|
+
source: { type: 'mcp', transport: 'host', serverName: 'notifications' },
|
|
26
|
+
actions: [{
|
|
27
|
+
name: 'notify_user',
|
|
28
|
+
description: 'Send the current user an email notification. Keep it concise and actionable.',
|
|
29
|
+
target: { name: 'notify_user' },
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
subject: { type: 'string', description: 'Short subject; defaults to the first 80 message characters.' },
|
|
34
|
+
message: { type: 'string', description: 'Notification body.' },
|
|
35
|
+
level: { type: 'string', enum: [...LEVELS], description: 'Severity; defaults to info.' },
|
|
36
|
+
link: { type: 'string', description: 'Optional HTTP(S) details URL.' },
|
|
37
|
+
},
|
|
38
|
+
required: ['message'],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
},
|
|
41
|
+
}],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
class Notifications {
|
|
45
|
+
#email;
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
this.#email = options.email;
|
|
48
|
+
}
|
|
49
|
+
document() {
|
|
50
|
+
return {
|
|
51
|
+
service: 'notifications',
|
|
52
|
+
actions: ['notifications.notify_user'],
|
|
53
|
+
channels: [{ id: 'email', configured: Boolean(this.#email) }],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async notify(input, context) {
|
|
57
|
+
const message = nonempty(input.message, 'message');
|
|
58
|
+
const rawLevel = optional(input.level) || 'info';
|
|
59
|
+
if (!LEVELS.has(rawLevel))
|
|
60
|
+
throw new Error(`Unsupported notification level: ${rawLevel}`);
|
|
61
|
+
const subject = (optional(input.subject) || message.slice(0, 80).replace(/\n/g, ' ')).slice(0, 80);
|
|
62
|
+
const link = optional(input.link);
|
|
63
|
+
if (link && !['http:', 'https:'].includes(new URL(link).protocol)) {
|
|
64
|
+
throw new Error('link must use http or https');
|
|
65
|
+
}
|
|
66
|
+
if (!this.#email)
|
|
67
|
+
throw new Error('Email notification delivery is not configured');
|
|
68
|
+
const level = rawLevel;
|
|
69
|
+
const sessionId = contextSessionId(context);
|
|
70
|
+
const receipt = await this.#email.send({
|
|
71
|
+
subject,
|
|
72
|
+
message,
|
|
73
|
+
level,
|
|
74
|
+
...(link ? { link } : {}),
|
|
75
|
+
...(sessionId ? { sessionId } : {}),
|
|
76
|
+
});
|
|
77
|
+
const reply = receipt.replyTo ? ' (replies route to this session)' : '';
|
|
78
|
+
return {
|
|
79
|
+
content: [{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: `Notification sent to ${receipt.email}${reply}.\n\nMessage: ${message}\nLevel: ${level}${link ? `\nLink: ${link}` : ''}`,
|
|
82
|
+
}],
|
|
83
|
+
structuredContent: { ok: true, channel: 'email', ...receipt },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function createNotificationsDriver(notifications) {
|
|
88
|
+
return {
|
|
89
|
+
type: 'mcp',
|
|
90
|
+
supports(tool) {
|
|
91
|
+
return tool.id === 'notifications'
|
|
92
|
+
&& tool.source.type === 'mcp'
|
|
93
|
+
&& tool.source.transport === 'host'
|
|
94
|
+
&& tool.source.serverName === 'notifications';
|
|
95
|
+
},
|
|
96
|
+
async call({ action, input, options }) {
|
|
97
|
+
if (action.id !== 'notifications.notify_user')
|
|
98
|
+
return errorResult(`Unknown notification action: ${action.id}`);
|
|
99
|
+
try {
|
|
100
|
+
return await notifications.notify(input, options.context);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return errorResult(error);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export { Notifications, createNotificationsDriver, notificationToolDefinition };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmailDelivery } from './notifications.js';
|
|
2
|
+
interface ProxyEmailDeliveryOptions {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
token: string;
|
|
5
|
+
fetch?: typeof globalThis.fetch;
|
|
6
|
+
}
|
|
7
|
+
declare function createProxyEmailDelivery(options: ProxyEmailDeliveryOptions): EmailDelivery;
|
|
8
|
+
export { createProxyEmailDelivery };
|
|
9
|
+
export type { ProxyEmailDeliveryOptions };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
function escapeHtml(value) {
|
|
2
|
+
return value
|
|
3
|
+
.replaceAll('&', '&')
|
|
4
|
+
.replaceAll('<', '<')
|
|
5
|
+
.replaceAll('>', '>')
|
|
6
|
+
.replaceAll('"', '"')
|
|
7
|
+
.replaceAll("'", ''');
|
|
8
|
+
}
|
|
9
|
+
function emailHtml(message, link) {
|
|
10
|
+
const body = escapeHtml(message).replace(/\n/g, '<br>');
|
|
11
|
+
const details = link
|
|
12
|
+
? `<p><a href="${escapeHtml(link)}">View details</a></p>`
|
|
13
|
+
: '';
|
|
14
|
+
return `<div>${body}</div>${details}`;
|
|
15
|
+
}
|
|
16
|
+
function notifyEndpoint(raw) {
|
|
17
|
+
const value = raw.trim().replace(/\/+$/, '').replace(/\/anthropic$/, '');
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
20
|
+
throw new Error('Notification proxy must use http or https');
|
|
21
|
+
if (url.username || url.password)
|
|
22
|
+
throw new Error('Notification proxy URL cannot contain credentials');
|
|
23
|
+
return `${value}/notify`;
|
|
24
|
+
}
|
|
25
|
+
function createProxyEmailDelivery(options) {
|
|
26
|
+
const endpoint = notifyEndpoint(options.baseUrl);
|
|
27
|
+
const token = options.token.trim();
|
|
28
|
+
if (!token)
|
|
29
|
+
throw new Error('Notification proxy token is required');
|
|
30
|
+
const request = options.fetch ?? globalThis.fetch;
|
|
31
|
+
return {
|
|
32
|
+
async send(notification) {
|
|
33
|
+
const response = await request(endpoint, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
authorization: `Bearer ${token}`,
|
|
37
|
+
'content-type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
subject: notification.subject,
|
|
41
|
+
html: emailHtml(notification.message, notification.link),
|
|
42
|
+
text: notification.message,
|
|
43
|
+
...(notification.sessionId ? { sessionId: notification.sessionId } : {}),
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const detail = (await response.text().catch(() => '')).slice(0, 200);
|
|
48
|
+
throw new Error(`Notification proxy failed: ${response.status}${detail ? ` ${detail}` : ''}`);
|
|
49
|
+
}
|
|
50
|
+
const body = await response.json();
|
|
51
|
+
const email = typeof body.email === 'string' && body.email.trim() ? body.email.trim() : '';
|
|
52
|
+
if (!email)
|
|
53
|
+
throw new Error('Notification proxy response did not identify the recipient');
|
|
54
|
+
return {
|
|
55
|
+
email,
|
|
56
|
+
...(typeof body.replyTo === 'string' && body.replyTo ? { replyTo: body.replyTo } : {}),
|
|
57
|
+
...(typeof body.id === 'string' && body.id ? { id: body.id } : {}),
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export { createProxyEmailDelivery };
|