@sendora/mcp 1.4.1 → 1.5.1
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 +6 -0
- package/dist/generated/docs.json +1 -1
- package/dist/generated/screens/domain-records.html +662 -0
- package/dist/generated/screens/message-timeline.html +711 -0
- package/dist/handler.js +4 -2
- package/dist/screens.js +68 -0
- package/dist/server.js +8 -1
- package/dist/tools/domains.js +5 -0
- package/dist/tools/messages.js +3 -0
- package/dist/tools/send.js +5 -2
- package/dist/version.js +1 -1
- package/package.json +7 -4
package/dist/handler.js
CHANGED
|
@@ -13,8 +13,10 @@ export const MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
|
13
13
|
* the oldest forgotten first. A key or an app's token is never kept: a
|
|
14
14
|
* request hands it on and drops it (docs/design/mcp.md §2.3); the exchange's
|
|
15
15
|
* own credential is kept for that minute. A revoked key or an ended
|
|
16
|
-
* connection may list tools for that minute
|
|
17
|
-
*
|
|
16
|
+
* connection may list tools for that minute, and every call that reaches the
|
|
17
|
+
* API is refused meanwhile. A connected app's `list_servers` answers from
|
|
18
|
+
* the kept exchange and never reaches the API, so it may still list servers
|
|
19
|
+
* the grant no longer reaches (docs/design/oauth.md §6.1).
|
|
18
20
|
*/
|
|
19
21
|
export class KeyCache {
|
|
20
22
|
#entries = new Map();
|
package/dist/screens.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
/*
|
|
3
|
+
* The two screens (docs/design/oauth.md §9), as MCP Apps: the extension
|
|
4
|
+
* io.modelcontextprotocol/ui. A tool names its screen in `_meta`, and a host
|
|
5
|
+
* that renders MCP Apps shows the tool's structured content in it; every
|
|
6
|
+
* other client reads the tool's text answer, which a screen leaves as it was.
|
|
7
|
+
* Each screen is one HTML document with its own script and style, which
|
|
8
|
+
* scripts/build-screens.ts builds from screens/. It loads nothing from
|
|
9
|
+
* outside, so it declares no origins and the host's strictest policy holds.
|
|
10
|
+
*/
|
|
11
|
+
export const SCREEN_MIME_TYPE = 'text/html;profile=mcp-app';
|
|
12
|
+
export const messageTimeline = {
|
|
13
|
+
name: 'message-timeline',
|
|
14
|
+
uri: 'ui://sendora/message-timeline',
|
|
15
|
+
title: 'Message timeline',
|
|
16
|
+
description: "A message's status for each recipient, its stream, and every event with its time and the receiver's answer.",
|
|
17
|
+
};
|
|
18
|
+
export const domainRecords = {
|
|
19
|
+
name: 'domain-records',
|
|
20
|
+
uri: 'ui://sendora/domain-records',
|
|
21
|
+
title: 'DNS records',
|
|
22
|
+
description: "A sending domain's two DNS records, whether each checks out and when they were last checked, with a button that checks again.",
|
|
23
|
+
};
|
|
24
|
+
export const screens = [messageTimeline, domainRecords];
|
|
25
|
+
/** The `_meta` of a tool shown in a screen: the extension's key, and the flat key that hosts read before it. */
|
|
26
|
+
export function shownIn(screen) {
|
|
27
|
+
return { ui: { resourceUri: screen.uri }, 'ui/resourceUri': screen.uri };
|
|
28
|
+
}
|
|
29
|
+
/** The host draws the border and the background, which a screen leaves transparent. */
|
|
30
|
+
const RESOURCE_META = { ui: { prefersBorder: true } };
|
|
31
|
+
const HOUR = { ttlMs: 3_600_000, cacheScope: 'public' };
|
|
32
|
+
const generated = new URL('./generated/screens/', import.meta.url);
|
|
33
|
+
const documents = new Map();
|
|
34
|
+
/** The screen's document beside this file: src/generated in the repository, dist/generated in the package. */
|
|
35
|
+
export function htmlOf(screen) {
|
|
36
|
+
let document = documents.get(screen.name);
|
|
37
|
+
if (document === undefined) {
|
|
38
|
+
try {
|
|
39
|
+
document = readFileSync(new URL(`${screen.name}.html`, generated), 'utf8');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new Error('The screens are missing; in the repository, run node apps/mcp/scripts/build-screens.ts', { cause: error });
|
|
43
|
+
}
|
|
44
|
+
documents.set(screen.name, document);
|
|
45
|
+
}
|
|
46
|
+
return document;
|
|
47
|
+
}
|
|
48
|
+
/** Registers each screen as a resource. The document is read when a host first asks for it. */
|
|
49
|
+
export function registerScreens(server, shown) {
|
|
50
|
+
for (const screen of shown) {
|
|
51
|
+
server.registerResource(screen.name, screen.uri, {
|
|
52
|
+
title: screen.title,
|
|
53
|
+
description: screen.description,
|
|
54
|
+
mimeType: SCREEN_MIME_TYPE,
|
|
55
|
+
_meta: RESOURCE_META,
|
|
56
|
+
cacheHint: HOUR,
|
|
57
|
+
}, () => ({
|
|
58
|
+
contents: [
|
|
59
|
+
{
|
|
60
|
+
uri: screen.uri,
|
|
61
|
+
mimeType: SCREEN_MIME_TYPE,
|
|
62
|
+
text: htmlOf(screen),
|
|
63
|
+
_meta: RESOURCE_META,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { registerDocuments } from './documents.js';
|
|
|
3
3
|
import { instructionsFor } from './instructions.js';
|
|
4
4
|
import { listed, targetsFor } from './listing.js';
|
|
5
5
|
import { registerPrompts } from './prompts.js';
|
|
6
|
+
import { registerScreens } from './screens.js';
|
|
6
7
|
import { toolTable } from './tool-table.js';
|
|
7
8
|
import { MCP_VERSION } from './version.js';
|
|
8
9
|
const HOUR = { ttlMs: 3_600_000, cacheScope: 'public' };
|
|
@@ -13,7 +14,8 @@ const MINUTE = { ttlMs: 60_000, cacheScope: 'private' };
|
|
|
13
14
|
* lists and no other, so a tool it may not use is unknown to it. Without a
|
|
14
15
|
* key every list is the documentation's, which changes only with a release,
|
|
15
16
|
* so anyone may cache it an hour; with one, what is listed depends on the
|
|
16
|
-
* key, so only this client may, for a minute (§4.1).
|
|
17
|
+
* key, so only this client may, for a minute (§4.1). A screen is registered
|
|
18
|
+
* where a tool that names it is (docs/design/oauth.md §9).
|
|
17
19
|
*/
|
|
18
20
|
export function buildServer(docs, connection) {
|
|
19
21
|
const lists = connection.kind === 'none' ? HOUR : MINUTE;
|
|
@@ -33,11 +35,16 @@ export function buildServer(docs, connection) {
|
|
|
33
35
|
'server/discover': lists,
|
|
34
36
|
},
|
|
35
37
|
});
|
|
38
|
+
const screens = new Set();
|
|
36
39
|
for (const tool of toolTable) {
|
|
37
40
|
if (listed(tool, connection)) {
|
|
38
41
|
tool.register(server, { docs, connection, targets: targetsFor(tool, connection) });
|
|
42
|
+
if (tool.screen !== undefined) {
|
|
43
|
+
screens.add(tool.screen);
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
}
|
|
47
|
+
registerScreens(server, screens);
|
|
41
48
|
registerDocuments(server, docs);
|
|
42
49
|
registerPrompts(server);
|
|
43
50
|
return server;
|
package/dist/tools/domains.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { toolError } from '../errors.js';
|
|
3
|
+
import { domainRecords, shownIn } from '../screens.js';
|
|
3
4
|
import { READING } from '../tool.js';
|
|
4
5
|
import { accountConnectionOf } from './servers.js';
|
|
5
6
|
/** A DNS record a domain needs and whether it was seen, for a sending domain and a domain for receiving alike. */
|
|
@@ -78,6 +79,7 @@ export const checkDomainTool = {
|
|
|
78
79
|
class: 'read',
|
|
79
80
|
reach: 'account',
|
|
80
81
|
operations: ['POST /v1/domains/{id}/verify'],
|
|
82
|
+
screen: domainRecords,
|
|
81
83
|
register: (server, { connection }) => server.registerTool('check_domain', {
|
|
82
84
|
title: 'Check a sending domain',
|
|
83
85
|
description: "Looks up both records of one of the account's sending domains now and answers the domain as it stands, with what each lookup found: ok, missing, mismatch or dns_error. Mail may go out from the domain once both are ok; a record just added may take a while to be seen. A verified domain whose record is found missing keeps sending for 24 hours (failingSince), and a dns_error changes nothing. A check that begins that warning or ends the verification mails the account's administrators, as Sendora's own checks do.",
|
|
@@ -86,6 +88,7 @@ export const checkDomainTool = {
|
|
|
86
88
|
}),
|
|
87
89
|
outputSchema: checkedDomainOutput,
|
|
88
90
|
annotations: { ...READING, openWorldHint: true },
|
|
91
|
+
_meta: shownIn(domainRecords),
|
|
89
92
|
}, async ({ domainId }) => {
|
|
90
93
|
try {
|
|
91
94
|
const domain = await accountConnectionOf(connection).account.domains.verify(domainId);
|
|
@@ -106,6 +109,7 @@ export const addDomainTool = {
|
|
|
106
109
|
class: 'write',
|
|
107
110
|
reach: 'account',
|
|
108
111
|
operations: ['POST /v1/domains'],
|
|
112
|
+
screen: domainRecords,
|
|
109
113
|
register: (server, { connection }) => server.registerTool('add_domain', {
|
|
110
114
|
title: 'Add a sending domain',
|
|
111
115
|
description: "Adds a domain the account sends from, with a DKIM key of its own, and answers the two records for the user to add to the domain's DNS: a CNAME for the return path, which also satisfies SPF, and a TXT record with the DKIM key. check_domain then looks them up, and every server of the account may send from the domain once both are seen. A domain the account already has is refused with domain_exists.",
|
|
@@ -121,6 +125,7 @@ export const addDomainTool = {
|
|
|
121
125
|
idempotentHint: false,
|
|
122
126
|
openWorldHint: false,
|
|
123
127
|
},
|
|
128
|
+
_meta: shownIn(domainRecords),
|
|
124
129
|
}, async (request) => {
|
|
125
130
|
try {
|
|
126
131
|
const domain = await accountConnectionOf(connection).account.domains.create(request);
|
package/dist/tools/messages.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { toolError } from '../errors.js';
|
|
3
|
+
import { messageTimeline, shownIn } from '../screens.js';
|
|
3
4
|
import { READING } from '../tool.js';
|
|
4
5
|
import { registerServerTool } from '../target.js';
|
|
5
6
|
const recipientStatus = z.enum(['queued', 'delivered', 'deferred', 'bounced', 'expired']);
|
|
@@ -124,6 +125,7 @@ export const getMessageTool = {
|
|
|
124
125
|
class: 'read',
|
|
125
126
|
reach: 'server',
|
|
126
127
|
operations: ['GET /v1/messages/{id}'],
|
|
128
|
+
screen: messageTimeline,
|
|
127
129
|
register: (server, { connection, targets }) => registerServerTool(server, targets, 'get_message', {
|
|
128
130
|
title: 'Read one message and its timeline',
|
|
129
131
|
description: "Reads one message of this server's log by its messageId: its recipients and their state, its attachments described but never their content, and every event on its timeline, oldest first, with the receiver's answer. The body is never returned.",
|
|
@@ -132,6 +134,7 @@ export const getMessageTool = {
|
|
|
132
134
|
}),
|
|
133
135
|
outputSchema: messageDetailOutput,
|
|
134
136
|
annotations: READING,
|
|
137
|
+
_meta: shownIn(messageTimeline),
|
|
135
138
|
}, async ({ messageId }, { sendora }) => {
|
|
136
139
|
try {
|
|
137
140
|
const message = await sendora.messages.get(messageId);
|
package/dist/tools/send.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SendoraError } from '@sendora/sdk';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { toolError } from '../errors.js';
|
|
4
|
+
import { messageTimeline, shownIn } from '../screens.js';
|
|
4
5
|
import { messageDetailOutput } from './messages.js';
|
|
5
6
|
import { sendInput } from './send-input.js';
|
|
6
7
|
import { modesOf, registerServerTool } from '../target.js';
|
|
@@ -82,7 +83,7 @@ export function notesFor(outcome, test, waitSeconds) {
|
|
|
82
83
|
}
|
|
83
84
|
return notes;
|
|
84
85
|
}
|
|
85
|
-
const
|
|
86
|
+
export const sentOutput = z.object({
|
|
86
87
|
messageId: z.string().describe('The id get_message and search_messages find it by.'),
|
|
87
88
|
submittedAt: z.string().describe('When the API accepted it.'),
|
|
88
89
|
test: z.boolean().describe('True when a test server took it: nobody receives it.'),
|
|
@@ -110,19 +111,21 @@ export const sendEmailTool = {
|
|
|
110
111
|
class: 'write',
|
|
111
112
|
reach: 'server',
|
|
112
113
|
operations: ['POST /v1/email', 'GET /v1/messages/{id}'],
|
|
114
|
+
screen: messageTimeline,
|
|
113
115
|
register: (server, { connection, targets }) => {
|
|
114
116
|
const modes = modesOf(targets);
|
|
115
117
|
registerServerTool(server, targets, 'send_email', {
|
|
116
118
|
title: 'Send one email',
|
|
117
119
|
description: descriptionFor(modes),
|
|
118
120
|
inputSchema: sendInput,
|
|
119
|
-
outputSchema,
|
|
121
|
+
outputSchema: sentOutput,
|
|
120
122
|
annotations: {
|
|
121
123
|
readOnlyHint: false,
|
|
122
124
|
destructiveHint: false,
|
|
123
125
|
idempotentHint: false,
|
|
124
126
|
openWorldHint: modes.includes('live'),
|
|
125
127
|
},
|
|
128
|
+
_meta: shownIn(messageTimeline),
|
|
126
129
|
}, async (input, { sendora }) => {
|
|
127
130
|
let accepted;
|
|
128
131
|
try {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** This package's version, held equal to its package.json by a test. */
|
|
2
|
-
export const MCP_VERSION = '1.
|
|
2
|
+
export const MCP_VERSION = '1.5.1';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sendora/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "The official MCP server for Sendora: its documentation and API for coding agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,22 +29,25 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@anthropic-ai/mcpb": "2.1.2",
|
|
32
|
+
"@axe-core/playwright": "^4.13.0",
|
|
32
33
|
"@modelcontextprotocol/client": "^2.1.0",
|
|
33
34
|
"@modelcontextprotocol/conformance": "0.2.0-alpha.11",
|
|
34
35
|
"@modelcontextprotocol/node": "^2.1.0",
|
|
36
|
+
"@playwright/test": "^1.63.0",
|
|
35
37
|
"esbuild": "^0.28.2",
|
|
36
38
|
"fast-check": "^4.10.2",
|
|
37
39
|
"fflate": "^0.8.3",
|
|
38
40
|
"typescript": "~7.0.2",
|
|
39
41
|
"vitest": "^4.1.11",
|
|
42
|
+
"@sendora/api": "0.0.0",
|
|
40
43
|
"@sendora/db": "0.0.0",
|
|
41
44
|
"@sendora/domain": "0.0.0",
|
|
42
45
|
"@sendora/observability": "0.0.0",
|
|
43
|
-
"@sendora/api": "0.0.0",
|
|
44
46
|
"@sendora/site": "0.0.0"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|
|
47
|
-
"build": "pnpm --filter @sendora/sdk build && node scripts/build-docs.ts && node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"fs.mkdirSync('dist/generated',{recursive:true});fs.copyFileSync('src/generated/docs.json','dist/generated/docs.json')\"",
|
|
48
|
-
"eval": "node eval/run.ts"
|
|
49
|
+
"build": "pnpm --filter @sendora/sdk build && node scripts/build-docs.ts && node scripts/build-screens.ts && node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node -e \"fs.mkdirSync('dist/generated',{recursive:true});fs.copyFileSync('src/generated/docs.json','dist/generated/docs.json');fs.cpSync('src/generated/screens','dist/generated/screens',{recursive:true})\"",
|
|
50
|
+
"eval": "node eval/run.ts",
|
|
51
|
+
"e2e": "playwright test"
|
|
49
52
|
}
|
|
50
53
|
}
|