@segmentstream-cloud/ontology 0.10.0 → 0.11.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.
Files changed (56) hide show
  1. package/dist/action.d.ts +19 -0
  2. package/dist/action.js +1 -0
  3. package/dist/analyze.d.ts +3 -1
  4. package/dist/analyze.js +81 -1
  5. package/dist/bin/migrate.js +3 -2
  6. package/dist/diagnostics.d.ts +3 -3
  7. package/dist/diagnostics.js +1 -0
  8. package/dist/index.d.ts +5 -3
  9. package/dist/index.js +4 -1
  10. package/dist/manifest.d.ts +48 -0
  11. package/dist/manifest.js +9 -0
  12. package/dist/migrate/apply.d.ts +7 -0
  13. package/dist/migrate/apply.js +18 -1
  14. package/dist/migrate/index.d.ts +1 -1
  15. package/dist/migrate/index.js +1 -1
  16. package/dist/migrate/model.d.ts +8 -0
  17. package/dist/migrate/plan.d.ts +6 -0
  18. package/dist/migrate/plan.js +10 -2
  19. package/dist/migrate/storage.d.ts +12 -0
  20. package/dist/migrate/storage.js +82 -0
  21. package/dist/names.d.ts +9 -1
  22. package/dist/names.js +11 -0
  23. package/dist/nitro.d.ts +13 -0
  24. package/dist/nitro.js +18 -0
  25. package/dist/runtime/generated-actions.d.ts +3 -0
  26. package/dist/runtime/generated-actions.js +3 -2
  27. package/dist/runtime/oauth.d.ts +9 -0
  28. package/dist/runtime/oauth.js +165 -0
  29. package/dist/runtime/principals.d.ts +5 -0
  30. package/dist/runtime/principals.js +15 -0
  31. package/dist/runtime/service-binding.js +2 -0
  32. package/dist/service/api.js +4 -10
  33. package/dist/service/authentication.d.ts +27 -1
  34. package/dist/service/authentication.js +48 -26
  35. package/dist/service/config.d.ts +5 -0
  36. package/dist/service/descriptions.d.ts +8 -0
  37. package/dist/service/descriptions.js +15 -0
  38. package/dist/service/errors.d.ts +5 -1
  39. package/dist/service/errors.js +6 -2
  40. package/dist/service/handlers.d.ts +1 -4
  41. package/dist/service/handlers.js +20 -203
  42. package/dist/service/mcp/server.d.ts +24 -0
  43. package/dist/service/mcp/server.js +370 -0
  44. package/dist/service/mcp/tools.d.ts +47 -0
  45. package/dist/service/mcp/tools.js +218 -0
  46. package/dist/service/oauth/protocol.d.ts +52 -0
  47. package/dist/service/oauth/protocol.js +110 -0
  48. package/dist/service/oauth/server.d.ts +27 -0
  49. package/dist/service/oauth/server.js +576 -0
  50. package/dist/service/operations.d.ts +114 -0
  51. package/dist/service/operations.js +218 -0
  52. package/dist/service/runtime-contract.d.ts +89 -0
  53. package/dist/service/schemas.d.ts +8 -0
  54. package/dist/service/service.d.ts +3 -3
  55. package/dist/service/service.js +10 -5
  56. package/package.json +6 -1
package/dist/action.d.ts CHANGED
@@ -17,6 +17,17 @@ export interface ActionContext<Input, Prepared = undefined> extends PrepareConte
17
17
  }
18
18
  /** What `prepare` may fail with: the action errors, and connection calls that failed. */
19
19
  export type PrepareError = ActionError | ConnectionError;
20
+ /**
21
+ * How an action behaves, as MCP tool annotations tell agents. Clients such as Claude ask the user
22
+ * before they run a destructive tool. Without hints an action is taken as destructive and not
23
+ * idempotent.
24
+ */
25
+ export interface ActionHints {
26
+ /** Whether the action may overwrite or remove what someone decided. Default true. */
27
+ readonly destructive?: boolean;
28
+ /** Whether running it again with the same input changes nothing more. Default false. */
29
+ readonly idempotent?: boolean;
30
+ }
20
31
  /** A described, typed, permissioned Effect function: the only way to change records. */
21
32
  export interface Action<Input extends Schema.Top = Schema.Top, Result = unknown> {
22
33
  readonly [ActionTypeId]: typeof ActionTypeId;
@@ -27,6 +38,8 @@ export interface Action<Input extends Schema.Top = Schema.Top, Result = unknown>
27
38
  * is not set, the action requires `<entity>:<action name>`, for example `lead:changeStatus`.
28
39
  */
29
40
  readonly permission: string | undefined;
41
+ /** How the action behaves, as `action({ hints })` set it; see {@link ActionHints}. */
42
+ readonly hints: ActionHints | undefined;
30
43
  /** The connections `prepare` may call; empty for an action without `prepare`. */
31
44
  readonly connections: ReadonlyArray<AnyConnectionHandle>;
32
45
  /**
@@ -64,6 +77,12 @@ interface ActionOptions<Input extends Schema.Top> {
64
77
  * Roles grant it in their `permissions`.
65
78
  */
66
79
  readonly permission?: string;
80
+ /**
81
+ * How the action behaves, for the agents that run it over MCP. Set `destructive: false` for an
82
+ * action that only adds or that history makes easy to reverse, and `idempotent: true` for one
83
+ * that a repeated call leaves as it was.
84
+ */
85
+ readonly hints?: ActionHints;
67
86
  }
68
87
  export interface ActionConfig<Input extends Schema.Top, Result> extends ActionOptions<Input> {
69
88
  /**
package/dist/action.js CHANGED
@@ -11,6 +11,7 @@ export function action(config) {
11
11
  description: config.description,
12
12
  input: config.input,
13
13
  permission: config.permission,
14
+ hints: config.hints,
14
15
  connections: config.connections ?? [],
15
16
  ...(config.prepare === undefined ? {} : { prepare: config.prepare }),
16
17
  // A prepare-only action answers with what prepare returned; it writes nothing.
package/dist/analyze.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Schema } from "effect";
2
- import { type Action } from "./action.js";
2
+ import { type Action, type ActionHints } from "./action.js";
3
3
  import { type AnyConnectionHandle } from "./connection.js";
4
4
  import type { Diagnostic } from "./diagnostics.js";
5
5
  import { type AnyEntity } from "./entity.js";
@@ -67,6 +67,8 @@ export interface ResolvedAction {
67
67
  readonly input: JsonSchemaObject | undefined;
68
68
  /** The keys of the connections the action's `prepare` calls, in declaration order. */
69
69
  readonly connections: ReadonlyArray<string>;
70
+ /** The hints the action declares, or `undefined` when it declares none. */
71
+ readonly hints: ActionHints | undefined;
70
72
  }
71
73
  /** A connection the ontology declares. */
72
74
  export interface ResolvedConnection {
package/dist/analyze.js CHANGED
@@ -5,7 +5,7 @@ import { BLOB_CONTENT_TYPES, MAX_BLOB_BYTES } from "./blob.js";
5
5
  import { isAction } from "./action.js";
6
6
  import { CONNECTION_KEY, isConnection } from "./connection.js";
7
7
  import { isEntity } from "./entity.js";
8
- import { LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MEMBER_ROLE, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
8
+ import { LEGACY_MEMBER_NAME, ENTITY_ID, GENERATED_ACTIONS, GENERATED_UPSERT_PREFIX, HISTORY_SUFFIX, MCP_READ_TOOLS, MCP_TOOL_NAME_MAX_LENGTH, MEMBER_ROLE, mcpToolName, RESERVED_ENTITY_NAMES, RESERVED_MEMBER_NAMES, RESERVED_RELATIONSHIP_NAMES, SNAKE_CASE_NAME, SYSTEM_PROPERTIES, isSystemProperty, } from "./names.js";
9
9
  import { REFERENCE_ANNOTATION, propertyDefinition, referenceTarget, } from "./property.js";
10
10
  import { generatedActionPermissions, parsePermission, permissionOf, READ_VERB, rolesGranting, WILDCARD, } from "./permissions.js";
11
11
  import { ON_DELETE, isRelationship } from "./relationship.js";
@@ -58,6 +58,7 @@ export function analyze(config, options = {}) {
58
58
  const resolved = entities.map((candidate) => resolveEntity(candidate, scope));
59
59
  checkEntityIdentities(resolved, report);
60
60
  checkMemberNames(resolved, report);
61
+ checkToolNames(resolved, report);
61
62
  checkWarehouseSource(resolved, report);
62
63
  checkConnectionsUsed(resolved, connections, config.connections ?? [], report);
63
64
  if (options.checkPermissions !== false) {
@@ -65,6 +66,58 @@ export function analyze(config, options = {}) {
65
66
  }
66
67
  return { entities: resolved, roles, signIn, connections, diagnostics };
67
68
  }
69
+ /**
70
+ * The operations of a stored entity that become MCP tools, each with the path a problem is reported
71
+ * at: the reads, the generated actions and the authored actions.
72
+ */
73
+ function toolOperations(candidate) {
74
+ const { permissions } = candidate;
75
+ const generated = [
76
+ ...(permissions.create !== undefined ? ["create", "update", "delete"] : []),
77
+ ...(permissions.create !== undefined
78
+ ? candidate.properties
79
+ .filter(({ definition }) => definition.unique)
80
+ .map(({ name }) => `${GENERATED_UPSERT_PREFIX}${name}`)
81
+ : []),
82
+ ...(permissions.link !== undefined ? ["link", "unlink"] : []),
83
+ ];
84
+ return [
85
+ ...[...MCP_READ_TOOLS, ...generated].map((operation) => ({ operation, path: candidate.name })),
86
+ // An authored action with a generated action's name is already refused as a reserved name.
87
+ ...candidate.actions
88
+ .filter(({ name }) => !GENERATED_ACTIONS.includes(name) && !GENERATED_UPSERT.test(name))
89
+ .map(({ name }) => ({ operation: name, path: `${candidate.name}.actions.${name}` })),
90
+ ];
91
+ }
92
+ /**
93
+ * Every MCP tool needs a name of its own, at most 64 characters long. A tool's name joins the
94
+ * entity's and the operation's with an underscore, so entity `pilot` with an action `request_find`
95
+ * would take the name of entity `pilot_request`'s `find`.
96
+ */
97
+ function checkToolNames(entities, report) {
98
+ const owners = new Map();
99
+ const seen = new Set();
100
+ for (const candidate of entities) {
101
+ // A second entity with the same name is already refused as a duplicate.
102
+ if (candidate.entity.definition.source !== undefined || seen.has(candidate.name)) {
103
+ continue;
104
+ }
105
+ seen.add(candidate.name);
106
+ for (const { operation, path } of toolOperations(candidate)) {
107
+ const name = mcpToolName(candidate.name, operation);
108
+ const owner = owners.get(name);
109
+ if (owner !== undefined) {
110
+ report("invalid-tool-name", path, `its MCP tool ${name} is already the tool of ${owner}; rename one of them.`);
111
+ continue;
112
+ }
113
+ owners.set(name, `${candidate.name}'s ${operation}`);
114
+ if (name.length > MCP_TOOL_NAME_MAX_LENGTH) {
115
+ report("invalid-tool-name", path, `its MCP tool ${name} is ${name.length} characters long; agents accept at most ` +
116
+ `${MCP_TOOL_NAME_MAX_LENGTH}. Shorten the entity or the action name.`);
117
+ }
118
+ }
119
+ }
120
+ }
68
121
  /** The handles `defineOntology({ connections })` lists, each with a valid key and a description. */
69
122
  function collectConnections(config, report) {
70
123
  const declared = config.connections;
@@ -546,10 +599,37 @@ function resolveActions(candidate, scope) {
546
599
  permission: resolvePermission(path, permissionOf(candidate.name, name), action, scope),
547
600
  input: resolveInput(path, action.input, scope),
548
601
  connections: resolveActionConnections(path, action, scope),
602
+ hints: resolveHints(path, action, report),
549
603
  });
550
604
  }
551
605
  return actions;
552
606
  }
607
+ const HINT_NAMES = ["destructive", "idempotent"];
608
+ /** The action's `hints`: an object of booleans, `destructive` and `idempotent`. */
609
+ function resolveHints(path, action, report) {
610
+ const declared = action.hints;
611
+ if (declared === undefined) {
612
+ return undefined;
613
+ }
614
+ const at = `${path}.hints`;
615
+ if (!isPlainObject(declared)) {
616
+ report("invalid-action", at, "hints is an object such as { destructive: false }.");
617
+ return undefined;
618
+ }
619
+ const hints = {};
620
+ for (const [key, value] of Object.entries(declared)) {
621
+ if (!HINT_NAMES.includes(key)) {
622
+ report("invalid-action", `${at}.${key}`, `is not a hint; use ${HINT_NAMES.join(" or ")}.`);
623
+ }
624
+ else if (typeof value !== "boolean") {
625
+ report("invalid-action", `${at}.${key}`, "is true or false.");
626
+ }
627
+ else {
628
+ hints[key] = value;
629
+ }
630
+ }
631
+ return hints;
632
+ }
553
633
  /** The action's own `permission`, which names an entity and a verb without wildcards. */
554
634
  function resolvePermission(path, fallback, action, scope) {
555
635
  const declared = action.permission;
@@ -2,7 +2,7 @@ import * as PgClient from "@effect/sql-pg/PgClient";
2
2
  import { Cause, Effect, Redacted } from "effect";
3
3
  import { SqlClient } from "effect/unstable/sql";
4
4
  import { isSqlError } from "effect/unstable/sql/SqlError";
5
- import { applyMigration, readCatalog } from "../migrate/apply.js";
5
+ import { applyMigration, readCatalog, readRuntimeTables, } from "../migrate/apply.js";
6
6
  import { MigrationRefused } from "../migrate/errors.js";
7
7
  import { isNoopMigration } from "../migrate/model.js";
8
8
  import { planMigration } from "../migrate/plan.js";
@@ -88,7 +88,8 @@ function migrateDatabase(input) {
88
88
  return Effect.gen(function* () {
89
89
  const sql = yield* SqlClient.SqlClient;
90
90
  const current = yield* readCatalog({ sql });
91
- const plan = planMigration({ current, target });
91
+ const existingTables = yield* readRuntimeTables({ sql });
92
+ const plan = planMigration({ current, target, existingTables });
92
93
  output.stdout(formatPlan(plan, { sql: input.planOnly }));
93
94
  if (plan.refusals.length > 0) {
94
95
  output.stderr(formatRefusals(plan.refusals));
@@ -1,10 +1,10 @@
1
1
  import { Schema } from "effect";
2
- export declare const DIAGNOSTIC_CODES: readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare"];
2
+ export declare const DIAGNOSTIC_CODES: readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"];
3
3
  export type DiagnosticCode = (typeof DIAGNOSTIC_CODES)[number];
4
4
  /** A problem `check` found in an ontology definition. */
5
5
  export declare const Diagnostic: Schema.Struct<{
6
6
  readonly severity: Schema.Literals<readonly ["error", "warning"]>;
7
- readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare"]>;
7
+ readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"]>;
8
8
  /** Where the problem is, for example `lead.properties.status.options[1]`. */
9
9
  readonly path: Schema.String;
10
10
  readonly message: Schema.String;
@@ -14,7 +14,7 @@ declare const InvalidOntology_base: Schema.Class<InvalidOntology, Schema.TaggedS
14
14
  readonly message: Schema.String;
15
15
  readonly diagnostics: Schema.$Array<Schema.Struct<{
16
16
  readonly severity: Schema.Literals<readonly ["error", "warning"]>;
17
- readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare"]>;
17
+ readonly code: Schema.Literals<readonly ["invalid-source", "missing-column", "invalid-column", "invalid-scope", "source-with-actions", "source-with-index", "invalid-measure", "unknown-measure", "circular-measure", "invalid-join", "missing-join", "invalid-entity", "invalid-property", "invalid-relationship", "invalid-actions", "invalid-action", "invalid-action-input", "invalid-role", "invalid-name", "reserved-name", "duplicate-name", "invalid-entity-id", "duplicate-entity-id", "missing-label", "missing-description", "invalid-title", "invalid-options", "invalid-default", "invalid-constraint", "unknown-relationship-target", "inverse-collision", "invalid-on-delete", "invalid-phrase", "invalid-index", "unknown-reference", "invalid-renamed-from", "invalid-guide", "managed-without-effect", "missing-permissions", "invalid-permission", "unknown-permission", "removed-allow", "unreachable-action", "wildcard-permission", "invalid-sign-in", "invalid-connection", "unknown-connection", "unused-connection", "invalid-prepare", "invalid-tool-name"]>;
18
18
  /** Where the problem is, for example `lead.properties.status.options[1]`. */
19
19
  readonly path: Schema.String;
20
20
  readonly message: Schema.String;
@@ -49,6 +49,7 @@ export const DIAGNOSTIC_CODES = [
49
49
  "unknown-connection",
50
50
  "unused-connection",
51
51
  "invalid-prepare",
52
+ "invalid-tool-name",
52
53
  ];
53
54
  /** A problem `check` found in an ontology definition. */
54
55
  export const Diagnostic = Schema.Struct({
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export type { WarehouseSource } from "./source.js";
4
4
  export { m } from "./measure.js";
5
5
  export type { Measure, Measures, MeasureExpression, MeasureOptions } from "./measure.js";
6
6
  export { action, isAction } from "./action.js";
7
- export type { Action, ActionConfig, ActionConfigWithConnections, ActionContext, ActionEncodedInput, ActionInput, ActionResult, Actions, AnyAction, PrepareContext, PrepareError, } from "./action.js";
7
+ export type { Action, ActionConfig, ActionConfigWithConnections, ActionContext, ActionEncodedInput, ActionHints, ActionInput, ActionResult, Actions, AnyAction, PrepareContext, PrepareError, } from "./action.js";
8
8
  export { CONNECTION_ERROR_REASONS, CONNECTION_KEY, CONNECTION_METHODS, connection, ConnectionError, Connections, ConnectionTransport, isConnection, MAXIMUM_CONNECTION_BODY_BYTES, } from "./connection.js";
9
9
  export type { AnyConnectionHandle, Connection, ConnectionCall, ConnectionClient, ConnectionErrorReason, ConnectionHandle, ConnectionHeaders, ConnectionMethod, ConnectionOptions, ConnectionReply, ConnectionRequest, ConnectionRequestOptions, ConnectionResponse, ConnectionsOf, ConnectionStub, StubRequest, StubResponse, } from "./connection.js";
10
10
  export type { EntityPermissions, JsonSchemaObject, MemberOptions, OntologyConfig, ResolvedAction, ResolvedConnection, ResolvedEntity, ResolvedIndex, ResolvedProperty, ResolvedRelationship, } from "./analyze.js";
@@ -55,10 +55,12 @@ export type { ConnectionAudit, ConnectionTransportFactory, LocalConnection, Loca
55
55
  export { configFromEnv, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, } from "./service/config.js";
56
56
  export type { ActionCommitted, AfterCommitHook, NeonAuthEnvironment, OntologyEnvironment, OntologyServiceConfig, } from "./service/config.js";
57
57
  export { NotInvited, OntologyBackend } from "./service/runtime-contract.js";
58
- export type { GeneratedActionsProvider, IdentityProvider, OntologyServiceRuntime, RunActionInput, SignInIdentity, } from "./service/runtime-contract.js";
58
+ export type { GeneratedActionsProvider, IdentityProvider, OAuthClient, OAuthClientAuthMethod, OAuthGrant, OAuthRequest, OAuthStore, OAuthToken, OAuthTokenKind, OntologyServiceRuntime, RunActionInput, SignInIdentity, UsedOAuthToken, } from "./service/runtime-contract.js";
59
59
  export { createOntologyService, ONTOLOGY_HASH_HEADER, REQUEST_ID_HEADER, } from "./service/service.js";
60
+ export { MCP_PATH, MCP_PROTOCOL_VERSIONS } from "./service/mcp/server.js";
61
+ export { MCP_TOOL_NAME_MAX_LENGTH, mcpToolName } from "./names.js";
60
62
  export type { OntologyService } from "./service/service.js";
61
- export { applyMigration, CatalogInvalid, CatalogMismatch, isNoopMigration, migrate, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationFailed, MigrationPlan, MigrationRefusal, MigrationRefused, MigrationStep, OntologyStorage, planMigration, readCatalog, } from "./migrate/index.js";
63
+ export { applyMigration, CatalogInvalid, CatalogMismatch, isNoopMigration, migrate, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationFailed, MigrationPlan, MigrationRefusal, MigrationRefused, MigrationStep, OntologyStorage, planMigration, readCatalog, readRuntimeTables, } from "./migrate/index.js";
62
64
  export type { ApplyMigrationError, ApplyMigrationOptions, CatalogInput, MigrateInput, MigrationOptions, MigrationRefusalCode, MigrationResult, MigrationStepClassification, MigrationStepKind, PlanMigrationInput, } from "./migrate/index.js";
63
65
  export type { BlobStorageBinding } from "./storage/provider.js";
64
66
  export { runBlobStorageMaintenance } from "./storage/operator.js";
package/dist/index.js CHANGED
@@ -34,6 +34,9 @@ export { deployedConnections, LocalConnectionContextError, localConnections, OID
34
34
  export { configFromEnv, ENVIRONMENT_VARIABLES, NEON_AUTH_ENVIRONMENT_VARIABLES, OntologyServiceConfigError, } from "./service/config.js";
35
35
  export { NotInvited, OntologyBackend } from "./service/runtime-contract.js";
36
36
  export { createOntologyService, ONTOLOGY_HASH_HEADER, REQUEST_ID_HEADER, } from "./service/service.js";
37
+ // The MCP server the service serves beside the HTTP API.
38
+ export { MCP_PATH, MCP_PROTOCOL_VERSIONS } from "./service/mcp/server.js";
39
+ export { MCP_TOOL_NAME_MAX_LENGTH, mcpToolName } from "./names.js";
37
40
  // Migrations: the planner, the applier and the physical mapping they share with the runtime.
38
- export { applyMigration, CatalogInvalid, CatalogMismatch, isNoopMigration, migrate, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationFailed, MigrationPlan, MigrationRefusal, MigrationRefused, MigrationStep, OntologyStorage, planMigration, readCatalog, } from "./migrate/index.js";
41
+ export { applyMigration, CatalogInvalid, CatalogMismatch, isNoopMigration, migrate, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationFailed, MigrationPlan, MigrationRefusal, MigrationRefused, MigrationStep, OntologyStorage, planMigration, readCatalog, readRuntimeTables, } from "./migrate/index.js";
39
42
  export { runBlobStorageMaintenance } from "./storage/operator.js";
@@ -85,6 +85,14 @@ export declare const ActionManifest: Schema.Struct<{
85
85
  * when there are some; absent reads as none.
86
86
  */
87
87
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
88
+ /**
89
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
90
+ * declares hints; absent reads as destructive and not idempotent.
91
+ */
92
+ readonly hints: Schema.optionalKey<Schema.Struct<{
93
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
94
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
95
+ }>>;
88
96
  }>;
89
97
  export type ActionManifest = typeof ActionManifest.Type;
90
98
  /**
@@ -186,6 +194,14 @@ export declare const EntityManifest: Schema.Struct<{
186
194
  * when there are some; absent reads as none.
187
195
  */
188
196
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
197
+ /**
198
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
199
+ * declares hints; absent reads as destructive and not idempotent.
200
+ */
201
+ readonly hints: Schema.optionalKey<Schema.Struct<{
202
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
203
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
204
+ }>>;
189
205
  }>>;
190
206
  /** Always written; absent from version 1 manifests. */
191
207
  readonly permissions: Schema.optionalKey<Schema.Struct<{
@@ -318,6 +334,14 @@ export declare const OntologyManifestWire: Schema.encodeKeys<Schema.Struct<{
318
334
  * when there are some; absent reads as none.
319
335
  */
320
336
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
337
+ /**
338
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
339
+ * declares hints; absent reads as destructive and not idempotent.
340
+ */
341
+ readonly hints: Schema.optionalKey<Schema.Struct<{
342
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
343
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
344
+ }>>;
321
345
  }>>;
322
346
  /** Always written; absent from version 1 manifests. */
323
347
  readonly permissions: Schema.optionalKey<Schema.Struct<{
@@ -453,6 +477,14 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
453
477
  * when there are some; absent reads as none.
454
478
  */
455
479
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
480
+ /**
481
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
482
+ * declares hints; absent reads as destructive and not idempotent.
483
+ */
484
+ readonly hints: Schema.optionalKey<Schema.Struct<{
485
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
486
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
487
+ }>>;
456
488
  }>>;
457
489
  /** Always written; absent from version 1 manifests. */
458
490
  readonly permissions: Schema.optionalKey<Schema.Struct<{
@@ -582,6 +614,14 @@ export declare const OntologyManifest: Schema.Union<readonly [Schema.encodeKeys<
582
614
  * when there are some; absent reads as none.
583
615
  */
584
616
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
617
+ /**
618
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
619
+ * declares hints; absent reads as destructive and not idempotent.
620
+ */
621
+ readonly hints: Schema.optionalKey<Schema.Struct<{
622
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
623
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
624
+ }>>;
585
625
  }>>;
586
626
  /** Always written; absent from version 1 manifests. */
587
627
  readonly permissions: Schema.optionalKey<Schema.Struct<{
@@ -629,6 +669,10 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
629
669
  };
630
670
  readonly permission?: string | undefined;
631
671
  readonly connections?: readonly string[] | undefined;
672
+ readonly hints?: {
673
+ readonly destructive?: boolean | undefined;
674
+ readonly idempotent?: boolean | undefined;
675
+ } | undefined;
632
676
  }[];
633
677
  readonly title: string;
634
678
  readonly properties: readonly {
@@ -729,6 +773,10 @@ export declare const decodeManifestDocument: (value: unknown) => Effect.Effect<{
729
773
  };
730
774
  readonly permission?: string | undefined;
731
775
  readonly connections?: readonly string[] | undefined;
776
+ readonly hints?: {
777
+ readonly destructive?: boolean | undefined;
778
+ readonly idempotent?: boolean | undefined;
779
+ } | undefined;
732
780
  }[];
733
781
  readonly title: string;
734
782
  readonly properties: readonly {
package/dist/manifest.js CHANGED
@@ -79,6 +79,14 @@ export const ActionManifest = Schema.Struct({
79
79
  * when there are some; absent reads as none.
80
80
  */
81
81
  connections: Schema.optionalKey(Schema.Array(Schema.String)),
82
+ /**
83
+ * How the action behaves, as MCP tool annotations tell agents. Written only when the action
84
+ * declares hints; absent reads as destructive and not idempotent.
85
+ */
86
+ hints: Schema.optionalKey(Schema.Struct({
87
+ destructive: Schema.optionalKey(Schema.Boolean),
88
+ idempotent: Schema.optionalKey(Schema.Boolean),
89
+ })),
82
90
  });
83
91
  /**
84
92
  * The permissions of an entity's reads and generated actions, so that readers need not rebuild the
@@ -352,6 +360,7 @@ function entityManifest(resolved) {
352
360
  permission: action.permission,
353
361
  input: action.input ?? {},
354
362
  ...(action.connections.length > 0 ? { connections: [...action.connections] } : {}),
363
+ ...(action.hints !== undefined ? { hints: { ...action.hints } } : {}),
355
364
  })),
356
365
  permissions: { ...resolved.permissions },
357
366
  };
@@ -54,6 +54,13 @@ export declare const applyMigration: (plan: MigrationPlan, options: ApplyMigrati
54
54
  * workspace database first as a dry run, then on production: the plan is made against each database's
55
55
  * own catalog.
56
56
  */
57
+ /** The runtime's tables, beside the catalog and the entity tables. */
58
+ export declare const RUNTIME_TABLES: ReadonlyArray<string>;
59
+ /**
60
+ * The runtime tables the database has, for `planMigration`'s `existingTables`: a database that an
61
+ * earlier SDK migrated lacks the tables added since.
62
+ */
63
+ export declare const readRuntimeTables: (input: CatalogInput) => Effect.Effect<ReadonlySet<string>, SqlError>;
57
64
  export declare const migrate: (input: MigrateInput) => Effect.Effect<{
58
65
  readonly plan: MigrationPlan;
59
66
  readonly result: MigrationResult;
@@ -3,7 +3,7 @@ import { canonicalJson, decodeManifestDocument, encodeManifest, manifestHash, }
3
3
  import { CatalogInvalid, CatalogMismatch, MigrationFailed, MigrationRefused } from "./errors.js";
4
4
  import { isNoopMigration } from "./model.js";
5
5
  import { planMigration } from "./plan.js";
6
- import { CATALOG_TABLE, quoteIdentifier } from "./storage.js";
6
+ import { API_KEYS_TABLE, CATALOG_TABLE, OAUTH_TABLES, USER_ROLES_TABLE, quoteIdentifier, quoteLiteral, } from "./storage.js";
7
7
  const CATALOG = quoteIdentifier(CATALOG_TABLE);
8
8
  /** Serializes migrations of one database; transaction-scoped, so it ends with the transaction. */
9
9
  const MIGRATION_LOCK = "segmentstream.ontology.migration";
@@ -133,11 +133,28 @@ export const applyMigration = (plan, options) => Effect.gen(function* () {
133
133
  * workspace database first as a dry run, then on production: the plan is made against each database's
134
134
  * own catalog.
135
135
  */
136
+ /** The runtime's tables, beside the catalog and the entity tables. */
137
+ export const RUNTIME_TABLES = [
138
+ USER_ROLES_TABLE,
139
+ API_KEYS_TABLE,
140
+ ...OAUTH_TABLES,
141
+ ];
142
+ /**
143
+ * The runtime tables the database has, for `planMigration`'s `existingTables`: a database that an
144
+ * earlier SDK migrated lacks the tables added since.
145
+ */
146
+ export const readRuntimeTables = (input) => input.sql
147
+ .unsafe(`SELECT "tablename" AS "name" FROM "pg_catalog"."pg_tables" ` +
148
+ `WHERE "schemaname" = current_schema() ` +
149
+ `AND "tablename" IN (${RUNTIME_TABLES.map(quoteLiteral).join(", ")})`)
150
+ .pipe(Effect.map((rows) => new Set(rows.map((row) => row.name))));
136
151
  export const migrate = (input) => Effect.gen(function* () {
137
152
  const current = yield* readCatalog({ sql: input.sql });
153
+ const existingTables = yield* readRuntimeTables({ sql: input.sql });
138
154
  const plan = planMigration({
139
155
  current,
140
156
  target: input.target,
157
+ existingTables,
141
158
  ...(input.options === undefined ? {} : { options: input.options }),
142
159
  });
143
160
  const result = yield* applyMigration(plan, input);
@@ -1,4 +1,4 @@
1
- export { applyMigration, migrate, readCatalog } from "./apply.js";
1
+ export { applyMigration, migrate, readCatalog, readRuntimeTables, RUNTIME_TABLES, } from "./apply.js";
2
2
  export type { ApplyMigrationError, ApplyMigrationOptions, CatalogInput, MigrateInput, MigrationResult, } from "./apply.js";
3
3
  export { CatalogInvalid, CatalogMismatch, MigrationFailed, MigrationRefused } from "./errors.js";
4
4
  export { isNoopMigration, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationPlan, MigrationRefusal, MigrationStep, } from "./model.js";
@@ -1,4 +1,4 @@
1
- export { applyMigration, migrate, readCatalog } from "./apply.js";
1
+ export { applyMigration, migrate, readCatalog, readRuntimeTables, RUNTIME_TABLES, } from "./apply.js";
2
2
  export { CatalogInvalid, CatalogMismatch, MigrationFailed, MigrationRefused } from "./errors.js";
3
3
  export { isNoopMigration, MIGRATION_REFUSAL_CODES, MIGRATION_STEP_CLASSIFICATIONS, MIGRATION_STEP_KINDS, MigrationPlan, MigrationRefusal, MigrationStep, } from "./model.js";
4
4
  export { planMigration } from "./plan.js";
@@ -123,6 +123,10 @@ export declare const MigrationPlan: Schema.Struct<{
123
123
  readonly permission: Schema.optionalKey<Schema.String>;
124
124
  readonly input: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
125
125
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
126
+ readonly hints: Schema.optionalKey<Schema.Struct<{
127
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
128
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
129
+ }>>;
126
130
  }>>;
127
131
  readonly permissions: Schema.optionalKey<Schema.Struct<{
128
132
  readonly read: Schema.String;
@@ -230,6 +234,10 @@ export declare const MigrationPlan: Schema.Struct<{
230
234
  readonly permission: Schema.optionalKey<Schema.String>;
231
235
  readonly input: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
232
236
  readonly connections: Schema.optionalKey<Schema.$Array<Schema.String>>;
237
+ readonly hints: Schema.optionalKey<Schema.Struct<{
238
+ readonly destructive: Schema.optionalKey<Schema.Boolean>;
239
+ readonly idempotent: Schema.optionalKey<Schema.Boolean>;
240
+ }>>;
233
241
  }>>;
234
242
  readonly permissions: Schema.optionalKey<Schema.Struct<{
235
243
  readonly read: Schema.String;
@@ -12,6 +12,12 @@ export interface PlanMigrationInput {
12
12
  readonly current: OntologyManifest | undefined;
13
13
  readonly target: OntologyManifest;
14
14
  readonly options?: MigrationOptions;
15
+ /**
16
+ * The runtime tables the database has, from `readRuntimeTables`. A database migrated by an earlier
17
+ * SDK lacks the tables added since, and the plan creates them. Without it, the plan assumes the
18
+ * database has every runtime table.
19
+ */
20
+ readonly existingTables?: ReadonlySet<string>;
15
21
  }
16
22
  /**
17
23
  * Plans the migration from the applied manifest to the target. It is pure: the same manifests always
@@ -1,6 +1,6 @@
1
1
  import { BLOB_TABLES_DDL } from "../storage/tables.js";
2
2
  import { SNAKE_CASE_NAME, isSystemProperty, toSnakeCase } from "../names.js";
3
- import { CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, USER_TABLE, addConstraintStatement, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, enumTypeName, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, tableName, } from "./storage.js";
3
+ import { CATALOG_TABLE_DDL, MAX_IDENTIFIER_LENGTH, OAUTH_TABLES, OAUTH_TABLES_DDL, USER_TABLE, addConstraintStatement, columnDefinition, constraintDefinition, createEnumStatement, createIndexStatement, createTableStatement, defaultExpression, entityStorage, enumTypeName, fieldColumnName, joinTable, propertyCheck, blobForeignKey, propertyColumn, propertyEnum, quoteIdentifier, quoteLiteral, relationshipColumn, RUNTIME_TABLES_DDL, relationshipForeignKey, SYSTEM_COLUMNS, tableName, } from "./storage.js";
4
4
  const GLOBAL_KINDS = new Set(["table", "type", "index"]);
5
5
  function step(input) {
6
6
  return {
@@ -69,10 +69,18 @@ export function planMigration(input) {
69
69
  draft.runtimeTables.push(step({
70
70
  kind: "createCatalog",
71
71
  classification: "additive",
72
- description: "Create the runtime's role assignment and API key tables.",
72
+ description: "Create the runtime's role assignment, API key and OAuth tables.",
73
73
  sql: RUNTIME_TABLES_DDL,
74
74
  }));
75
75
  }
76
+ else if (OAUTH_TABLES.some((table) => input.existingTables?.has(table) === false)) {
77
+ draft.runtimeTables.push(step({
78
+ kind: "createCatalog",
79
+ classification: "additive",
80
+ description: "Create the runtime's OAuth tables, which connect agents to the MCP server.",
81
+ sql: OAUTH_TABLES_DDL,
82
+ }));
83
+ }
76
84
  if (storedTargets.some((entity) => entity.properties.some((property) => property.type === "blob")) &&
77
85
  !current?.entities.some((entity) => entity.properties.some((property) => property.type === "blob"))) {
78
86
  draft.runtimeTables.push(step({
@@ -151,6 +151,18 @@ export declare const USER_ROLES_TABLE = "_user_roles";
151
151
  * secret only as a salted PBKDF2 hash.
152
152
  */
153
153
  export declare const API_KEYS_TABLE = "_api_keys";
154
+ /** OAuth clients that registered themselves to connect agents to the MCP server. */
155
+ export declare const OAUTH_CLIENTS_TABLE = "_oauth_clients";
156
+ /** Authorization requests waiting for a member's consent, for a few minutes. */
157
+ export declare const OAUTH_REQUESTS_TABLE = "_oauth_requests";
158
+ /** What a member allowed a client to do: revoking a grant stops its tokens. */
159
+ export declare const OAUTH_GRANTS_TABLE = "_oauth_grants";
160
+ /** Authorization codes, access tokens and refresh tokens, stored only as SHA-256 hashes. */
161
+ export declare const OAUTH_TOKENS_TABLE = "_oauth_tokens";
162
+ /** The OAuth tables, in the order they are created. */
163
+ export declare const OAUTH_TABLES: ReadonlyArray<string>;
164
+ /** The OAuth tables of the MCP server's authorization server. Every statement is idempotent. */
165
+ export declare const OAUTH_TABLES_DDL: ReadonlyArray<string>;
154
166
  /**
155
167
  * The runtime's own tables beside the catalog. They are created with it, after the entity tables
156
168
  * because they refer to the built-in user table, and every statement is idempotent.