@treeseed/sdk 0.13.0-rc.5 → 0.13.0-rc.6
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/dist/entrypoints/clients/control-plane-client.d.ts +62 -0
- package/dist/entrypoints/clients/control-plane-client.js +96 -0
- package/dist/operator-contracts/control-plane-operation.d.ts +60 -0
- package/dist/operator-contracts/control-plane-operation.js +75 -0
- package/dist/operator-contracts/index.d.ts +3 -0
- package/dist/operator-contracts/index.js +3 -0
- package/dist/operator-contracts/mcp.d.ts +78 -0
- package/dist/operator-contracts/mcp.js +23 -0
- package/dist/operator-contracts/oauth.d.ts +38 -0
- package/dist/operator-contracts/oauth.js +0 -0
- package/dist/standards/mcp/compare.d.ts +2 -0
- package/dist/standards/mcp/compare.js +38 -0
- package/dist/standards/mcp/contracts.d.ts +34 -0
- package/dist/standards/mcp/contracts.js +0 -0
- package/dist/standards/mcp/index.d.ts +3 -0
- package/dist/standards/mcp/index.js +3 -0
- package/dist/standards/mcp/normalize.d.ts +4 -0
- package/dist/standards/mcp/normalize.js +30 -0
- package/package.json +9 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ApiPrincipal } from './remote.js';
|
|
2
|
+
export declare const DEFAULT_CONTROL_PLANE_BASE_URL = "http://127.0.0.1:3002";
|
|
3
|
+
export declare const CONTROL_PLANE_BASE_URL_ENV = "TREESEED_API_BASE_URL";
|
|
4
|
+
export interface ControlPlaneServerProfile {
|
|
5
|
+
serverId: string;
|
|
6
|
+
label: string;
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ControlPlaneServerSession {
|
|
10
|
+
serverId: string;
|
|
11
|
+
accessToken: string;
|
|
12
|
+
refreshToken?: string;
|
|
13
|
+
expiresAt?: string;
|
|
14
|
+
principal?: ApiPrincipal | null;
|
|
15
|
+
}
|
|
16
|
+
export interface ControlPlaneClientOptions {
|
|
17
|
+
profile: ControlPlaneServerProfile;
|
|
18
|
+
accessToken?: string | null;
|
|
19
|
+
fetchImpl?: typeof fetch;
|
|
20
|
+
userAgent?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ControlPlaneResponseEnvelope<T> {
|
|
23
|
+
data: T;
|
|
24
|
+
meta?: Record<string, unknown>;
|
|
25
|
+
links?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
export interface ProblemDetails {
|
|
28
|
+
type: string;
|
|
29
|
+
title: string;
|
|
30
|
+
status: number;
|
|
31
|
+
detail?: string;
|
|
32
|
+
instance?: string;
|
|
33
|
+
code: string;
|
|
34
|
+
requestId?: string;
|
|
35
|
+
traceId?: string;
|
|
36
|
+
fields?: Record<string, string[]>;
|
|
37
|
+
}
|
|
38
|
+
export interface ControlPlaneCallOptions {
|
|
39
|
+
method?: 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
|
|
40
|
+
path: `/v1/${string}` | '/openapi.json';
|
|
41
|
+
input?: unknown;
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
idempotencyKey?: string;
|
|
44
|
+
ifMatch?: string;
|
|
45
|
+
signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
export declare class ControlPlaneClientError extends Error {
|
|
48
|
+
readonly status: number;
|
|
49
|
+
readonly problem: ProblemDetails;
|
|
50
|
+
readonly responseHeaders: Headers;
|
|
51
|
+
constructor(message: string, status: number, problem: ProblemDetails, responseHeaders: Headers);
|
|
52
|
+
}
|
|
53
|
+
export declare function defaultLocalControlPlaneServer(env?: Record<string, string | undefined>): ControlPlaneServerProfile;
|
|
54
|
+
export declare class ControlPlaneClient {
|
|
55
|
+
readonly options: ControlPlaneClientOptions;
|
|
56
|
+
readonly baseUrl: string;
|
|
57
|
+
readonly accessToken: string | null;
|
|
58
|
+
readonly fetchImpl: typeof fetch;
|
|
59
|
+
readonly userAgent?: string;
|
|
60
|
+
constructor(options: ControlPlaneClientOptions);
|
|
61
|
+
call<T>(options: ControlPlaneCallOptions): Promise<ControlPlaneResponseEnvelope<T>>;
|
|
62
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const DEFAULT_CONTROL_PLANE_BASE_URL = "http://127.0.0.1:3002";
|
|
2
|
+
const CONTROL_PLANE_BASE_URL_ENV = "TREESEED_API_BASE_URL";
|
|
3
|
+
class ControlPlaneClientError extends Error {
|
|
4
|
+
constructor(message, status, problem, responseHeaders) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.problem = problem;
|
|
8
|
+
this.responseHeaders = responseHeaders;
|
|
9
|
+
this.name = "ControlPlaneClientError";
|
|
10
|
+
}
|
|
11
|
+
status;
|
|
12
|
+
problem;
|
|
13
|
+
responseHeaders;
|
|
14
|
+
}
|
|
15
|
+
function normalizeBaseUrl(value) {
|
|
16
|
+
const normalized = value.trim().replace(/\/+$/u, "");
|
|
17
|
+
if (!/^https?:\/\//u.test(normalized)) throw new Error("Control-plane server URLs must use HTTP or HTTPS.");
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
function defaultLocalControlPlaneServer(env = process.env) {
|
|
21
|
+
return {
|
|
22
|
+
serverId: "local",
|
|
23
|
+
label: "Local TreeSeed control plane",
|
|
24
|
+
baseUrl: normalizeBaseUrl(env[CONTROL_PLANE_BASE_URL_ENV] ?? DEFAULT_CONTROL_PLANE_BASE_URL)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
async function responsePayload(response) {
|
|
28
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
29
|
+
if (contentType.includes("json")) return response.json();
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
return text.length > 0 ? text : null;
|
|
32
|
+
}
|
|
33
|
+
function problemFrom(payload, status) {
|
|
34
|
+
const source = payload && typeof payload === "object" ? payload : {};
|
|
35
|
+
return {
|
|
36
|
+
type: typeof source.type === "string" ? source.type : "about:blank",
|
|
37
|
+
title: typeof source.title === "string" ? source.title : "Control-plane request failed",
|
|
38
|
+
status,
|
|
39
|
+
detail: typeof source.detail === "string" ? source.detail : void 0,
|
|
40
|
+
instance: typeof source.instance === "string" ? source.instance : void 0,
|
|
41
|
+
code: typeof source.code === "string" ? source.code : "control_plane_request_failed",
|
|
42
|
+
requestId: typeof source.requestId === "string" ? source.requestId : void 0,
|
|
43
|
+
traceId: typeof source.traceId === "string" ? source.traceId : void 0,
|
|
44
|
+
fields: source.fields && typeof source.fields === "object" ? source.fields : void 0
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
class ControlPlaneClient {
|
|
48
|
+
constructor(options) {
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.baseUrl = normalizeBaseUrl(options.profile.baseUrl);
|
|
51
|
+
this.accessToken = options.accessToken ?? null;
|
|
52
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
53
|
+
this.userAgent = options.userAgent;
|
|
54
|
+
}
|
|
55
|
+
options;
|
|
56
|
+
baseUrl;
|
|
57
|
+
accessToken;
|
|
58
|
+
fetchImpl;
|
|
59
|
+
userAgent;
|
|
60
|
+
async call(options) {
|
|
61
|
+
const headers = new Headers(options.headers);
|
|
62
|
+
headers.set("accept", "application/json, application/problem+json");
|
|
63
|
+
if (this.accessToken) headers.set("authorization", `Bearer ${this.accessToken}`);
|
|
64
|
+
if (this.userAgent) headers.set("user-agent", this.userAgent);
|
|
65
|
+
if (options.idempotencyKey) headers.set("idempotency-key", options.idempotencyKey);
|
|
66
|
+
if (options.ifMatch) headers.set("if-match", options.ifMatch);
|
|
67
|
+
if (options.input !== void 0) headers.set("content-type", "application/json");
|
|
68
|
+
const response = await this.fetchImpl(`${this.baseUrl}${options.path}`, {
|
|
69
|
+
method: options.method ?? "GET",
|
|
70
|
+
headers,
|
|
71
|
+
body: options.input === void 0 ? void 0 : JSON.stringify(options.input),
|
|
72
|
+
signal: options.signal
|
|
73
|
+
});
|
|
74
|
+
const payload = await responsePayload(response);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
const problem = problemFrom(payload, response.status);
|
|
77
|
+
throw new ControlPlaneClientError(problem.detail ?? problem.title, response.status, problem, response.headers);
|
|
78
|
+
}
|
|
79
|
+
if (!payload || typeof payload !== "object" || !("data" in payload)) {
|
|
80
|
+
throw new ControlPlaneClientError("The control plane returned an invalid success envelope.", 502, {
|
|
81
|
+
type: "https://treeseed.dev/problems/invalid-upstream-response",
|
|
82
|
+
title: "Invalid control-plane response",
|
|
83
|
+
status: 502,
|
|
84
|
+
code: "control_plane_response_invalid"
|
|
85
|
+
}, response.headers);
|
|
86
|
+
}
|
|
87
|
+
return payload;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
CONTROL_PLANE_BASE_URL_ENV,
|
|
92
|
+
ControlPlaneClient,
|
|
93
|
+
ControlPlaneClientError,
|
|
94
|
+
DEFAULT_CONTROL_PLANE_BASE_URL,
|
|
95
|
+
defaultLocalControlPlaneServer
|
|
96
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export declare const CONTROL_PLANE_OPERATION_SCHEMA_VERSION: "treeseed.control-plane-operation/v1";
|
|
2
|
+
export type ControlPlaneHttpMethod = 'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT';
|
|
3
|
+
export type ControlPlaneOperationKind = 'read' | 'mutation';
|
|
4
|
+
export type ControlPlaneRiskClass = 'ordinary' | 'destructive' | 'credential' | 'authority' | 'production' | 'irreversible';
|
|
5
|
+
export type ControlPlaneConfirmationPolicy = 'never' | 'input_required';
|
|
6
|
+
export type ControlPlaneOperationSurface = 'rest' | 'cli' | 'mcp_tool' | 'mcp_resource' | 'internal';
|
|
7
|
+
export type ControlPlaneCacheScope = 'none' | 'principal' | 'team' | 'project' | 'public';
|
|
8
|
+
export type ControlPlanePaginationKind = 'none' | 'cursor';
|
|
9
|
+
export interface ControlPlaneRestBinding {
|
|
10
|
+
method: ControlPlaneHttpMethod;
|
|
11
|
+
path: `/v1/${string}`;
|
|
12
|
+
}
|
|
13
|
+
export interface ControlPlaneSchemaBinding {
|
|
14
|
+
input: string;
|
|
15
|
+
output: string;
|
|
16
|
+
parameters?: string;
|
|
17
|
+
errors: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ControlPlaneIdempotencyContract {
|
|
20
|
+
required: boolean;
|
|
21
|
+
header: 'Idempotency-Key';
|
|
22
|
+
}
|
|
23
|
+
export interface ControlPlaneConcurrencyContract {
|
|
24
|
+
required: boolean;
|
|
25
|
+
readHeader: 'ETag';
|
|
26
|
+
writeHeader: 'If-Match';
|
|
27
|
+
}
|
|
28
|
+
export interface ControlPlaneOperationDescriptor {
|
|
29
|
+
schemaVersion: typeof CONTROL_PLANE_OPERATION_SCHEMA_VERSION;
|
|
30
|
+
operationId: `${string}.${string}`;
|
|
31
|
+
description: string;
|
|
32
|
+
rest?: ControlPlaneRestBinding;
|
|
33
|
+
schemas: ControlPlaneSchemaBinding;
|
|
34
|
+
capability: string;
|
|
35
|
+
oauthScopes: OAuthScope[];
|
|
36
|
+
kind: ControlPlaneOperationKind;
|
|
37
|
+
riskClass: ControlPlaneRiskClass;
|
|
38
|
+
confirmation: ControlPlaneConfirmationPolicy;
|
|
39
|
+
idempotency: ControlPlaneIdempotencyContract;
|
|
40
|
+
concurrency: ControlPlaneConcurrencyContract;
|
|
41
|
+
surfaces: ControlPlaneOperationSurface[];
|
|
42
|
+
cacheScope: ControlPlaneCacheScope;
|
|
43
|
+
pagination: ControlPlanePaginationKind;
|
|
44
|
+
audited: boolean;
|
|
45
|
+
receipt: boolean;
|
|
46
|
+
redactedPaths: string[];
|
|
47
|
+
}
|
|
48
|
+
export declare const TREESEED_OAUTH_SCOPES: readonly ["treeseed:read", "treeseed:knowledge:write", "treeseed:governance:write", "treeseed:projects:write", "treeseed:execution", "treeseed:admin"];
|
|
49
|
+
export type OAuthScope = typeof TREESEED_OAUTH_SCOPES[number];
|
|
50
|
+
export interface ControlPlaneCatalog {
|
|
51
|
+
schemaVersion: 'treeseed.control-plane-catalog/v1';
|
|
52
|
+
operations: ControlPlaneOperationDescriptor[];
|
|
53
|
+
}
|
|
54
|
+
export interface ControlPlaneCatalogDiagnostic {
|
|
55
|
+
code: string;
|
|
56
|
+
path: string;
|
|
57
|
+
message: string;
|
|
58
|
+
}
|
|
59
|
+
export declare function validateControlPlaneCatalog(catalog: ControlPlaneCatalog): ControlPlaneCatalogDiagnostic[];
|
|
60
|
+
export declare function indexControlPlaneCatalog(catalog: ControlPlaneCatalog): Map<`${string}.${string}`, ControlPlaneOperationDescriptor>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const CONTROL_PLANE_OPERATION_SCHEMA_VERSION = "treeseed.control-plane-operation/v1";
|
|
2
|
+
const TREESEED_OAUTH_SCOPES = [
|
|
3
|
+
"treeseed:read",
|
|
4
|
+
"treeseed:knowledge:write",
|
|
5
|
+
"treeseed:governance:write",
|
|
6
|
+
"treeseed:projects:write",
|
|
7
|
+
"treeseed:execution",
|
|
8
|
+
"treeseed:admin"
|
|
9
|
+
];
|
|
10
|
+
const OPERATION_ID = /^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+$/u;
|
|
11
|
+
const PATH_PARAMETER = /\{([A-Za-z][A-Za-z0-9]*)\}/gu;
|
|
12
|
+
function duplicates(values) {
|
|
13
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14
|
+
return values.filter((value) => seen.size === seen.add(value).size);
|
|
15
|
+
}
|
|
16
|
+
function validateControlPlaneCatalog(catalog) {
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
19
|
+
const restBindings = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const [index, operation] of catalog.operations.entries()) {
|
|
21
|
+
const path = `operations.${index}`;
|
|
22
|
+
if (operation.schemaVersion !== CONTROL_PLANE_OPERATION_SCHEMA_VERSION) {
|
|
23
|
+
diagnostics.push({ code: "operation_schema_version_invalid", path: `${path}.schemaVersion`, message: "Operation schemaVersion is not supported." });
|
|
24
|
+
}
|
|
25
|
+
if (!OPERATION_ID.test(operation.operationId)) {
|
|
26
|
+
diagnostics.push({ code: "operation_id_invalid", path: `${path}.operationId`, message: "Operation IDs must be stable dotted lowercase words." });
|
|
27
|
+
}
|
|
28
|
+
if (operationIds.has(operation.operationId)) {
|
|
29
|
+
diagnostics.push({ code: "operation_id_duplicate", path: `${path}.operationId`, message: `Duplicate operation ID ${operation.operationId}.` });
|
|
30
|
+
}
|
|
31
|
+
operationIds.add(operation.operationId);
|
|
32
|
+
if (operation.surfaces.includes("rest") !== Boolean(operation.rest)) {
|
|
33
|
+
diagnostics.push({ code: "rest_binding_mismatch", path: `${path}.rest`, message: "REST surface and REST binding must be declared together." });
|
|
34
|
+
}
|
|
35
|
+
if (operation.rest) {
|
|
36
|
+
const binding = `${operation.rest.method} ${operation.rest.path}`;
|
|
37
|
+
if (restBindings.has(binding)) diagnostics.push({ code: "rest_binding_duplicate", path: `${path}.rest`, message: `Duplicate REST binding ${binding}.` });
|
|
38
|
+
restBindings.add(binding);
|
|
39
|
+
const parameters = [...operation.rest.path.matchAll(PATH_PARAMETER)].map((match) => match[1]);
|
|
40
|
+
if (parameters.length > 0 && !operation.schemas.parameters) {
|
|
41
|
+
diagnostics.push({ code: "parameter_schema_required", path: `${path}.schemas.parameters`, message: "Parameterized REST paths require a parameter schema." });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
for (const scope of operation.oauthScopes) {
|
|
45
|
+
if (!TREESEED_OAUTH_SCOPES.includes(scope)) diagnostics.push({ code: "oauth_scope_invalid", path: `${path}.oauthScopes`, message: `Unknown OAuth scope ${scope}.` });
|
|
46
|
+
}
|
|
47
|
+
for (const duplicate of duplicates(operation.surfaces)) diagnostics.push({ code: "surface_duplicate", path: `${path}.surfaces`, message: `Duplicate operation surface ${duplicate}.` });
|
|
48
|
+
for (const duplicate of duplicates(operation.oauthScopes)) diagnostics.push({ code: "oauth_scope_duplicate", path: `${path}.oauthScopes`, message: `Duplicate OAuth scope ${duplicate}.` });
|
|
49
|
+
const elevatedRisk = operation.riskClass !== "ordinary";
|
|
50
|
+
if (elevatedRisk !== (operation.confirmation === "input_required")) {
|
|
51
|
+
diagnostics.push({ code: "confirmation_policy_invalid", path: `${path}.confirmation`, message: "Elevated-risk operations require input_required; ordinary operations must not." });
|
|
52
|
+
}
|
|
53
|
+
if (operation.kind === "read" && (operation.idempotency.required || operation.concurrency.required || operation.receipt)) {
|
|
54
|
+
diagnostics.push({ code: "read_mutation_contract_invalid", path, message: "Read operations cannot require mutation idempotency, write concurrency, or mutation receipts." });
|
|
55
|
+
}
|
|
56
|
+
if (operation.kind === "mutation" && operation.surfaces.some((surface) => surface !== "internal") && !operation.audited) {
|
|
57
|
+
diagnostics.push({ code: "mutation_audit_required", path: `${path}.audited`, message: "Every externally reachable mutation must be audited." });
|
|
58
|
+
}
|
|
59
|
+
if (operation.kind === "mutation" && !operation.receipt) {
|
|
60
|
+
diagnostics.push({ code: "mutation_receipt_required", path: `${path}.receipt`, message: "Mutations must return durable receipts." });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return diagnostics.sort((left, right) => `${left.path}:${left.code}`.localeCompare(`${right.path}:${right.code}`));
|
|
64
|
+
}
|
|
65
|
+
function indexControlPlaneCatalog(catalog) {
|
|
66
|
+
const diagnostics = validateControlPlaneCatalog(catalog);
|
|
67
|
+
if (diagnostics.length > 0) throw new Error(`Invalid control-plane catalog: ${diagnostics.map((entry) => entry.code).join(", ")}`);
|
|
68
|
+
return new Map(catalog.operations.map((operation) => [operation.operationId, operation]));
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
CONTROL_PLANE_OPERATION_SCHEMA_VERSION,
|
|
72
|
+
TREESEED_OAUTH_SCOPES,
|
|
73
|
+
indexControlPlaneCatalog,
|
|
74
|
+
validateControlPlaneCatalog
|
|
75
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ControlPlaneOperationDescriptor, OAuthScope } from './control-plane-operation.js';
|
|
2
|
+
export declare const MCP_PROTOCOL_VERSION: "2026-07-28";
|
|
3
|
+
export interface ResourceLink {
|
|
4
|
+
type: 'resource_link';
|
|
5
|
+
uri: `treeseed://${string}`;
|
|
6
|
+
name: string;
|
|
7
|
+
title?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
mimeType?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ActorChain {
|
|
12
|
+
principalId: string;
|
|
13
|
+
delegatedAgentId?: string;
|
|
14
|
+
oauthClientId: string;
|
|
15
|
+
interface: 'rest' | 'cli' | 'mcp' | 'site_bff' | 'internal';
|
|
16
|
+
conversationId?: string;
|
|
17
|
+
modelClaim?: string;
|
|
18
|
+
skillClaim?: string;
|
|
19
|
+
traceId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ConfirmationState {
|
|
22
|
+
schemaVersion: 'treeseed.confirmation-state/v1';
|
|
23
|
+
principalId: string;
|
|
24
|
+
clientId: string;
|
|
25
|
+
operationId: string;
|
|
26
|
+
argumentsDigest: `sha256:${string}`;
|
|
27
|
+
expiresAt: string;
|
|
28
|
+
nonce: string;
|
|
29
|
+
signature: string;
|
|
30
|
+
}
|
|
31
|
+
export interface InputRequired {
|
|
32
|
+
type: 'input_required';
|
|
33
|
+
requestId: string;
|
|
34
|
+
prompt: string;
|
|
35
|
+
confirmation: ConfirmationState;
|
|
36
|
+
}
|
|
37
|
+
export interface McpToolDescriptor {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
inputSchemaId: string;
|
|
41
|
+
outputSchemaId: string;
|
|
42
|
+
operationId: string;
|
|
43
|
+
readOnlyHint: boolean;
|
|
44
|
+
destructiveHint: boolean;
|
|
45
|
+
idempotentHint: boolean;
|
|
46
|
+
openWorldHint: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface McpResourceDescriptor {
|
|
49
|
+
uriTemplate: `treeseed://${string}`;
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
mimeType: string;
|
|
53
|
+
operationId: string;
|
|
54
|
+
subscribable: boolean;
|
|
55
|
+
cacheTtlSeconds?: number;
|
|
56
|
+
}
|
|
57
|
+
export interface McpPromptDescriptor {
|
|
58
|
+
name: string;
|
|
59
|
+
description: string;
|
|
60
|
+
argumentSchemaId: string;
|
|
61
|
+
requiredScopes: OAuthScope[];
|
|
62
|
+
}
|
|
63
|
+
export interface McpCatalog {
|
|
64
|
+
schemaVersion: 'treeseed.mcp-catalog/v1';
|
|
65
|
+
protocolVersion: typeof MCP_PROTOCOL_VERSION;
|
|
66
|
+
tools: McpToolDescriptor[];
|
|
67
|
+
resources: McpResourceDescriptor[];
|
|
68
|
+
prompts: McpPromptDescriptor[];
|
|
69
|
+
capabilities: {
|
|
70
|
+
completion: true;
|
|
71
|
+
progress: true;
|
|
72
|
+
cancellation: true;
|
|
73
|
+
inputRequired: true;
|
|
74
|
+
resourceSubscriptions: true;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export declare function operationToMcpTool(operation: ControlPlaneOperationDescriptor): McpToolDescriptor | null;
|
|
78
|
+
export declare function buildMcpTools(operations: readonly ControlPlaneOperationDescriptor[]): McpToolDescriptor[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const MCP_PROTOCOL_VERSION = "2026-07-28";
|
|
2
|
+
function operationToMcpTool(operation) {
|
|
3
|
+
if (!operation.surfaces.includes("mcp_tool")) return null;
|
|
4
|
+
return {
|
|
5
|
+
name: operation.operationId,
|
|
6
|
+
description: operation.description,
|
|
7
|
+
inputSchemaId: operation.schemas.input,
|
|
8
|
+
outputSchemaId: operation.schemas.output,
|
|
9
|
+
operationId: operation.operationId,
|
|
10
|
+
readOnlyHint: operation.kind === "read",
|
|
11
|
+
destructiveHint: operation.riskClass !== "ordinary",
|
|
12
|
+
idempotentHint: operation.kind === "read" || operation.idempotency.required,
|
|
13
|
+
openWorldHint: false
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function buildMcpTools(operations) {
|
|
17
|
+
return operations.map(operationToMcpTool).filter((tool) => tool !== null).sort((left, right) => left.name.localeCompare(right.name));
|
|
18
|
+
}
|
|
19
|
+
export {
|
|
20
|
+
MCP_PROTOCOL_VERSION,
|
|
21
|
+
buildMcpTools,
|
|
22
|
+
operationToMcpTool
|
|
23
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { OAuthScope } from './control-plane-operation.js';
|
|
2
|
+
export interface OAuthAuthorizationServerMetadata {
|
|
3
|
+
issuer: string;
|
|
4
|
+
authorization_endpoint: string;
|
|
5
|
+
token_endpoint: string;
|
|
6
|
+
device_authorization_endpoint: string;
|
|
7
|
+
revocation_endpoint: string;
|
|
8
|
+
response_types_supported: ['code'];
|
|
9
|
+
grant_types_supported: ['authorization_code', 'refresh_token', 'urn:ietf:params:oauth:grant-type:device_code'];
|
|
10
|
+
code_challenge_methods_supported: ['S256'];
|
|
11
|
+
scopes_supported: OAuthScope[];
|
|
12
|
+
}
|
|
13
|
+
export interface OAuthProtectedResourceMetadata {
|
|
14
|
+
resource: string;
|
|
15
|
+
authorization_servers: string[];
|
|
16
|
+
scopes_supported: OAuthScope[];
|
|
17
|
+
bearer_methods_supported: ['header'];
|
|
18
|
+
}
|
|
19
|
+
export interface OAuthDeviceAuthorizationRequest {
|
|
20
|
+
clientId: string;
|
|
21
|
+
scope: OAuthScope[];
|
|
22
|
+
}
|
|
23
|
+
export interface OAuthDeviceAuthorizationResponse {
|
|
24
|
+
deviceCode: string;
|
|
25
|
+
userCode: string;
|
|
26
|
+
verificationUri: string;
|
|
27
|
+
verificationUriComplete: string;
|
|
28
|
+
expiresIn: number;
|
|
29
|
+
interval: number;
|
|
30
|
+
}
|
|
31
|
+
export interface OAuthTokenReceipt {
|
|
32
|
+
tokenType: 'Bearer';
|
|
33
|
+
accessToken: string;
|
|
34
|
+
expiresIn: number;
|
|
35
|
+
refreshToken?: string;
|
|
36
|
+
scope: OAuthScope[];
|
|
37
|
+
audience: string;
|
|
38
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const rank = { unchanged: 0, compatible_addition: 1, breaking: 2 };
|
|
2
|
+
function same(left, right) {
|
|
3
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
4
|
+
}
|
|
5
|
+
function compareMcp(baseline, candidate) {
|
|
6
|
+
const findings = [];
|
|
7
|
+
const add = (code, path, message, classification) => findings.push({ code, path, message, classification });
|
|
8
|
+
if (baseline.protocolVersion !== candidate.protocolVersion) add("mcp_protocol_changed", "protocolVersion", "The MCP protocol version changed.", "breaking");
|
|
9
|
+
for (const [name, tool] of Object.entries(baseline.tools)) {
|
|
10
|
+
const next = candidate.tools[name];
|
|
11
|
+
if (!next) {
|
|
12
|
+
add("mcp_tool_removed", `tools.${name}`, "An MCP tool was removed.", "breaking");
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!same(tool.inputSchema, next.inputSchema)) add("mcp_tool_input_changed", `tools.${name}.inputSchema`, "Tool input changed.", "breaking");
|
|
16
|
+
if (!same(tool.outputSchema, next.outputSchema)) add("mcp_tool_output_changed", `tools.${name}.outputSchema`, "Tool output changed.", "breaking");
|
|
17
|
+
if (next.requiredScopes.some((scope) => !tool.requiredScopes.includes(scope))) add("mcp_tool_scope_escalated", `tools.${name}.requiredScopes`, "Tool scope requirements increased.", "breaking");
|
|
18
|
+
if (tool.riskClass !== next.riskClass) add("mcp_tool_risk_changed", `tools.${name}.riskClass`, "Tool risk classification changed.", "breaking");
|
|
19
|
+
}
|
|
20
|
+
for (const name of Object.keys(candidate.tools).filter((name2) => !(name2 in baseline.tools))) add("mcp_tool_added", `tools.${name}`, "An MCP tool was added.", "compatible_addition");
|
|
21
|
+
for (const [uri, resource] of Object.entries(baseline.resources)) {
|
|
22
|
+
const next = candidate.resources[uri];
|
|
23
|
+
if (!next) add("mcp_resource_removed", `resources.${uri}`, "An MCP resource was removed.", "breaking");
|
|
24
|
+
else if (!same(resource, next)) add("mcp_resource_changed", `resources.${uri}`, "An MCP resource contract changed.", "breaking");
|
|
25
|
+
}
|
|
26
|
+
for (const uri of Object.keys(candidate.resources).filter((uri2) => !(uri2 in baseline.resources))) add("mcp_resource_added", `resources.${uri}`, "An MCP resource was added.", "compatible_addition");
|
|
27
|
+
for (const [name, prompt] of Object.entries(baseline.prompts)) {
|
|
28
|
+
const next = candidate.prompts[name];
|
|
29
|
+
if (!next) add("mcp_prompt_removed", `prompts.${name}`, "An MCP prompt was removed.", "breaking");
|
|
30
|
+
else if (!same(prompt, next)) add("mcp_prompt_changed", `prompts.${name}`, "An MCP prompt contract changed.", "breaking");
|
|
31
|
+
}
|
|
32
|
+
for (const name of Object.keys(candidate.prompts).filter((name2) => !(name2 in baseline.prompts))) add("mcp_prompt_added", `prompts.${name}`, "An MCP prompt was added.", "compatible_addition");
|
|
33
|
+
findings.sort((left, right) => `${left.path}:${left.code}`.localeCompare(`${right.path}:${right.code}`));
|
|
34
|
+
return { classification: findings.reduce((value, finding) => rank[finding.classification] > rank[value] ? finding.classification : value, "unchanged"), findings };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
compareMcp
|
|
38
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CompatibilityClassification } from '../contracts.js';
|
|
2
|
+
import type { OAuthScope } from '../../operator-contracts/control-plane-operation.js';
|
|
3
|
+
export interface McpNormalizedTool {
|
|
4
|
+
inputSchema: unknown;
|
|
5
|
+
outputSchema: unknown;
|
|
6
|
+
requiredScopes: OAuthScope[];
|
|
7
|
+
riskClass: string;
|
|
8
|
+
}
|
|
9
|
+
export interface McpNormalizedResource {
|
|
10
|
+
uriTemplate: string;
|
|
11
|
+
operationId: string;
|
|
12
|
+
subscribable: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface McpNormalizedPrompt {
|
|
15
|
+
argumentSchema: unknown;
|
|
16
|
+
requiredScopes: OAuthScope[];
|
|
17
|
+
}
|
|
18
|
+
export interface McpContractModel {
|
|
19
|
+
schemaVersion: 1;
|
|
20
|
+
protocolVersion: string;
|
|
21
|
+
tools: Record<string, McpNormalizedTool>;
|
|
22
|
+
resources: Record<string, McpNormalizedResource>;
|
|
23
|
+
prompts: Record<string, McpNormalizedPrompt>;
|
|
24
|
+
}
|
|
25
|
+
export interface McpCompatibilityFinding {
|
|
26
|
+
code: string;
|
|
27
|
+
path: string;
|
|
28
|
+
message: string;
|
|
29
|
+
classification: CompatibilityClassification;
|
|
30
|
+
}
|
|
31
|
+
export interface McpCompatibilityComparison {
|
|
32
|
+
classification: CompatibilityClassification;
|
|
33
|
+
findings: McpCompatibilityFinding[];
|
|
34
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpCatalog } from '../../operator-contracts/mcp.js';
|
|
2
|
+
import type { ControlPlaneOperationDescriptor } from '../../operator-contracts/control-plane-operation.js';
|
|
3
|
+
import type { McpContractModel } from './contracts.js';
|
|
4
|
+
export declare function normalizeMcpCatalog(catalog: McpCatalog, operations: readonly ControlPlaneOperationDescriptor[], schemas: Readonly<Record<string, unknown>>): McpContractModel;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { canonicalizeStandardsValue } from "../canonicalize.js";
|
|
2
|
+
function normalizeMcpCatalog(catalog, operations, schemas) {
|
|
3
|
+
const operationById = new Map(operations.map((operation) => [operation.operationId, operation]));
|
|
4
|
+
return {
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
protocolVersion: catalog.protocolVersion,
|
|
7
|
+
tools: Object.fromEntries([...catalog.tools].sort((left, right) => left.name.localeCompare(right.name)).map((tool) => {
|
|
8
|
+
const operation = operationById.get(tool.operationId);
|
|
9
|
+
if (!operation) throw new Error(`MCP tool ${tool.name} references unknown operation ${tool.operationId}.`);
|
|
10
|
+
return [tool.name, {
|
|
11
|
+
inputSchema: canonicalizeStandardsValue(schemas[tool.inputSchemaId] ?? {}),
|
|
12
|
+
outputSchema: canonicalizeStandardsValue(schemas[tool.outputSchemaId] ?? {}),
|
|
13
|
+
requiredScopes: [...operation.oauthScopes].sort(),
|
|
14
|
+
riskClass: operation.riskClass
|
|
15
|
+
}];
|
|
16
|
+
})),
|
|
17
|
+
resources: Object.fromEntries([...catalog.resources].sort((left, right) => left.uriTemplate.localeCompare(right.uriTemplate)).map((resource) => [resource.uriTemplate, {
|
|
18
|
+
uriTemplate: resource.uriTemplate,
|
|
19
|
+
operationId: resource.operationId,
|
|
20
|
+
subscribable: resource.subscribable
|
|
21
|
+
}])),
|
|
22
|
+
prompts: Object.fromEntries([...catalog.prompts].sort((left, right) => left.name.localeCompare(right.name)).map((prompt) => [prompt.name, {
|
|
23
|
+
argumentSchema: canonicalizeStandardsValue(schemas[prompt.argumentSchemaId] ?? {}),
|
|
24
|
+
requiredScopes: [...prompt.requiredScopes].sort()
|
|
25
|
+
}]))
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
normalizeMcpCatalog
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treeseed/sdk",
|
|
3
|
-
"version": "0.13.0-rc.
|
|
3
|
+
"version": "0.13.0-rc.6",
|
|
4
4
|
"description": "Shared Treeseed SDK for content-backed and D1-backed object models.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -135,6 +135,10 @@
|
|
|
135
135
|
"types": "./dist/standards/openapi/index.d.ts",
|
|
136
136
|
"default": "./dist/standards/openapi/index.js"
|
|
137
137
|
},
|
|
138
|
+
"./standards/mcp": {
|
|
139
|
+
"types": "./dist/standards/mcp/index.d.ts",
|
|
140
|
+
"default": "./dist/standards/mcp/index.js"
|
|
141
|
+
},
|
|
138
142
|
"./operator-contracts": {
|
|
139
143
|
"types": "./dist/operator-contracts/index.d.ts",
|
|
140
144
|
"default": "./dist/operator-contracts/index.js"
|
|
@@ -215,6 +219,10 @@
|
|
|
215
219
|
"types": "./dist/entrypoints/clients/market-client.d.ts",
|
|
216
220
|
"default": "./dist/entrypoints/clients/market-client.js"
|
|
217
221
|
},
|
|
222
|
+
"./control-plane-client": {
|
|
223
|
+
"types": "./dist/entrypoints/clients/control-plane-client.d.ts",
|
|
224
|
+
"default": "./dist/entrypoints/clients/control-plane-client.js"
|
|
225
|
+
},
|
|
218
226
|
"./market-gateway": {
|
|
219
227
|
"types": "./dist/gateway/index.d.ts",
|
|
220
228
|
"default": "./dist/gateway/index.js"
|