@dokebi/mcp 0.0.0-stage → 0.1.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.
@@ -0,0 +1,161 @@
1
+ import { defineOptions, option as o } from '@dokebi/core';
2
+ import { appOption, routeIdsOption } from '@dokebi/web/points';
3
+ /*
4
+ * dokebi-mcp options (D-0094, D-0063 §3, D-0071 vocabulary): this declaration is both the parser
5
+ * definePlugin runs and options.schema.json, which the build writes from it.
6
+ */
7
+ /** A resource id of @dokebi/oauth (the audience of the tokens this route accepts). */
8
+ const RESOURCE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
9
+ /** The route path. */
10
+ const PATH = /^\/(?![/\\])[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
11
+ /** An origin: scheme, host and port, no path. */
12
+ const ORIGIN = /^https?:\/\/(?:[A-Za-z0-9.-]{1,253}|\[[0-9A-Fa-f:.]{2,45}\])(?::[0-9]{1,5})?$/;
13
+ /** A limit bucket (`read`, `write`, `trash`). */
14
+ export const BUCKET = /^[a-z][a-z0-9-]{0,31}$/;
15
+ /** A @dokebi/rate-limit policy name. */
16
+ const POLICY = /^[a-z][a-z0-9-]{0,31}$/;
17
+ /** Default of `uriMaxChars`: the longest resource URI of `resources/read` (R4 audit, 2026-10-26). */
18
+ export const URI_MAX_CHARS = 8_192;
19
+ /** The defaults of the image limits (D-0143), for `callTool` of @dokebi/mcp/testing too. */
20
+ export const IMAGE_LIMITS = Object.freeze({
21
+ imageMaxBytes: 1_048_576,
22
+ imageTotalMaxBytes: 4_194_304,
23
+ maxImages: 8,
24
+ });
25
+ const declaration = defineOptions({
26
+ resource: o.string({
27
+ pattern: RESOURCE_ID,
28
+ default: 'mcp',
29
+ description: 'The @dokebi/oauth resource this server is (options.dokebi-oauth.resources.<id>): only its tokens are accepted, and tool scopes must be its scopes',
30
+ }),
31
+ app: appOption('The app or apps of @dokebi/app whose hosts answer the MCP route (D-0132): mcp, or [mcp, api]. Other hosts get the 404 of an unknown route. Unset: every host'),
32
+ path: o.string({
33
+ pattern: PATH,
34
+ description: 'The route path; default the path of the resource URI (https://example.com/mcp → /mcp). Set it when a proxy changes the path',
35
+ }),
36
+ routeIds: routeIdsOption('dokebi-mcp', {
37
+ endpoint: 'the MCP route (POST, and the 405 answer of GET and DELETE, at path)',
38
+ }),
39
+ name: o.string({
40
+ minLength: 1,
41
+ maxLength: 200,
42
+ description: 'serverInfo.name; default the resource name',
43
+ }),
44
+ title: o.string({
45
+ minLength: 1,
46
+ maxLength: 200,
47
+ description: 'serverInfo.title (a human title for client screens)',
48
+ }),
49
+ version: o.string({
50
+ minLength: 1,
51
+ maxLength: 64,
52
+ default: '1.0.0',
53
+ description: 'serverInfo.version (your server, not the protocol)',
54
+ }),
55
+ instructions: o.string({
56
+ minLength: 1,
57
+ maxLength: 16_000,
58
+ description: 'Server instructions the model reads (initialize, server/discover); the extension hook instructions() wins when it gives one',
59
+ }),
60
+ allowedOrigins: o.array(o.string({ pattern: ORIGIN }), {
61
+ reloadable: true,
62
+ default: [],
63
+ maxItems: 64,
64
+ uniqueItems: true,
65
+ description: 'Origins a browser page may call from (the resource origin is always allowed); a request with another Origin is 403. Requests without Origin (server-side clients) pass',
66
+ }),
67
+ bodyLimitBytes: o.integer({
68
+ minimum: 1_024,
69
+ maximum: 67_108_864,
70
+ default: 1_048_576,
71
+ description: 'Largest request body of this route only (default 1 MiB; dokebi-web maxBodyBytes stays for other routes); beyond it 413 BODY_TOO_LARGE',
72
+ }),
73
+ resultMaxChars: o.integer({
74
+ reloadable: true,
75
+ minimum: 1_024,
76
+ maximum: 10_000_000,
77
+ default: 60_000,
78
+ description: 'Largest tool result (characters of its JSON); a larger one is the tool error RESULT_TOO_LARGE, never cut silently (page() helps)',
79
+ }),
80
+ uriMaxChars: o.integer({
81
+ reloadable: true,
82
+ minimum: 256,
83
+ maximum: 1_048_576,
84
+ default: URI_MAX_CHARS,
85
+ description: 'Longest resource URI of resources/read (characters); a longer one is -32602 INVALID_INPUT before any template is matched. Matching costs a pass over the URI per template, and the URI is client input of up to bodyLimitBytes',
86
+ }),
87
+ imageMaxBytes: o.integer({
88
+ reloadable: true,
89
+ minimum: 1_024,
90
+ maximum: 20_971_520,
91
+ default: IMAGE_LIMITS.imageMaxBytes,
92
+ description: 'Largest picture of a tool result in bytes (D-0143; default 1 MiB, sent as base64, about 4/3 of it); a larger one is the tool error IMAGE_TOO_LARGE. Send a reduced copy (a thumbnail), not the original',
93
+ }),
94
+ imageTotalMaxBytes: o.integer({
95
+ reloadable: true,
96
+ minimum: 1_024,
97
+ maximum: 67_108_864,
98
+ default: IMAGE_LIMITS.imageTotalMaxBytes,
99
+ description: 'Largest sum of the pictures of one tool result in bytes (D-0143; default 4 MiB); beyond it the tool error IMAGE_TOTAL_TOO_LARGE. Counted apart from resultMaxChars',
100
+ }),
101
+ maxImages: o.integer({
102
+ reloadable: true,
103
+ minimum: 0,
104
+ maximum: 100,
105
+ default: IMAGE_LIMITS.maxImages,
106
+ description: 'Pictures of one tool result at most (D-0143); more is the tool error TOO_MANY_IMAGES. 0 refuses every result with pictures',
107
+ }),
108
+ toolTimeoutMs: o.integer({
109
+ reloadable: true,
110
+ minimum: 1_000,
111
+ maximum: 600_000,
112
+ default: 50_000,
113
+ description: 'Time budget of one tool call, prompt or resource read (clients give up at 60 seconds); beyond it the signal aborts and the answer is TOOL_TIMEOUT',
114
+ }),
115
+ maxBatch: o.integer({
116
+ reloadable: true,
117
+ minimum: 1,
118
+ maximum: 1_024,
119
+ default: 32,
120
+ description: 'Messages of one JSON-RPC batch at most (versions that have batches); more is 400 INVALID_REQUEST (D-0114). Each message is a tool call with its own time budget and rate limit',
121
+ }),
122
+ listTtlMs: o.integer({
123
+ reloadable: true,
124
+ minimum: 0,
125
+ maximum: 86_400_000,
126
+ default: 300_000,
127
+ description: 'ttlMs of the modern list results (tools/list, prompts/list, resources/list): how long a client may reuse the answer for this grant (D-0114). Lists change only with a deployment',
128
+ }),
129
+ logRepeatWindowMs: o.integer({
130
+ minimum: 1_000,
131
+ maximum: 3_600_000,
132
+ default: 10_000,
133
+ description: 'Window of the repeated tool failure logs in ms: the first of a tool and code is logged in full, the rest of the window are counted (repeated, intervalMs) (D-0114)',
134
+ }),
135
+ onCallMaxPending: o.integer({
136
+ minimum: 1,
137
+ maximum: 1_000_000,
138
+ default: 1_000,
139
+ description: 'Promises returned by the extension hook onCall that may be pending at once (D-0134); an ended call that finds them all pending is not handed to the hook (warn MCP_ON_CALL_DROPPED, counted). The hook is never awaited',
140
+ }),
141
+ limits: o.record(o.string({ pattern: POLICY }), {
142
+ default: {},
143
+ keys: { pattern: BUCKET },
144
+ maxProperties: 32,
145
+ description: 'A @dokebi/rate-limit policy per tool bucket: { read: mcp-read, write: mcp-write }, counted per grant and bucket. When set, every tool bucket needs one; empty: no limit',
146
+ }),
147
+ }, {
148
+ checks: [
149
+ {
150
+ // The pattern admits shapes the URL parser refuses (`http://[1:2]`, `https://999.1.1.1`):
151
+ // such an origin is refused here, at start and at a reload, never at the first request.
152
+ path: 'allowedOrigins',
153
+ message: 'must be origins a browser can send (the URL parser reads them)',
154
+ test: (value) => value.allowedOrigins.every((origin) => URL.canParse(origin)),
155
+ },
156
+ ],
157
+ });
158
+ const same = true;
159
+ void same;
160
+ /** The `dokebi-mcp` options declaration (parser and options.schema.json). */
161
+ export const options = declaration;
@@ -0,0 +1,32 @@
1
+ import { type PluginFactory } from '@dokebi/core';
2
+ import { type McpCallStats } from './calls.js';
3
+ /** The plugin API: `app.get(plugin)` or `context.getDependency(dependency)`. */
4
+ export interface McpApi {
5
+ /** The route path. */
6
+ readonly path: string;
7
+ /** The @dokebi/oauth resource id. */
8
+ readonly resource: string;
9
+ /** The resource URI, as clients use it. */
10
+ readonly uri: string;
11
+ /** Names of the contributed tools, prompts and resources, in contribution order. */
12
+ readonly tools: readonly string[];
13
+ readonly prompts: readonly string[];
14
+ readonly resources: readonly string[];
15
+ /** Protocol versions this server answers, newest first. */
16
+ readonly versions: readonly string[];
17
+ /**
18
+ * What the `onCall` hook got so far (D-0134): delivered, pending, failed and dropped entries;
19
+ * all 0 without the hook.
20
+ */
21
+ callStats(): McpCallStats;
22
+ }
23
+ /** The user entry module (`src/infra/mcp/extension.ts`), or an extension class. */
24
+ export interface McpEntry {
25
+ /** `export default class AppMcp extends McpExtension { … }` */
26
+ readonly default?: unknown;
27
+ }
28
+ /**
29
+ * Factory input: `{ app, options?, entry? }`. Options default to `app.config['dokebi-mcp']`;
30
+ * `entry` is the module of src/infra/mcp/extension.ts (D-0064), optional.
31
+ */
32
+ export declare const createPlugin: PluginFactory<McpApi>;