@agentage/extension 0.1.2 → 0.2.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.
Files changed (137) hide show
  1. package/README.md +6 -5
  2. package/dist/agcid/current.d.ts +7 -0
  3. package/dist/agcid/current.js +4 -0
  4. package/dist/agcid/scope.d.ts +0 -1
  5. package/dist/agcid/scope.js +2 -4
  6. package/dist/auth/route-access.d.ts +2 -0
  7. package/dist/auth/route-access.js +21 -0
  8. package/dist/auth/token-check.d.ts +10 -0
  9. package/dist/auth/token-check.js +18 -0
  10. package/dist/command/args.d.ts +14 -0
  11. package/dist/command/args.js +60 -0
  12. package/dist/command/check-cli.d.ts +1 -0
  13. package/dist/command/check-cli.js +19 -0
  14. package/dist/command/command.types.d.ts +14 -0
  15. package/dist/command/command.types.js +2 -0
  16. package/dist/command/out.d.ts +19 -0
  17. package/dist/command/out.js +32 -0
  18. package/dist/command/run-command.d.ts +16 -0
  19. package/dist/command/run-command.js +43 -0
  20. package/dist/context/context.d.ts +8 -14
  21. package/dist/context/context.js +20 -26
  22. package/dist/context/settings.d.ts +5 -0
  23. package/dist/context/settings.js +32 -0
  24. package/dist/context/test-context.d.ts +44 -0
  25. package/dist/context/test-context.js +75 -0
  26. package/dist/discovery/discovery.d.ts +12 -1
  27. package/dist/discovery/discovery.js +15 -6
  28. package/dist/discovery/discovery.schema.d.ts +7 -0
  29. package/dist/discovery/discovery.schema.js +4 -2
  30. package/dist/discovery/openapi.d.ts +3 -0
  31. package/dist/discovery/openapi.js +19 -0
  32. package/dist/health/health.schema.js +1 -1
  33. package/dist/index.d.ts +16 -3
  34. package/dist/index.js +16 -3
  35. package/dist/log/log.js +1 -1
  36. package/dist/manifest/entry.d.ts +5 -0
  37. package/dist/manifest/entry.js +29 -0
  38. package/dist/manifest/manifest.schema.d.ts +18 -0
  39. package/dist/manifest/manifest.schema.js +1 -0
  40. package/dist/manifest/parts.d.ts +1 -0
  41. package/dist/manifest/parts.js +1 -0
  42. package/dist/mcp/mcp-app.d.ts +8 -0
  43. package/dist/mcp/mcp-app.js +94 -0
  44. package/dist/mcp/server.d.ts +8 -0
  45. package/dist/mcp/server.js +38 -0
  46. package/dist/mcp/snapshot.d.ts +11 -0
  47. package/dist/mcp/snapshot.js +69 -0
  48. package/dist/mcp/tools.d.ts +26 -0
  49. package/dist/mcp/tools.js +21 -0
  50. package/dist/pages/pages-app.d.ts +8 -0
  51. package/dist/pages/pages-app.js +29 -0
  52. package/dist/pages/pages.d.ts +4 -0
  53. package/dist/pages/pages.js +16 -0
  54. package/dist/runtime/part-runners.d.ts +20 -0
  55. package/dist/runtime/part-runners.js +63 -0
  56. package/dist/runtime/run-part.d.ts +7 -3
  57. package/dist/runtime/run-part.js +67 -29
  58. package/dist/server/app.d.ts +20 -2
  59. package/dist/server/app.js +39 -11
  60. package/dist/store/migrate.d.ts +2 -0
  61. package/dist/store/migrate.js +22 -0
  62. package/dist/store/migrations.d.ts +12 -0
  63. package/dist/store/migrations.js +81 -0
  64. package/dist/store/store.d.ts +16 -0
  65. package/dist/store/store.js +55 -0
  66. package/dist/store/store.types.d.ts +7 -0
  67. package/dist/store/store.types.js +1 -0
  68. package/dist/store/test-store.d.ts +5 -0
  69. package/dist/store/test-store.js +39 -0
  70. package/dist/testing/test-server.d.ts +6 -0
  71. package/dist/testing/test-server.js +31 -9
  72. package/dist/tool/build.d.ts +4 -0
  73. package/dist/tool/build.js +21 -26
  74. package/dist/tool/check.d.ts +16 -0
  75. package/dist/tool/check.js +30 -0
  76. package/dist/tool/docker.js +3 -2
  77. package/dist/tool/exec.d.ts +5 -0
  78. package/dist/tool/exec.js +48 -9
  79. package/dist/tool/index.d.ts +8 -0
  80. package/dist/tool/index.js +8 -0
  81. package/dist/tool/kit-version.d.ts +1 -0
  82. package/dist/tool/kit-version.js +3 -0
  83. package/dist/tool/main.d.ts +6 -0
  84. package/dist/tool/main.js +60 -35
  85. package/dist/tool/record-extras.d.ts +4 -0
  86. package/dist/tool/record-extras.js +56 -0
  87. package/dist/tool/stage.d.ts +4 -0
  88. package/dist/tool/stage.js +39 -21
  89. package/dist/tool/timeouts.d.ts +10 -0
  90. package/dist/tool/timeouts.js +19 -0
  91. package/dist/tool/up/compose-run.d.ts +15 -0
  92. package/dist/tool/up/compose-run.js +25 -0
  93. package/dist/tool/up/compose.d.ts +19 -0
  94. package/dist/tool/up/compose.js +80 -0
  95. package/dist/tool/up/dev-issuer-app.d.ts +22 -0
  96. package/dist/tool/up/dev-issuer-app.js +80 -0
  97. package/dist/tool/up/dev-issuer.d.ts +2 -0
  98. package/dist/tool/up/dev-issuer.js +6 -0
  99. package/dist/tool/up/down.d.ts +13 -0
  100. package/dist/tool/up/down.js +47 -0
  101. package/dist/tool/up/gateway.d.ts +36 -0
  102. package/dist/tool/up/gateway.js +60 -0
  103. package/dist/tool/up/lines.d.ts +2 -0
  104. package/dist/tool/up/lines.js +3 -0
  105. package/dist/tool/up/local-record.d.ts +44 -0
  106. package/dist/tool/up/local-record.js +28 -0
  107. package/dist/tool/up/ports.d.ts +2 -0
  108. package/dist/tool/up/ports.js +22 -0
  109. package/dist/tool/up/project.d.ts +4 -0
  110. package/dist/tool/up/project.js +20 -0
  111. package/dist/tool/up/store-line.d.ts +11 -0
  112. package/dist/tool/up/store-line.js +36 -0
  113. package/dist/tool/up/token.d.ts +6 -0
  114. package/dist/tool/up/token.js +25 -0
  115. package/dist/tool/up/up.d.ts +16 -0
  116. package/dist/tool/up/up.js +84 -0
  117. package/dist/tool/up/wait.d.ts +8 -0
  118. package/dist/tool/up/wait.js +26 -0
  119. package/dist/ui/ui-app.d.ts +8 -0
  120. package/dist/ui/ui-app.js +39 -0
  121. package/dist/worker/cron.d.ts +11 -0
  122. package/dist/worker/cron.js +73 -0
  123. package/dist/worker/job-options.d.ts +9 -0
  124. package/dist/worker/job-options.js +17 -0
  125. package/dist/worker/run-log.d.ts +9 -0
  126. package/dist/worker/run-log.js +38 -0
  127. package/dist/worker/runs.d.ts +20 -0
  128. package/dist/worker/runs.js +59 -0
  129. package/dist/worker/scheduler.d.ts +20 -0
  130. package/dist/worker/scheduler.js +106 -0
  131. package/dist/worker/worker.types.d.ts +5 -0
  132. package/dist/worker/worker.types.js +2 -0
  133. package/package.json +11 -2
  134. package/schemas/extension-discovery.schema.json +5 -0
  135. package/schemas/extension-package.schema.json +3 -0
  136. package/schemas/extension-record.schema.json +5 -0
  137. package/schemas/health.schema.json +1 -1
@@ -4,21 +4,30 @@ import { normalizeParts } from '../manifest/parts.js';
4
4
  import {} from '../manifest/primitives.schema.js';
5
5
  import { ExtensionRecordSchema } from './discovery.schema.js';
6
6
  export const DEFAULT_RECORD_FILE = '/run/agentage/agentage-extension.json';
7
- const partsOf = (pkg, images) => {
7
+ const partsOf = (pkg, images, migrations) => {
8
8
  const parts = {};
9
9
  for (const [kind, image] of Object.entries(images))
10
10
  parts[kind] = { image };
11
11
  const store = normalizeParts(pkg.agentage.parts).store;
12
12
  if (store)
13
- parts['store'] = { kind: store };
13
+ parts['store'] = migrations === undefined ? { kind: store } : { kind: store, migrations };
14
14
  return parts;
15
15
  };
16
- // From the manifest, the build and the pushed images by digest; OpenAPI, tools, widgets come later.
17
- export const recordOf = (pkg, build, images) => {
16
+ // From the manifest, the build, the images by digest and what the code serves; OpenAPI comes later.
17
+ export const recordOf = (pkg, build, images, extras = {}) => {
18
18
  const { id, commands } = pkg.agentage;
19
19
  const { commit, buildTime } = build;
20
- const record = { id, version: pkg.version, commit, buildTime, parts: partsOf(pkg, images) };
21
- return commands ? { ...record, commands: commands.map((command) => command.name) } : record;
20
+ const parts = partsOf(pkg, images, extras.migrations);
21
+ const record = { id, version: pkg.version, commit, buildTime, parts };
22
+ if (extras.openapi)
23
+ record.openapi = extras.openapi;
24
+ if (extras.tools)
25
+ record.tools = extras.tools;
26
+ if (commands)
27
+ record.commands = commands.map((command) => command.name);
28
+ if (extras.widgets)
29
+ record.widgets = extras.widgets;
30
+ return record;
22
31
  };
23
32
  export const validateRecord = (value) => {
24
33
  const result = ExtensionRecordSchema.safeParse(value);
@@ -1,4 +1,9 @@
1
1
  import { z } from 'zod';
2
+ export declare const WidgetSchema: z.ZodObject<{
3
+ name: z.ZodString;
4
+ element: z.ZodString;
5
+ module: z.ZodString;
6
+ }, z.core.$strict>;
2
7
  export declare const ExtensionRecordSchema: z.ZodObject<{
3
8
  id: z.ZodString;
4
9
  version: z.ZodString;
@@ -26,6 +31,7 @@ export declare const ExtensionRecordSchema: z.ZodObject<{
26
31
  git: "git";
27
32
  signoz: "signoz";
28
33
  }>;
34
+ migrations: z.ZodOptional<z.ZodInt>;
29
35
  }, z.core.$strict>>;
30
36
  }, z.core.$strict>;
31
37
  openapi: z.ZodOptional<z.ZodString>;
@@ -65,6 +71,7 @@ export declare const DiscoveryDocumentSchema: z.ZodObject<{
65
71
  git: "git";
66
72
  signoz: "signoz";
67
73
  }>;
74
+ migrations: z.ZodOptional<z.ZodInt>;
68
75
  }, z.core.$strict>>;
69
76
  }, z.core.$strict>;
70
77
  openapi: z.ZodOptional<z.ZodString>;
@@ -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.strictObject({ kind: StoreKindSchema }).optional(),
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',
@@ -22,7 +24,7 @@ const OriginSchema = z
22
24
  .string()
23
25
  .max(270)
24
26
  .regex(/^(https:\/\/[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+|http:\/\/(127\.0\.0\.1|localhost))(:[0-9]{1,5})?$/);
25
- const WidgetSchema = z.strictObject({
27
+ export const WidgetSchema = z.strictObject({
26
28
  name: z.string().regex(/^[a-z][a-z0-9-]*$/),
27
29
  element: z.string().regex(/^[a-z][a-z0-9]*-[a-z0-9-]+$/),
28
30
  module: z.string().regex(/^\/ui\/[^/]+\/[^/]+\.m?js$/),
@@ -0,0 +1,3 @@
1
+ export declare const OPENAPI_FILE = "openapi.json";
2
+ export declare const OPENAPI_PATH = "/api/v1/openapi.json";
3
+ export declare const readOpenapi: (dir: string) => Promise<Record<string, unknown> | null>;
@@ -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-]*$/), HealthStatusSchema),
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 { createTestContext, type ExtensionInfo, type LogEntry, type PartContext, type ServerPartKind, type SettingValue, type TestContext, type TestContextOptions, } from './context/context.js';
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 { startTestServer, type TestServer, type TestServerOptions } from './testing/test-server.js';
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 { createTestContext, } from './context/context.js';
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 { startTestServer } from './testing/test-server.js';
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
@@ -1,4 +1,4 @@
1
- import { currentAgcid } from '../agcid/scope.js';
1
+ import { currentAgcid } from '../agcid/current.js';
2
2
  export const messageOf = (error) => error instanceof Error ? error.message : String(error);
3
3
  const errorFields = (error) => {
4
4
  if (!(error instanceof Error))
@@ -0,0 +1,5 @@
1
+ import { type ExtensionPackage } from './manifest.schema.js';
2
+ import { type ImagePartKind } from './primitives.schema.js';
3
+ export type ImportModule = (file: string) => Promise<unknown>;
4
+ export declare const createOf: (mod: unknown, file: string) => ((ctx: never) => unknown);
5
+ export declare const loadCreate: <C>(dir: string, pkg: ExtensionPackage, part: ImagePartKind | "cli", importModule?: ImportModule) => Promise<(ctx: C) => unknown>;
@@ -0,0 +1,29 @@
1
+ import { join } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ import {} from './manifest.schema.js';
4
+ import { entryOf } from './parts.js';
5
+ import {} from './primitives.schema.js';
6
+ const importFile = (file) => import(pathToFileURL(file).href);
7
+ export const createOf = (mod, file) => {
8
+ const create = mod?.create;
9
+ if (typeof create !== 'function')
10
+ throw new Error(`${file} does not export create(ctx)`);
11
+ return create;
12
+ };
13
+ // The entry itself missing (not a module it imports) means the package was never built.
14
+ const notBuilt = (err, file) => {
15
+ const missing = err?.code === 'ERR_MODULE_NOT_FOUND';
16
+ const message = String(err?.message);
17
+ const named = [file, pathToFileURL(file).href].some((name) => message.includes(`'${name}'`));
18
+ if (!missing || !named)
19
+ return err;
20
+ return new Error(`${file} not found: build the package first (npm run build)`);
21
+ };
22
+ // The one way the kit reaches part code: import the part's entry, take its create(ctx).
23
+ export const loadCreate = async (dir, pkg, part, importModule = importFile) => {
24
+ const file = join(dir, entryOf(pkg, part));
25
+ const mod = await importModule(file).catch((err) => {
26
+ throw notBuilt(err, file);
27
+ });
28
+ return createOf(mod, file);
29
+ };
@@ -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 {};
@@ -27,6 +27,7 @@ const CommandSchema = z.strictObject({
27
27
  .array(z.strictObject({
28
28
  name: CommandNameSchema,
29
29
  type: ArgTypeSchema,
30
+ optional: z.boolean().optional(),
30
31
  required: z.boolean().optional(),
31
32
  description: z.string().min(1).max(200).optional(),
32
33
  }))
@@ -9,3 +9,4 @@ export interface NormalizedParts {
9
9
  export declare const normalizeParts: (parts: Parts) => NormalizedParts;
10
10
  export declare const imageParts: (pkg: ExtensionPackage) => ImagePartKind[];
11
11
  export declare const entryOf: (pkg: ExtensionPackage, part: ImagePartKind | "cli") => string;
12
+ export declare const hasPostgres: (pkg: ExtensionPackage) => boolean;
@@ -22,3 +22,4 @@ export const imageParts = (pkg) => {
22
22
  return IMAGE_PART_KINDS.filter((kind) => code[kind] !== undefined);
23
23
  };
24
24
  export const entryOf = (pkg, part) => normalizeParts(pkg.agentage.parts).code[part]?.entry ?? `./dist/${part}/index.js`;
25
+ export const hasPostgres = (pkg) => normalizeParts(pkg.agentage.parts).store === 'postgres';
@@ -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,94 @@
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 { strongest } from '../auth/token-check.js';
7
+ import { fail } from '../envelope/envelope.js';
8
+ import { BODY_LIMIT, createPartApp, PUBLIC_CORS } from '../server/app.js';
9
+ import { serverFactory } from './server.js';
10
+ import { prefixed } from './tools.js';
11
+ const MCP_HEADERS = ['Mcp-Protocol-Version', 'Mcp-Method', 'Mcp-Name', 'Accept'];
12
+ const MCP_CORS = {
13
+ ...PUBLIC_CORS,
14
+ allowedHeaders: [...PUBLIC_CORS.allowedHeaders, ...MCP_HEADERS],
15
+ };
16
+ const parsed = (body) => {
17
+ try {
18
+ return JSON.parse(body.toString('utf8'));
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
24
+ const messagesOf = (body) => (Array.isArray(body) ? body : [body]);
25
+ const isListing = (body) => messagesOf(body).some((message) => message?.method === 'tools/list');
26
+ // Each tools/call needs its tool's declared scope (read when it declares none).
27
+ const callAccess = (options, body) => messagesOf(body)
28
+ .filter((message) => message?.method === 'tools/call')
29
+ .map((message) => {
30
+ const id = options.pkg.agentage.id;
31
+ const entry = options.tools.find((t) => prefixed(id, t.name) === message?.params?.name);
32
+ return entry?.spec.scope ?? 'read';
33
+ });
34
+ // Tokens off: calls stay anonymous and a closed listing refuses every tools/list (nothing can verify).
35
+ const guard = async (options, req, body) => {
36
+ const listing = options.listing === 'token' && isListing(body) ? ['read'] : [];
37
+ const needed = [...listing, ...callAccess(options, body)];
38
+ if (!options.tokenCheck) {
39
+ if (listing.length > 0)
40
+ throw fail('UNAUTHORIZED', 'tools/list needs a token');
41
+ return;
42
+ }
43
+ if (needed.length > 0)
44
+ await options.tokenCheck(req.get('authorization'), strongest(needed));
45
+ };
46
+ const webRequest = (req, body) => {
47
+ const headers = new Headers();
48
+ for (const [key, value] of Object.entries(req.headers)) {
49
+ if (typeof value === 'string')
50
+ headers.set(key, value);
51
+ }
52
+ return new globalThis.Request(`http://${req.get('host') ?? 'mcp'}/mcp`, {
53
+ method: 'POST',
54
+ headers,
55
+ body,
56
+ });
57
+ };
58
+ const send = async (response, res) => {
59
+ res.status(response.status);
60
+ response.headers.forEach((value, key) => void res.setHeader(key, value));
61
+ if (!response.body)
62
+ return void res.end();
63
+ await pipeline(Readable.fromWeb(response.body), res);
64
+ };
65
+ const handle = (handler, options) => async (req, res) => {
66
+ const body = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
67
+ const message = parsed(body);
68
+ await guard(options, req, message);
69
+ await send(await handler.fetch(webRequest(req, body), { parsedBody: message }), res);
70
+ };
71
+ const methodNotAllowed = (_req, res) => {
72
+ res.setHeader('Allow', 'POST');
73
+ res
74
+ .status(405)
75
+ .json({ jsonrpc: '2.0', error: { code: -32000, message: 'Method not allowed.' }, id: null });
76
+ };
77
+ // Stateless Streamable HTTP on SDK v2: POST /mcp only, GET and DELETE (sessions) answer 405.
78
+ export const createMcpApp = (options) => {
79
+ const identity = { id: options.pkg.agentage.id, version: options.pkg.version };
80
+ const handler = createMcpHandler(serverFactory(identity, options.tools, options.log), {
81
+ legacy: 'stateless',
82
+ onerror: (err) => options.log.warn('mcp request refused', { reason: err.message }),
83
+ });
84
+ const raw = express.raw({ type: () => true, limit: BODY_LIMIT });
85
+ return createPartApp({
86
+ ...options,
87
+ part: 'mcp',
88
+ mount: (app) => {
89
+ app.use('/mcp', cors(MCP_CORS));
90
+ app.post('/mcp', raw, handle(handler, options));
91
+ app.all('/mcp', methodNotAllowed);
92
+ },
93
+ });
94
+ };
@@ -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,38 @@
1
+ import { McpServer } from '@modelcontextprotocol/server';
2
+ import { agcidOf } from '../agcid/agcid.js';
3
+ import { currentAgcid } from '../agcid/current.js';
4
+ import { fail, isFailure } from '../envelope/envelope.js';
5
+ import {} from '../log/log.js';
6
+ import { prefixed } from './tools.js';
7
+ const text = (value) => [{ type: 'text', text: JSON.stringify(value) }];
8
+ // A failed tool is isError with the REST error shape; anything unexpected is a generic INTERNAL.
9
+ // Outside a request (no agcid in scope) the error still carries a valid one, minted here.
10
+ const errorResult = (err, log) => {
11
+ const failure = isFailure(err) ? err : fail('INTERNAL', 'Internal error');
12
+ if (failure.status >= 500)
13
+ log.error(err);
14
+ const error = { code: failure.code, message: failure.message, agcid: agcidOf(currentAgcid()) };
15
+ return { isError: true, content: text({ error }), structuredContent: { error } };
16
+ };
17
+ const callback = (entry, log) => async (args) => {
18
+ try {
19
+ const output = entry.spec.output.parse(await entry.handler(args));
20
+ return { content: text(output), structuredContent: output };
21
+ }
22
+ catch (err) {
23
+ return errorResult(err, log);
24
+ }
25
+ };
26
+ // One fresh server per request (stateless); serverInfo agentage-{id} at the package version.
27
+ export const serverFactory = (identity, tools, log) => () => {
28
+ const server = new McpServer({ name: `agentage-${identity.id}`, version: identity.version });
29
+ for (const entry of tools) {
30
+ const config = {
31
+ description: entry.spec.description,
32
+ inputSchema: entry.spec.input,
33
+ outputSchema: entry.spec.output,
34
+ };
35
+ server.registerTool(prefixed(identity.id, entry.name), config, callback(entry, log));
36
+ }
37
+ return server;
38
+ };
@@ -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,26 @@
1
+ import { type z } from 'zod';
2
+ import { type Access } from '../auth/token-check.js';
3
+ import { type PartContext } from '../context/context.js';
4
+ type ObjectSchema = z.ZodObject;
5
+ export interface ToolSpec<I extends ObjectSchema, O extends ObjectSchema> {
6
+ description: string;
7
+ input: I;
8
+ output: O;
9
+ scope?: Access;
10
+ }
11
+ export type ToolHandler<I extends ObjectSchema, O extends ObjectSchema> = (args: z.output<I>) => z.input<O> | Promise<z.input<O>>;
12
+ export type ToolRegistrar = <I extends ObjectSchema, O extends ObjectSchema>(name: string, spec: ToolSpec<I, O>, handler: ToolHandler<I, O>) => void;
13
+ export interface McpContext extends PartContext {
14
+ readonly tool: ToolRegistrar;
15
+ }
16
+ export interface ToolEntry {
17
+ name: string;
18
+ spec: ToolSpec<ObjectSchema, ObjectSchema>;
19
+ handler: (args: Record<string, unknown>) => unknown;
20
+ }
21
+ export declare const createToolRegistry: () => {
22
+ tool: ToolRegistrar;
23
+ tools: ToolEntry[];
24
+ };
25
+ export declare const prefixed: (id: string, name: string) => string;
26
+ export {};
@@ -0,0 +1,21 @@
1
+ import {} from 'zod';
2
+ import {} from '../auth/token-check.js';
3
+ import {} from '../context/context.js';
4
+ const TOOL_NAME = /^[a-z][a-z0-9_-]*$/;
5
+ const SCOPES = ['public', 'read', 'write', 'admin'];
6
+ export const createToolRegistry = () => {
7
+ const tools = [];
8
+ const tool = (name, spec, handler) => {
9
+ if (!TOOL_NAME.test(name))
10
+ throw new Error(`tool name ${name}: lowercase, digits, _ and -`);
11
+ if (tools.some((entry) => entry.name === name))
12
+ throw new Error(`tool ${name} is registered twice`);
13
+ if (!spec.description)
14
+ throw new Error(`tool ${name} needs a description: it routes the model`);
15
+ if (spec.scope !== undefined && !SCOPES.includes(spec.scope))
16
+ throw new Error(`tool ${name} scope ${String(spec.scope)}: one of ${SCOPES.join(', ')}`);
17
+ tools.push({ name, spec, handler: handler });
18
+ };
19
+ return { tool, tools };
20
+ };
21
+ 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,4 @@
1
+ import { type PartContext } from '../context/context.js';
2
+ export type PagesContext = PartContext;
3
+ export type PagesSite = Readonly<Record<string, string>>;
4
+ export declare const versionPage: (ctx: PagesContext) => string;
@@ -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
+ };