@agentage/extension 0.1.2 → 0.2.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 +6 -5
- package/dist/agcid/current.d.ts +7 -0
- package/dist/agcid/current.js +4 -0
- package/dist/agcid/scope.d.ts +1 -1
- package/dist/agcid/scope.js +3 -4
- package/dist/command/args.d.ts +14 -0
- package/dist/command/args.js +60 -0
- package/dist/command/check-cli.d.ts +1 -0
- package/dist/command/check-cli.js +24 -0
- package/dist/command/command.types.d.ts +14 -0
- package/dist/command/command.types.js +2 -0
- package/dist/command/out.d.ts +19 -0
- package/dist/command/out.js +32 -0
- package/dist/command/run-command.d.ts +15 -0
- package/dist/command/run-command.js +53 -0
- package/dist/context/context.d.ts +8 -14
- package/dist/context/context.js +20 -26
- package/dist/context/settings.d.ts +5 -0
- package/dist/context/settings.js +32 -0
- package/dist/context/test-context.d.ts +44 -0
- package/dist/context/test-context.js +72 -0
- package/dist/discovery/discovery.d.ts +12 -1
- package/dist/discovery/discovery.js +15 -6
- package/dist/discovery/discovery.schema.d.ts +2 -0
- package/dist/discovery/discovery.schema.js +3 -1
- package/dist/discovery/openapi.d.ts +3 -0
- package/dist/discovery/openapi.js +19 -0
- package/dist/health/health.schema.js +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +16 -3
- package/dist/log/log.js +1 -1
- package/dist/manifest/manifest.schema.d.ts +18 -0
- package/dist/manifest/manifest.schema.js +1 -0
- package/dist/mcp/mcp-app.d.ts +8 -0
- package/dist/mcp/mcp-app.js +74 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +36 -0
- package/dist/mcp/snapshot.d.ts +11 -0
- package/dist/mcp/snapshot.js +69 -0
- package/dist/mcp/tools.d.ts +24 -0
- package/dist/mcp/tools.js +17 -0
- package/dist/pages/pages-app.d.ts +8 -0
- package/dist/pages/pages-app.js +29 -0
- package/dist/pages/pages.d.ts +4 -0
- package/dist/pages/pages.js +16 -0
- package/dist/runtime/part-runners.d.ts +20 -0
- package/dist/runtime/part-runners.js +68 -0
- package/dist/runtime/run-part.d.ts +4 -2
- package/dist/runtime/run-part.js +49 -27
- package/dist/server/app.d.ts +18 -2
- package/dist/server/app.js +28 -11
- package/dist/store/migrate.d.ts +2 -0
- package/dist/store/migrate.js +18 -0
- package/dist/store/migrations.d.ts +10 -0
- package/dist/store/migrations.js +55 -0
- package/dist/store/store.d.ts +10 -0
- package/dist/store/store.js +25 -0
- package/dist/store/store.types.d.ts +7 -0
- package/dist/store/store.types.js +1 -0
- package/dist/store/test-store.d.ts +5 -0
- package/dist/store/test-store.js +39 -0
- package/dist/testing/test-server.d.ts +6 -0
- package/dist/testing/test-server.js +31 -9
- package/dist/tool/build.d.ts +6 -0
- package/dist/tool/build.js +18 -25
- package/dist/tool/exec.d.ts +4 -0
- package/dist/tool/exec.js +15 -3
- package/dist/tool/index.d.ts +8 -0
- package/dist/tool/index.js +8 -0
- package/dist/tool/kit-version.d.ts +1 -0
- package/dist/tool/kit-version.js +3 -0
- package/dist/tool/main.d.ts +6 -0
- package/dist/tool/main.js +48 -22
- package/dist/tool/record-extras.d.ts +4 -0
- package/dist/tool/record-extras.js +60 -0
- package/dist/tool/stage.d.ts +3 -0
- package/dist/tool/stage.js +41 -21
- package/dist/tool/up/compose-run.d.ts +14 -0
- package/dist/tool/up/compose-run.js +24 -0
- package/dist/tool/up/compose.d.ts +18 -0
- package/dist/tool/up/compose.js +77 -0
- package/dist/tool/up/dev-issuer-app.d.ts +22 -0
- package/dist/tool/up/dev-issuer-app.js +80 -0
- package/dist/tool/up/dev-issuer.d.ts +2 -0
- package/dist/tool/up/dev-issuer.js +5 -0
- package/dist/tool/up/down.d.ts +12 -0
- package/dist/tool/up/down.js +38 -0
- package/dist/tool/up/gateway.d.ts +27 -0
- package/dist/tool/up/gateway.js +66 -0
- package/dist/tool/up/lines.d.ts +2 -0
- package/dist/tool/up/lines.js +3 -0
- package/dist/tool/up/local-record.d.ts +44 -0
- package/dist/tool/up/local-record.js +28 -0
- package/dist/tool/up/token.d.ts +6 -0
- package/dist/tool/up/token.js +18 -0
- package/dist/tool/up/up.d.ts +19 -0
- package/dist/tool/up/up.js +81 -0
- package/dist/tool/up/wait.d.ts +8 -0
- package/dist/tool/up/wait.js +24 -0
- package/dist/ui/ui-app.d.ts +6 -0
- package/dist/ui/ui-app.js +26 -0
- package/dist/worker/cron.d.ts +11 -0
- package/dist/worker/cron.js +73 -0
- package/dist/worker/job-options.d.ts +9 -0
- package/dist/worker/job-options.js +17 -0
- package/dist/worker/run-log.d.ts +9 -0
- package/dist/worker/run-log.js +38 -0
- package/dist/worker/runs.d.ts +20 -0
- package/dist/worker/runs.js +59 -0
- package/dist/worker/scheduler.d.ts +20 -0
- package/dist/worker/scheduler.js +97 -0
- package/dist/worker/worker.types.d.ts +5 -0
- package/dist/worker/worker.types.js +2 -0
- package/package.json +11 -2
- package/schemas/extension-discovery.schema.json +5 -0
- package/schemas/extension-package.schema.json +3 -0
- package/schemas/extension-record.schema.json +5 -0
- package/schemas/health.schema.json +1 -1
|
@@ -13,7 +13,9 @@ const PartsRecordSchema = z
|
|
|
13
13
|
api: PartImageSchema.optional(),
|
|
14
14
|
mcp: PartImageSchema.optional(),
|
|
15
15
|
worker: PartImageSchema.optional(),
|
|
16
|
-
store: z
|
|
16
|
+
store: z
|
|
17
|
+
.strictObject({ kind: StoreKindSchema, migrations: z.int().min(0).optional() })
|
|
18
|
+
.optional(),
|
|
17
19
|
})
|
|
18
20
|
.refine((parts) => IMAGE_PART_KINDS.some((kind) => parts[kind] !== undefined), {
|
|
19
21
|
message: 'at least one part with an image',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { readJsonIfExists } from '../manifest/read-manifest.js';
|
|
4
|
+
export const OPENAPI_FILE = 'openapi.json';
|
|
5
|
+
export const OPENAPI_PATH = '/api/v1/openapi.json';
|
|
6
|
+
const OpenapiSchema = z.looseObject({
|
|
7
|
+
openapi: z.string().regex(/^3\.1\.[0-9]+$/, { message: 'OpenAPI 3.1' }),
|
|
8
|
+
paths: z.record(z.string(), z.unknown()),
|
|
9
|
+
});
|
|
10
|
+
// The api's contract, committed beside package.json (the repo diffs it against its Zod schemas).
|
|
11
|
+
export const readOpenapi = async (dir) => {
|
|
12
|
+
const value = await readJsonIfExists(join(dir, OPENAPI_FILE));
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return null;
|
|
15
|
+
const result = OpenapiSchema.safeParse(value);
|
|
16
|
+
if (!result.success)
|
|
17
|
+
throw new Error(`${OPENAPI_FILE} is not an OpenAPI 3.1 document`);
|
|
18
|
+
return result.data;
|
|
19
|
+
};
|
|
@@ -15,6 +15,6 @@ export const HealthSchema = z.strictObject({
|
|
|
15
15
|
instance: z.string().regex(/^[0-9a-f]{8,32}$/),
|
|
16
16
|
uptimeSeconds: z.int().min(0),
|
|
17
17
|
checkedAt: z.iso.datetime(),
|
|
18
|
-
checks: z.record(z.string().regex(/^[a-z][a-z0-9-]
|
|
18
|
+
checks: z.record(z.string().regex(/^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$/), HealthStatusSchema),
|
|
19
19
|
}),
|
|
20
20
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { type ArgValue } from './command/args.js';
|
|
2
|
+
export { type CliContext, type CliCreate, type CommandExtension, type CommandHandler, } from './command/command.types.js';
|
|
3
|
+
export { type Out } from './command/out.js';
|
|
4
|
+
export { type ExtensionInfo, type PartContext, type ServerPartKind, type SettingValue, } from './context/context.js';
|
|
5
|
+
export { createTestContext, type LogEntry, type TestCommandContext, type TestCommandOptions, type TestContext, type TestContextOptions, } from './context/test-context.js';
|
|
2
6
|
export { DiscoveryDocumentSchema, ExtensionRecordSchema, type DiscoveryDocument, type ExtensionRecord, } from './discovery/discovery.schema.js';
|
|
7
|
+
export { AGCID_HEADER, isAgcid } from './agcid/agcid.js';
|
|
3
8
|
export { ERROR_CODES, ERROR_STATUS, type ErrorCode } from './envelope/codes.js';
|
|
4
9
|
export { fail, isFailure, ok, type ErrorDetail, type ErrorEnvelope, type Failure, type FailOptions, type OkEnvelope, } from './envelope/envelope.js';
|
|
5
10
|
export { ErrorEnvelopeSchema, OkEnvelopeSchema } from './envelope/envelope.schema.js';
|
|
6
11
|
export { parse, type ParseSchema } from './envelope/parse.js';
|
|
7
12
|
export { HealthSchema, type HealthBody, type HealthStatus } from './health/health.schema.js';
|
|
8
13
|
export { type Log, type LogFields } from './log/log.js';
|
|
9
|
-
export { PackageSchema, type ExtensionPackage } from './manifest/manifest.schema.js';
|
|
14
|
+
export { PackageSchema, type CommandSpec, type ExtensionPackage, } from './manifest/manifest.schema.js';
|
|
10
15
|
export { imageParts } from './manifest/parts.js';
|
|
11
16
|
export { readManifest, validateManifest } from './manifest/read-manifest.js';
|
|
17
|
+
export { canonical, mcpSnapshot, PROTOCOL_VERSIONS, type McpSnapshot, type ProtocolVersion, } from './mcp/snapshot.js';
|
|
18
|
+
export { type McpContext, type ToolHandler, type ToolRegistrar, type ToolSpec, } from './mcp/tools.js';
|
|
19
|
+
export { versionPage, type PagesContext, type PagesSite } from './pages/pages.js';
|
|
12
20
|
export { type ApiCreate, type ApiHandler, type ApiPart } from './server/part.types.js';
|
|
13
|
-
export {
|
|
21
|
+
export { type QueryResult, type Row, type Store } from './store/store.types.js';
|
|
22
|
+
export { startTestMcp, startTestServer, type TestMcpOptions, type TestServer, type TestServerOptions, } from './testing/test-server.js';
|
|
23
|
+
export { type JobOptions } from './worker/job-options.js';
|
|
24
|
+
export { type JobRun, type RunStatus } from './worker/runs.js';
|
|
25
|
+
export { type Job, type Schedule } from './worker/scheduler.js';
|
|
26
|
+
export { type WorkerContext } from './worker/worker.types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {} from './command/args.js';
|
|
2
|
+
export {} from './command/command.types.js';
|
|
3
|
+
export {} from './command/out.js';
|
|
4
|
+
export {} from './context/context.js';
|
|
5
|
+
export { createTestContext, } from './context/test-context.js';
|
|
2
6
|
export { DiscoveryDocumentSchema, ExtensionRecordSchema, } from './discovery/discovery.schema.js';
|
|
7
|
+
export { AGCID_HEADER, isAgcid } from './agcid/agcid.js';
|
|
3
8
|
export { ERROR_CODES, ERROR_STATUS } from './envelope/codes.js';
|
|
4
9
|
export { fail, isFailure, ok, } from './envelope/envelope.js';
|
|
5
10
|
export { ErrorEnvelopeSchema, OkEnvelopeSchema } from './envelope/envelope.schema.js';
|
|
6
11
|
export { parse } from './envelope/parse.js';
|
|
7
12
|
export { HealthSchema } from './health/health.schema.js';
|
|
8
13
|
export {} from './log/log.js';
|
|
9
|
-
export { PackageSchema } from './manifest/manifest.schema.js';
|
|
14
|
+
export { PackageSchema, } from './manifest/manifest.schema.js';
|
|
10
15
|
export { imageParts } from './manifest/parts.js';
|
|
11
16
|
export { readManifest, validateManifest } from './manifest/read-manifest.js';
|
|
17
|
+
export { canonical, mcpSnapshot, PROTOCOL_VERSIONS, } from './mcp/snapshot.js';
|
|
18
|
+
export {} from './mcp/tools.js';
|
|
19
|
+
export { versionPage } from './pages/pages.js';
|
|
12
20
|
export {} from './server/part.types.js';
|
|
13
|
-
export {
|
|
21
|
+
export {} from './store/store.types.js';
|
|
22
|
+
export { startTestMcp, startTestServer, } from './testing/test-server.js';
|
|
23
|
+
export {} from './worker/job-options.js';
|
|
24
|
+
export {} from './worker/runs.js';
|
|
25
|
+
export {} from './worker/scheduler.js';
|
|
26
|
+
export {} from './worker/worker.types.js';
|
package/dist/log/log.js
CHANGED
|
@@ -33,6 +33,22 @@ declare const PartsSchema: z.ZodObject<{
|
|
|
33
33
|
}>;
|
|
34
34
|
}, z.core.$strict>]>>;
|
|
35
35
|
}, z.core.$strict>;
|
|
36
|
+
declare const CommandSchema: z.ZodObject<{
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
summary: z.ZodString;
|
|
39
|
+
args: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
40
|
+
name: z.ZodString;
|
|
41
|
+
type: z.ZodEnum<{
|
|
42
|
+
string: "string";
|
|
43
|
+
number: "number";
|
|
44
|
+
boolean: "boolean";
|
|
45
|
+
integer: "integer";
|
|
46
|
+
}>;
|
|
47
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
required: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
description: z.ZodOptional<z.ZodString>;
|
|
50
|
+
}, z.core.$strict>>>;
|
|
51
|
+
}, z.core.$strict>;
|
|
36
52
|
export declare const PackageSchema: z.ZodObject<{
|
|
37
53
|
name: z.ZodString;
|
|
38
54
|
version: z.ZodString;
|
|
@@ -83,6 +99,7 @@ export declare const PackageSchema: z.ZodObject<{
|
|
|
83
99
|
boolean: "boolean";
|
|
84
100
|
integer: "integer";
|
|
85
101
|
}>;
|
|
102
|
+
optional: z.ZodOptional<z.ZodBoolean>;
|
|
86
103
|
required: z.ZodOptional<z.ZodBoolean>;
|
|
87
104
|
description: z.ZodOptional<z.ZodString>;
|
|
88
105
|
}, z.core.$strict>>>;
|
|
@@ -102,6 +119,7 @@ export declare const PackageSchema: z.ZodObject<{
|
|
|
102
119
|
}, z.core.$strict>;
|
|
103
120
|
}, z.core.$loose>;
|
|
104
121
|
export type CodePart = z.infer<typeof CodePartSchema>;
|
|
122
|
+
export type CommandSpec = z.infer<typeof CommandSchema>;
|
|
105
123
|
export type Parts = z.infer<typeof PartsSchema>;
|
|
106
124
|
export type ExtensionPackage = z.infer<typeof PackageSchema>;
|
|
107
125
|
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartAppBase } from '../server/app.js';
|
|
3
|
+
import { type ToolEntry } from './tools.js';
|
|
4
|
+
export interface McpAppOptions extends PartAppBase {
|
|
5
|
+
tools: ToolEntry[];
|
|
6
|
+
listing: 'open' | 'token';
|
|
7
|
+
}
|
|
8
|
+
export declare const createMcpApp: (options: McpAppOptions) => Express;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
|
+
import cors from 'cors';
|
|
4
|
+
import express, {} from 'express';
|
|
5
|
+
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
6
|
+
import { fail } from '../envelope/envelope.js';
|
|
7
|
+
import { BODY_LIMIT, createPartApp, PUBLIC_CORS } from '../server/app.js';
|
|
8
|
+
import { serverFactory } from './server.js';
|
|
9
|
+
import {} from './tools.js';
|
|
10
|
+
const MCP_HEADERS = ['Mcp-Protocol-Version', 'Mcp-Method', 'Mcp-Name', 'Accept'];
|
|
11
|
+
const MCP_CORS = {
|
|
12
|
+
...PUBLIC_CORS,
|
|
13
|
+
allowedHeaders: [...PUBLIC_CORS.allowedHeaders, ...MCP_HEADERS],
|
|
14
|
+
};
|
|
15
|
+
const parsed = (body) => {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(body.toString('utf8'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const isListing = (body) => (Array.isArray(body) ? body : [body]).some((message) => message?.method === 'tools/list');
|
|
24
|
+
const webRequest = (req, body) => {
|
|
25
|
+
const headers = new Headers();
|
|
26
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
headers.set(key, value);
|
|
29
|
+
}
|
|
30
|
+
return new globalThis.Request(`http://${req.get('host') ?? 'mcp'}/mcp`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers,
|
|
33
|
+
body,
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
const send = async (response, res) => {
|
|
37
|
+
res.status(response.status);
|
|
38
|
+
response.headers.forEach((value, key) => void res.setHeader(key, value));
|
|
39
|
+
if (!response.body)
|
|
40
|
+
return void res.end();
|
|
41
|
+
await pipeline(Readable.fromWeb(response.body), res);
|
|
42
|
+
};
|
|
43
|
+
// The kit verifies no token yet, so a closed listing refuses every tools/list.
|
|
44
|
+
const handle = (handler, listing) => async (req, res) => {
|
|
45
|
+
const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
|
|
46
|
+
const message = parsed(body);
|
|
47
|
+
if (listing === 'token' && isListing(message))
|
|
48
|
+
throw fail('UNAUTHORIZED', 'tools/list needs a token');
|
|
49
|
+
await send(await handler.fetch(webRequest(req, body), { parsedBody: message }), res);
|
|
50
|
+
};
|
|
51
|
+
const methodNotAllowed = (_req, res) => {
|
|
52
|
+
res.setHeader('Allow', 'POST');
|
|
53
|
+
res
|
|
54
|
+
.status(405)
|
|
55
|
+
.json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
|
|
56
|
+
};
|
|
57
|
+
// Stateless Streamable HTTP on SDK v2: POST /mcp only, GET and DELETE (sessions) answer 405.
|
|
58
|
+
export const createMcpApp = (options) => {
|
|
59
|
+
const identity = { id: options.pkg.agentage.id, version: options.pkg.version };
|
|
60
|
+
const handler = createMcpHandler(serverFactory(identity, options.tools, options.log), {
|
|
61
|
+
legacy: 'stateless',
|
|
62
|
+
onerror: (err) => options.log.warn('mcp request refused', { reason: err.message }),
|
|
63
|
+
});
|
|
64
|
+
const raw = express.raw({ type: () => true, limit: BODY_LIMIT });
|
|
65
|
+
return createPartApp({
|
|
66
|
+
...options,
|
|
67
|
+
part: 'mcp',
|
|
68
|
+
mount: (app) => {
|
|
69
|
+
app.use('/mcp', cors(MCP_CORS));
|
|
70
|
+
app.post('/mcp', raw, handle(handler, options.listing));
|
|
71
|
+
app.all('/mcp', methodNotAllowed);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { type Log } from '../log/log.js';
|
|
3
|
+
import { type ToolEntry } from './tools.js';
|
|
4
|
+
export interface ServerIdentity {
|
|
5
|
+
id: string;
|
|
6
|
+
version: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const serverFactory: (identity: ServerIdentity, tools: ToolEntry[], log: Log) => () => McpServer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
2
|
+
import { currentAgcid } from '../agcid/current.js';
|
|
3
|
+
import { fail, isFailure } from '../envelope/envelope.js';
|
|
4
|
+
import {} from '../log/log.js';
|
|
5
|
+
import { prefixed } from './tools.js';
|
|
6
|
+
const text = (value) => [{ type: 'text', text: JSON.stringify(value) }];
|
|
7
|
+
// A failed tool is isError with the REST error shape; anything unexpected is a generic INTERNAL.
|
|
8
|
+
const errorResult = (err, log) => {
|
|
9
|
+
const failure = isFailure(err) ? err : fail('INTERNAL', 'Internal error');
|
|
10
|
+
if (failure.status >= 500)
|
|
11
|
+
log.error(err);
|
|
12
|
+
const error = { code: failure.code, message: failure.message, agcid: currentAgcid() ?? '' };
|
|
13
|
+
return { isError: true, content: text({ error }), structuredContent: { error } };
|
|
14
|
+
};
|
|
15
|
+
const callback = (entry, log) => async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
const output = entry.spec.output.parse(await entry.handler(args));
|
|
18
|
+
return { content: text(output), structuredContent: output };
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
return errorResult(err, log);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
// One fresh server per request (stateless); serverInfo agentage-{id} at the package version.
|
|
25
|
+
export const serverFactory = (identity, tools, log) => () => {
|
|
26
|
+
const server = new McpServer({ name: `agentage-${identity.id}`, version: identity.version });
|
|
27
|
+
for (const entry of tools) {
|
|
28
|
+
const config = {
|
|
29
|
+
description: entry.spec.description,
|
|
30
|
+
inputSchema: entry.spec.input,
|
|
31
|
+
outputSchema: entry.spec.output,
|
|
32
|
+
};
|
|
33
|
+
server.registerTool(prefixed(identity.id, entry.name), config, callback(entry, log));
|
|
34
|
+
}
|
|
35
|
+
return server;
|
|
36
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSIONS: readonly ["2025-11-25", "2026-07-28"];
|
|
2
|
+
export type ProtocolVersion = (typeof PROTOCOL_VERSIONS)[number];
|
|
3
|
+
export interface McpSnapshot {
|
|
4
|
+
serverInfo: {
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
};
|
|
8
|
+
tools: Record<string, unknown>[];
|
|
9
|
+
}
|
|
10
|
+
export declare const canonical: (value: unknown) => unknown;
|
|
11
|
+
export declare const mcpSnapshot: (url: string, version: ProtocolVersion) => Promise<McpSnapshot>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const PROTOCOL_VERSIONS = ['2025-11-25', '2026-07-28'];
|
|
2
|
+
const CLIENT = { name: 'agentage-snapshot', version: '1.0.0' };
|
|
3
|
+
// Key order never counts: the two eras serialize the same schema in different orders.
|
|
4
|
+
export const canonical = (value) => {
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return value.map(canonical);
|
|
7
|
+
if (typeof value !== 'object' || value === null)
|
|
8
|
+
return value;
|
|
9
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
|
|
10
|
+
return Object.fromEntries(entries.map(([key, v]) => [key, canonical(v)]));
|
|
11
|
+
};
|
|
12
|
+
// A JSON answer or the stateless legacy leg's one-message SSE stream.
|
|
13
|
+
const payload = async (res) => {
|
|
14
|
+
const text = await res.text();
|
|
15
|
+
if (!res.ok)
|
|
16
|
+
throw new Error(`mcp answered ${res.status}: ${text.slice(0, 200)}`);
|
|
17
|
+
const data = (res.headers.get('content-type') ?? '').includes('text/event-stream')
|
|
18
|
+
? (text
|
|
19
|
+
.split('\n')
|
|
20
|
+
.find((line) => line.startsWith('data:'))
|
|
21
|
+
?.slice(5) ?? '{}')
|
|
22
|
+
: text;
|
|
23
|
+
const message = JSON.parse(data);
|
|
24
|
+
if (message['error'])
|
|
25
|
+
throw new Error(`mcp error: ${JSON.stringify(message['error'])}`);
|
|
26
|
+
return message['result'];
|
|
27
|
+
};
|
|
28
|
+
const META = {
|
|
29
|
+
'io.modelcontextprotocol/protocolVersion': '2026-07-28',
|
|
30
|
+
'io.modelcontextprotocol/clientInfo': CLIENT,
|
|
31
|
+
'io.modelcontextprotocol/clientCapabilities': {},
|
|
32
|
+
};
|
|
33
|
+
const post = async (url, version, method, params) => {
|
|
34
|
+
const modern = version === '2026-07-28';
|
|
35
|
+
const headers = {
|
|
36
|
+
'content-type': 'application/json',
|
|
37
|
+
accept: 'application/json, text/event-stream',
|
|
38
|
+
'mcp-protocol-version': version,
|
|
39
|
+
...(modern ? { 'mcp-method': method } : {}),
|
|
40
|
+
};
|
|
41
|
+
const body = {
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
id: 1,
|
|
44
|
+
method,
|
|
45
|
+
params: modern ? { ...params, _meta: META } : params,
|
|
46
|
+
};
|
|
47
|
+
return payload(await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }));
|
|
48
|
+
};
|
|
49
|
+
const serverInfoOf = async (url, version) => {
|
|
50
|
+
if (version === '2025-11-25') {
|
|
51
|
+
const init = await post(url, version, 'initialize', {
|
|
52
|
+
protocolVersion: version,
|
|
53
|
+
capabilities: {},
|
|
54
|
+
clientInfo: CLIENT,
|
|
55
|
+
});
|
|
56
|
+
return init['serverInfo'];
|
|
57
|
+
}
|
|
58
|
+
const found = await post(url, version, 'server/discover', {});
|
|
59
|
+
return found['_meta']['io.modelcontextprotocol/serverInfo'];
|
|
60
|
+
};
|
|
61
|
+
// What a client of that protocol version sees: serverInfo and tools/list, canonical; the contract test diffs it.
|
|
62
|
+
export const mcpSnapshot = async (url, version) => {
|
|
63
|
+
const { name, version: serverVersion } = await serverInfoOf(url, version);
|
|
64
|
+
const listed = await post(url, version, 'tools/list', {});
|
|
65
|
+
return canonical({
|
|
66
|
+
serverInfo: { name, version: serverVersion },
|
|
67
|
+
tools: listed['tools'],
|
|
68
|
+
});
|
|
69
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type z } from 'zod';
|
|
2
|
+
import { type PartContext } from '../context/context.js';
|
|
3
|
+
type ObjectSchema = z.ZodObject;
|
|
4
|
+
export interface ToolSpec<I extends ObjectSchema, O extends ObjectSchema> {
|
|
5
|
+
description: string;
|
|
6
|
+
input: I;
|
|
7
|
+
output: O;
|
|
8
|
+
}
|
|
9
|
+
export type ToolHandler<I extends ObjectSchema, O extends ObjectSchema> = (args: z.output<I>) => z.input<O> | Promise<z.input<O>>;
|
|
10
|
+
export type ToolRegistrar = <I extends ObjectSchema, O extends ObjectSchema>(name: string, spec: ToolSpec<I, O>, handler: ToolHandler<I, O>) => void;
|
|
11
|
+
export interface McpContext extends PartContext {
|
|
12
|
+
readonly tool: ToolRegistrar;
|
|
13
|
+
}
|
|
14
|
+
export interface ToolEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
spec: ToolSpec<ObjectSchema, ObjectSchema>;
|
|
17
|
+
handler: (args: Record<string, unknown>) => unknown;
|
|
18
|
+
}
|
|
19
|
+
export declare const createToolRegistry: () => {
|
|
20
|
+
tool: ToolRegistrar;
|
|
21
|
+
tools: ToolEntry[];
|
|
22
|
+
};
|
|
23
|
+
export declare const prefixed: (id: string, name: string) => string;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {} from 'zod';
|
|
2
|
+
import {} from '../context/context.js';
|
|
3
|
+
const TOOL_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
4
|
+
export const createToolRegistry = () => {
|
|
5
|
+
const tools = [];
|
|
6
|
+
const tool = (name, spec, handler) => {
|
|
7
|
+
if (!TOOL_NAME.test(name))
|
|
8
|
+
throw new Error(`tool name ${name}: lowercase, digits, _ and -`);
|
|
9
|
+
if (tools.some((entry) => entry.name === name))
|
|
10
|
+
throw new Error(`tool ${name} is registered twice`);
|
|
11
|
+
if (!spec.description)
|
|
12
|
+
throw new Error(`tool ${name} needs a description: it routes the model`);
|
|
13
|
+
tools.push({ name, spec, handler: handler });
|
|
14
|
+
};
|
|
15
|
+
return { tool, tools };
|
|
16
|
+
};
|
|
17
|
+
export const prefixed = (id, name) => `${id}__${name}`;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartAppBase } from '../server/app.js';
|
|
3
|
+
import { type PagesSite } from './pages.js';
|
|
4
|
+
export interface PagesAppOptions extends PartAppBase {
|
|
5
|
+
site: PagesSite;
|
|
6
|
+
}
|
|
7
|
+
export declare const NOT_FOUND_PAGE = "<!doctype html>\n<html lang=\"en\"><head><meta charset=\"utf-8\"><title>Not found</title></head><body><p>Not found</p></body></html>\n";
|
|
8
|
+
export declare const createPagesApp: (options: PagesAppOptions) => Express;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {} from 'express';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { createPartApp } from '../server/app.js';
|
|
4
|
+
import {} from './pages.js';
|
|
5
|
+
const SiteSchema = z.record(z.string().regex(/^\/[A-Za-z0-9._~/-]*$/), z.string());
|
|
6
|
+
export const NOT_FOUND_PAGE = '<!doctype html>\n<html lang="en"><head><meta charset="utf-8"><title>Not found</title></head><body><p>Not found</p></body></html>\n';
|
|
7
|
+
const serve = (site) => (req, res, next) => {
|
|
8
|
+
const html = site[req.path];
|
|
9
|
+
if (html === undefined)
|
|
10
|
+
return next();
|
|
11
|
+
res.type('html').setHeader('Cache-Control', 'no-cache');
|
|
12
|
+
res.send(html);
|
|
13
|
+
};
|
|
14
|
+
const missing = (_req, res) => {
|
|
15
|
+
res.status(404).type('html').send(NOT_FOUND_PAGE);
|
|
16
|
+
};
|
|
17
|
+
// Everything the gateway does not reserve comes here: a page from the site map, or an HTML 404.
|
|
18
|
+
export const createPagesApp = (options) => {
|
|
19
|
+
const site = SiteSchema.parse(options.site);
|
|
20
|
+
return createPartApp({
|
|
21
|
+
...options,
|
|
22
|
+
part: 'pages',
|
|
23
|
+
helmet: { contentSecurityPolicy: { directives: { upgradeInsecureRequests: null } } },
|
|
24
|
+
mount: (app) => {
|
|
25
|
+
app.get(/.*/, serve(site));
|
|
26
|
+
app.get(/.*/, missing);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {} from '../context/context.js';
|
|
2
|
+
const escape = (value) => value.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
3
|
+
const STYLE = 'body{font:16px/1.5 system-ui,sans-serif;margin:3rem;color:#1d1d1f;background:#fff}' +
|
|
4
|
+
'@media (prefers-color-scheme:dark){body{color:#f5f5f7;background:#111}}code{font-size:.9em}';
|
|
5
|
+
// One static page that names what runs: "{id} {version} · {commit}".
|
|
6
|
+
export const versionPage = (ctx) => {
|
|
7
|
+
const { id, version, commit } = ctx.extension;
|
|
8
|
+
const title = escape(id);
|
|
9
|
+
return [
|
|
10
|
+
'<!doctype html>',
|
|
11
|
+
`<html lang="en"><head><meta charset="utf-8"><title>${title}</title>`,
|
|
12
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1"><style>${STYLE}</style></head>`,
|
|
13
|
+
`<body><main><h1>${title}</h1><p>${title} ${escape(version)} · <code>${escape(commit)}</code></p></main></body></html>`,
|
|
14
|
+
'',
|
|
15
|
+
].join('\n');
|
|
16
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import { type PartContext } from '../context/context.js';
|
|
3
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
4
|
+
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
5
|
+
import { type PartAppBase } from '../server/app.js';
|
|
6
|
+
export interface RunnerInput {
|
|
7
|
+
dir: string;
|
|
8
|
+
pkg: ExtensionPackage;
|
|
9
|
+
ctx: PartContext;
|
|
10
|
+
base: PartAppBase;
|
|
11
|
+
importModule?: (file: string) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface Running {
|
|
14
|
+
app: Express;
|
|
15
|
+
stop?: () => void;
|
|
16
|
+
}
|
|
17
|
+
type PartCreate = (ctx: never) => unknown;
|
|
18
|
+
export declare const createOf: (mod: unknown, file: string) => PartCreate;
|
|
19
|
+
export declare const RUNNERS: Record<ImagePartKind, (input: RunnerInput) => Promise<Running>>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import {} from 'express';
|
|
4
|
+
import {} from '../context/context.js';
|
|
5
|
+
import { readOpenapi } from '../discovery/openapi.js';
|
|
6
|
+
import {} from '../manifest/manifest.schema.js';
|
|
7
|
+
import { entryOf, normalizeParts } from '../manifest/parts.js';
|
|
8
|
+
import {} from '../manifest/primitives.schema.js';
|
|
9
|
+
import { createMcpApp } from '../mcp/mcp-app.js';
|
|
10
|
+
import { createToolRegistry } from '../mcp/tools.js';
|
|
11
|
+
import { createPagesApp } from '../pages/pages-app.js';
|
|
12
|
+
import { createApiApp, createPartApp } from '../server/app.js';
|
|
13
|
+
import {} from '../server/part.types.js';
|
|
14
|
+
import { createUiApp } from '../ui/ui-app.js';
|
|
15
|
+
import { runLog } from '../worker/run-log.js';
|
|
16
|
+
import { memoryRuns, storeRuns } from '../worker/runs.js';
|
|
17
|
+
import { createScheduler } from '../worker/scheduler.js';
|
|
18
|
+
const importFile = (file) => import(pathToFileURL(file).href);
|
|
19
|
+
export const createOf = (mod, file) => {
|
|
20
|
+
const create = mod?.create;
|
|
21
|
+
if (typeof create !== 'function')
|
|
22
|
+
throw new Error(`${file} does not export create(ctx)`);
|
|
23
|
+
return create;
|
|
24
|
+
};
|
|
25
|
+
// Imports the part's entry and calls its create(ctx); ui has no code to run.
|
|
26
|
+
const callCreate = async (input, part, ctx) => {
|
|
27
|
+
const file = join(input.dir, entryOf(input.pkg, part));
|
|
28
|
+
const mod = await (input.importModule ?? importFile)(file);
|
|
29
|
+
return createOf(mod, file)(ctx);
|
|
30
|
+
};
|
|
31
|
+
const api = async (input) => {
|
|
32
|
+
const router = (await callCreate(input, 'api', input.ctx));
|
|
33
|
+
const openapi = await readOpenapi(input.dir);
|
|
34
|
+
return { app: createApiApp({ ...input.base, router, openapi }) };
|
|
35
|
+
};
|
|
36
|
+
const mcp = async (input) => {
|
|
37
|
+
const registry = createToolRegistry();
|
|
38
|
+
await callCreate(input, 'mcp', { ...input.ctx, tool: registry.tool });
|
|
39
|
+
const listing = input.ctx.settings['mcp.listing'] === 'token' ? 'token' : 'open';
|
|
40
|
+
return { app: createMcpApp({ ...input.base, tools: registry.tools, listing }) };
|
|
41
|
+
};
|
|
42
|
+
const pages = async (input) => {
|
|
43
|
+
const site = (await callCreate(input, 'pages', input.ctx));
|
|
44
|
+
return { app: createPagesApp({ ...input.base, site }) };
|
|
45
|
+
};
|
|
46
|
+
const ui = async (input) => ({
|
|
47
|
+
app: createUiApp({ ...input.base, dir: join(input.dir, 'dist', 'ui') }),
|
|
48
|
+
});
|
|
49
|
+
const worker = async (input) => {
|
|
50
|
+
const log = runLog(input.ctx.log);
|
|
51
|
+
const hasStore = normalizeParts(input.pkg.agentage.parts).store === 'postgres';
|
|
52
|
+
const runs = hasStore ? storeRuns(input.ctx.store) : memoryRuns();
|
|
53
|
+
const scheduler = createScheduler({ runs, log });
|
|
54
|
+
await callCreate(input, 'worker', { ...input.ctx, log, schedule: scheduler.schedule });
|
|
55
|
+
const checks = { ...input.base.checks, ...scheduler.checks() };
|
|
56
|
+
scheduler.start();
|
|
57
|
+
return {
|
|
58
|
+
app: createPartApp({ ...input.base, checks, part: 'worker', mount: () => undefined }),
|
|
59
|
+
stop: scheduler.stop,
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export const RUNNERS = {
|
|
63
|
+
api,
|
|
64
|
+
mcp,
|
|
65
|
+
pages,
|
|
66
|
+
ui,
|
|
67
|
+
worker,
|
|
68
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
3
|
+
import { type ImagePartKind } from '../manifest/primitives.schema.js';
|
|
3
4
|
import { type ServeOptions } from '../server/serve.js';
|
|
4
5
|
import { type Env } from './build-info.js';
|
|
6
|
+
export { createOf } from './part-runners.js';
|
|
5
7
|
export interface RunPartOptions {
|
|
6
8
|
dir: string;
|
|
7
9
|
part: string;
|
|
@@ -9,5 +11,5 @@ export interface RunPartOptions {
|
|
|
9
11
|
importModule?: (file: string) => Promise<unknown>;
|
|
10
12
|
serve?: Pick<ServeOptions, 'signals' | 'exit'>;
|
|
11
13
|
}
|
|
12
|
-
export declare const
|
|
14
|
+
export declare const partOf: (pkg: ExtensionPackage, part: string) => ImagePartKind;
|
|
13
15
|
export declare const runPart: (options: RunPartOptions) => Promise<Server>;
|