@cleocode/lafs-protocol 0.5.0 → 1.0.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 (38) hide show
  1. package/LICENSE +0 -0
  2. package/README.md +0 -0
  3. package/dist/examples/discovery-server.d.ts +8 -0
  4. package/dist/examples/discovery-server.js +216 -0
  5. package/dist/examples/mcp-lafs-client.d.ts +10 -0
  6. package/dist/examples/mcp-lafs-client.js +427 -0
  7. package/dist/examples/mcp-lafs-server.d.ts +10 -0
  8. package/dist/examples/mcp-lafs-server.js +358 -0
  9. package/dist/schemas/v1/envelope.schema.json +0 -0
  10. package/dist/schemas/v1/error-registry.json +0 -0
  11. package/dist/src/budgetEnforcement.d.ts +84 -0
  12. package/dist/src/budgetEnforcement.js +328 -0
  13. package/dist/src/cli.d.ts +0 -0
  14. package/dist/src/cli.js +0 -0
  15. package/dist/src/conformance.d.ts +0 -0
  16. package/dist/src/conformance.js +0 -0
  17. package/dist/src/discovery.d.ts +127 -0
  18. package/dist/src/discovery.js +304 -0
  19. package/dist/src/errorRegistry.d.ts +0 -0
  20. package/dist/src/errorRegistry.js +0 -0
  21. package/dist/src/flagSemantics.d.ts +0 -0
  22. package/dist/src/flagSemantics.js +0 -0
  23. package/dist/src/index.d.ts +4 -0
  24. package/dist/src/index.js +4 -0
  25. package/dist/src/mcpAdapter.d.ts +28 -0
  26. package/dist/src/mcpAdapter.js +281 -0
  27. package/dist/src/tokenEstimator.d.ts +87 -0
  28. package/dist/src/tokenEstimator.js +238 -0
  29. package/dist/src/types.d.ts +25 -0
  30. package/dist/src/types.js +0 -0
  31. package/dist/src/validateEnvelope.d.ts +0 -0
  32. package/dist/src/validateEnvelope.js +0 -0
  33. package/lafs.md +164 -0
  34. package/package.json +8 -3
  35. package/schemas/v1/context-ledger.schema.json +0 -0
  36. package/schemas/v1/discovery.schema.json +132 -0
  37. package/schemas/v1/envelope.schema.json +0 -0
  38. package/schemas/v1/error-registry.json +0 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * LAFS Agent Discovery - Express/Fastify Middleware
3
+ * Serves discovery document at /.well-known/lafs.json
4
+ */
5
+ import { createRequire } from "node:module";
6
+ import { createHash } from "crypto";
7
+ import { readFileSync } from "fs";
8
+ import { fileURLToPath } from "url";
9
+ import { dirname, join } from "path";
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ // Handle ESM/CommonJS interop for AJV
13
+ const require = createRequire(import.meta.url);
14
+ const AjvModule = require("ajv");
15
+ const AddFormatsModule = require("ajv-formats");
16
+ const AjvCtor = (typeof AjvModule === "function" ? AjvModule : AjvModule.default);
17
+ const addFormats = (typeof AddFormatsModule === "function" ? AddFormatsModule : AddFormatsModule.default);
18
+ let ajvInstance = null;
19
+ let validateDiscovery = null;
20
+ /**
21
+ * Initialize AJV validator for discovery documents
22
+ */
23
+ function initValidator() {
24
+ if (ajvInstance && validateDiscovery)
25
+ return;
26
+ ajvInstance = new AjvCtor({ strict: true, allErrors: true });
27
+ addFormats(ajvInstance);
28
+ try {
29
+ // Try to load schema from schemas directory
30
+ const schemaPath = join(__dirname, "..", "..", "schemas", "v1", "discovery.schema.json");
31
+ const schema = JSON.parse(readFileSync(schemaPath, "utf-8"));
32
+ validateDiscovery = ajvInstance.compile(schema);
33
+ }
34
+ catch (e) {
35
+ // Fallback to inline schema if file not found
36
+ const fallbackSchema = {
37
+ $schema: "http://json-schema.org/draft-07/schema#",
38
+ type: "object",
39
+ required: ["$schema", "lafs_version", "service", "capabilities", "endpoints"],
40
+ properties: {
41
+ $schema: { type: "string", format: "uri" },
42
+ lafs_version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
43
+ service: {
44
+ type: "object",
45
+ required: ["name", "version"],
46
+ properties: {
47
+ name: { type: "string", minLength: 1 },
48
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
49
+ description: { type: "string" }
50
+ }
51
+ },
52
+ capabilities: {
53
+ type: "array",
54
+ items: {
55
+ type: "object",
56
+ required: ["name", "version", "operations"],
57
+ properties: {
58
+ name: { type: "string", minLength: 1 },
59
+ version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
60
+ description: { type: "string" },
61
+ operations: { type: "array", items: { type: "string" } },
62
+ optional: { type: "boolean" }
63
+ }
64
+ }
65
+ },
66
+ endpoints: {
67
+ type: "object",
68
+ required: ["envelope", "discovery"],
69
+ properties: {
70
+ envelope: { type: "string", minLength: 1 },
71
+ context: { type: "string", minLength: 1 },
72
+ discovery: { type: "string", minLength: 1 }
73
+ }
74
+ }
75
+ }
76
+ };
77
+ validateDiscovery = ajvInstance.compile(fallbackSchema);
78
+ }
79
+ }
80
+ /**
81
+ * Build absolute URL from base and path
82
+ */
83
+ function buildUrl(base, path, req) {
84
+ // If path is already absolute, return it
85
+ if (path.startsWith("http://") || path.startsWith("https://")) {
86
+ return path;
87
+ }
88
+ // If base is provided, use it
89
+ if (base) {
90
+ const separator = base.endsWith("/") || path.startsWith("/") ? "" : "/";
91
+ return `${base}${separator}${path}`;
92
+ }
93
+ // Otherwise try to construct from request
94
+ if (req) {
95
+ const protocol = req.headers["x-forwarded-proto"] || req.protocol || "http";
96
+ const host = req.headers.host || "localhost";
97
+ const separator = path.startsWith("/") ? "" : "/";
98
+ return `${protocol}://${host}${separator}${path}`;
99
+ }
100
+ // Fallback to relative path
101
+ return path.startsWith("/") ? path : `/${path}`;
102
+ }
103
+ /**
104
+ * Generate ETag from document content
105
+ */
106
+ function generateETag(content) {
107
+ return `"${createHash("sha256").update(content).digest("hex").slice(0, 32)}"`;
108
+ }
109
+ /**
110
+ * Build discovery document from configuration
111
+ */
112
+ function buildDiscoveryDocument(config, req) {
113
+ const schemaUrl = config.schemaUrl || "https://lafs.dev/schemas/v1/discovery.schema.json";
114
+ const lafsVersion = config.lafsVersion || "1.0.0";
115
+ return {
116
+ $schema: schemaUrl,
117
+ lafs_version: lafsVersion,
118
+ service: config.service,
119
+ capabilities: config.capabilities,
120
+ endpoints: {
121
+ envelope: buildUrl(config.baseUrl, config.endpoints.envelope, req),
122
+ context: config.endpoints.context
123
+ ? buildUrl(config.baseUrl, config.endpoints.context, req)
124
+ : undefined,
125
+ discovery: config.endpoints.discovery
126
+ ? buildUrl(config.baseUrl, config.endpoints.discovery, req)
127
+ : buildUrl(config.baseUrl, "/.well-known/lafs.json", req)
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Validate discovery document against schema
133
+ */
134
+ function validateDocument(doc) {
135
+ initValidator();
136
+ if (!validateDiscovery) {
137
+ throw new Error("Discovery document validator not initialized");
138
+ }
139
+ const valid = validateDiscovery(doc);
140
+ if (!valid) {
141
+ const errors = validateDiscovery.errors;
142
+ const errorMessages = errors?.map((e) => `${e.instancePath || "root"}: ${e.message}`).join("; ");
143
+ throw new Error(`Discovery document validation failed: ${errorMessages}`);
144
+ }
145
+ }
146
+ /**
147
+ * Create Express middleware for serving LAFS discovery document
148
+ *
149
+ * @param config - Discovery configuration
150
+ * @param options - Middleware options
151
+ * @returns Express RequestHandler
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * import express from "express";
156
+ * import { discoveryMiddleware } from "./discovery.js";
157
+ *
158
+ * const app = express();
159
+ *
160
+ * app.use(discoveryMiddleware({
161
+ * service: {
162
+ * name: "my-lafs-service",
163
+ * version: "1.0.0",
164
+ * description: "A LAFS-compliant API service"
165
+ * },
166
+ * capabilities: [
167
+ * {
168
+ * name: "envelope-processor",
169
+ * version: "1.0.0",
170
+ * operations: ["process", "validate"],
171
+ * description: "Process LAFS envelopes"
172
+ * }
173
+ * ],
174
+ * endpoints: {
175
+ * envelope: "/api/v1/envelope",
176
+ * context: "/api/v1/context"
177
+ * }
178
+ * }));
179
+ * ```
180
+ */
181
+ export function discoveryMiddleware(config, options = {}) {
182
+ const path = options.path || "/.well-known/lafs.json";
183
+ const enableHead = options.enableHead !== false;
184
+ const enableEtag = options.enableEtag !== false;
185
+ const cacheMaxAge = config.cacheMaxAge || 3600;
186
+ // Validate configuration
187
+ if (!config.service?.name || !config.service?.version) {
188
+ throw new Error("Discovery config requires service.name and service.version");
189
+ }
190
+ if (!Array.isArray(config.capabilities)) {
191
+ throw new Error("Discovery config requires capabilities array");
192
+ }
193
+ if (!config.endpoints?.envelope) {
194
+ throw new Error("Discovery config requires endpoints.envelope");
195
+ }
196
+ return function discoveryHandler(req, res, next) {
197
+ // Only handle requests to the discovery path
198
+ if (req.path !== path) {
199
+ next();
200
+ return;
201
+ }
202
+ // Handle HEAD requests
203
+ if (req.method === "HEAD") {
204
+ if (!enableHead) {
205
+ res.status(405).json({
206
+ error: "Method Not Allowed",
207
+ message: "HEAD requests are disabled for this endpoint"
208
+ });
209
+ return;
210
+ }
211
+ // For HEAD, we still need to build the document to get the ETag
212
+ const doc = buildDiscoveryDocument(config, req);
213
+ const json = JSON.stringify(doc);
214
+ // Generate stable ETag from config hash (not request-dependent document)
215
+ const configHash = generateETag(JSON.stringify({
216
+ schemaUrl: config.schemaUrl,
217
+ lafsVersion: config.lafsVersion,
218
+ service: config.service,
219
+ capabilities: config.capabilities,
220
+ endpoints: config.endpoints,
221
+ cacheMaxAge: config.cacheMaxAge
222
+ }));
223
+ const etag = enableEtag ? configHash : undefined;
224
+ res.set({
225
+ "Content-Type": "application/json",
226
+ "Cache-Control": `public, max-age=${cacheMaxAge}`,
227
+ ...(etag && { "ETag": etag }),
228
+ "Content-Length": Buffer.byteLength(json)
229
+ });
230
+ res.status(200).end();
231
+ return;
232
+ }
233
+ // Only handle GET requests
234
+ if (req.method !== "GET") {
235
+ res.status(405).json({
236
+ error: "Method Not Allowed",
237
+ message: `Method ${req.method} not allowed. Use GET or HEAD.`
238
+ });
239
+ return;
240
+ }
241
+ try {
242
+ // Build discovery document
243
+ const doc = buildDiscoveryDocument(config, req);
244
+ // Validate against schema
245
+ validateDocument(doc);
246
+ // Serialize document
247
+ const json = JSON.stringify(doc);
248
+ // Generate ETag from config hash (stable) rather than request-dependent document
249
+ // This ensures ETag is consistent across requests even when URLs are constructed from request
250
+ const configHash = generateETag(JSON.stringify({
251
+ schemaUrl: config.schemaUrl,
252
+ lafsVersion: config.lafsVersion,
253
+ service: config.service,
254
+ capabilities: config.capabilities,
255
+ endpoints: config.endpoints,
256
+ cacheMaxAge: config.cacheMaxAge
257
+ }));
258
+ const etag = enableEtag ? configHash : undefined;
259
+ // Check If-None-Match for conditional request
260
+ if (enableEtag && req.headers["if-none-match"] === etag) {
261
+ res.status(304).end();
262
+ return;
263
+ }
264
+ // Set response headers
265
+ const headers = {
266
+ "Content-Type": "application/json",
267
+ "Cache-Control": `public, max-age=${cacheMaxAge}`,
268
+ ...config.headers
269
+ };
270
+ if (etag) {
271
+ headers["ETag"] = etag;
272
+ }
273
+ res.set(headers);
274
+ res.status(200).send(json);
275
+ }
276
+ catch (error) {
277
+ next(error);
278
+ }
279
+ };
280
+ }
281
+ /**
282
+ * Fastify plugin for LAFS discovery (for Fastify users)
283
+ *
284
+ * @param fastify - Fastify instance
285
+ * @param options - Plugin options
286
+ */
287
+ export async function discoveryFastifyPlugin(fastify, options) {
288
+ const path = options.path || "/.well-known/lafs.json";
289
+ const config = options.config;
290
+ const cacheMaxAge = config.cacheMaxAge || 3600;
291
+ const handler = async (request, reply) => {
292
+ const doc = buildDiscoveryDocument(config, request.raw);
293
+ validateDocument(doc);
294
+ const json = JSON.stringify(doc);
295
+ const etag = generateETag(json);
296
+ reply.header("Content-Type", "application/json");
297
+ reply.header("Cache-Control", `public, max-age=${cacheMaxAge}`);
298
+ reply.header("ETag", etag);
299
+ return doc;
300
+ };
301
+ // Note: Actual route registration depends on Fastify's API
302
+ // This is a type-safe signature for the plugin
303
+ }
304
+ export default discoveryMiddleware;
File without changes
File without changes
File without changes
File without changes
@@ -3,3 +3,7 @@ export * from "./errorRegistry.js";
3
3
  export * from "./validateEnvelope.js";
4
4
  export * from "./flagSemantics.js";
5
5
  export * from "./conformance.js";
6
+ export * from "./tokenEstimator.js";
7
+ export * from "./budgetEnforcement.js";
8
+ export * from "./mcpAdapter.js";
9
+ export * from "./discovery.js";
package/dist/src/index.js CHANGED
@@ -3,3 +3,7 @@ export * from "./errorRegistry.js";
3
3
  export * from "./validateEnvelope.js";
4
4
  export * from "./flagSemantics.js";
5
5
  export * from "./conformance.js";
6
+ export * from "./tokenEstimator.js";
7
+ export * from "./budgetEnforcement.js";
8
+ export * from "./mcpAdapter.js";
9
+ export * from "./discovery.js";
@@ -0,0 +1,28 @@
1
+ import type { CallToolResult, TextContent } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { LAFSEnvelope, LAFSError } from "./types.js";
3
+ /**
4
+ * Wrap MCP tool result in LAFS envelope
5
+ *
6
+ * @param mcpResult - The raw MCP CallToolResult
7
+ * @param operation - The operation name (tool name)
8
+ * @param budget - Optional token budget for response
9
+ * @returns LAFS-compliant envelope
10
+ */
11
+ export declare function wrapMCPResult(mcpResult: CallToolResult, operation: string, budget?: number): LAFSEnvelope;
12
+ /**
13
+ * Create a LAFS error envelope for MCP adapter errors
14
+ *
15
+ * @param message - Error message
16
+ * @param operation - The operation being performed
17
+ * @param category - Error category
18
+ * @returns LAFS error envelope
19
+ */
20
+ export declare function createAdapterErrorEnvelope(message: string, operation: string, category?: LAFSError["category"]): LAFSEnvelope;
21
+ /**
22
+ * Type guard to check if content is TextContent
23
+ */
24
+ export declare function isTextContent(content: unknown): content is TextContent;
25
+ /**
26
+ * Parse MCP text content as JSON if possible
27
+ */
28
+ export declare function parseMCPTextContent(content: TextContent): unknown;
@@ -0,0 +1,281 @@
1
+ import { randomUUID } from "node:crypto";
2
+ /**
3
+ * Extract result from MCP content array
4
+ * Attempts to parse JSON content, falls back to text representation
5
+ */
6
+ function extractResultFromContent(content) {
7
+ if (!content || content.length === 0) {
8
+ return null;
9
+ }
10
+ // Combine all text content
11
+ const textParts = [];
12
+ const otherContent = [];
13
+ for (const item of content) {
14
+ if (item.type === "text" && item.text) {
15
+ textParts.push(item.text);
16
+ }
17
+ else {
18
+ otherContent.push(item);
19
+ }
20
+ }
21
+ // Try to parse as JSON if single text item looks like JSON
22
+ if (textParts.length === 1) {
23
+ const text = textParts[0]?.trim() ?? "";
24
+ if (text.startsWith("{") || text.startsWith("[")) {
25
+ try {
26
+ const parsed = JSON.parse(text);
27
+ if (typeof parsed === "object" && parsed !== null) {
28
+ return parsed;
29
+ }
30
+ }
31
+ catch {
32
+ // Not valid JSON, treat as text
33
+ }
34
+ }
35
+ }
36
+ // Combine into result object
37
+ const result = {};
38
+ if (textParts.length > 0) {
39
+ result.text = textParts.length === 1 ? textParts[0] : textParts.join("\n");
40
+ }
41
+ if (otherContent.length > 0) {
42
+ result.content = otherContent;
43
+ }
44
+ return result;
45
+ }
46
+ /**
47
+ * Estimate token count from content
48
+ * Rough estimation: ~4 characters per token
49
+ */
50
+ function estimateTokens(content) {
51
+ if (!content)
52
+ return 0;
53
+ const jsonString = JSON.stringify(content);
54
+ return Math.ceil(jsonString.length / 4);
55
+ }
56
+ /**
57
+ * Truncate content to fit within budget
58
+ */
59
+ function truncateToBudget(content, budget) {
60
+ if (!content)
61
+ return { result: null, truncated: false, originalEstimate: 0 };
62
+ const originalEstimate = estimateTokens(content);
63
+ if (originalEstimate <= budget) {
64
+ return { result: content, truncated: false, originalEstimate };
65
+ }
66
+ // Calculate truncation ratio
67
+ const ratio = budget / originalEstimate;
68
+ const jsonString = JSON.stringify(content);
69
+ const targetLength = Math.floor(jsonString.length * ratio);
70
+ // Truncate the string and try to make it valid JSON
71
+ let truncated = jsonString.slice(0, targetLength);
72
+ // Close any open structures
73
+ const openBraces = (truncated.match(/\{/g) || []).length - (truncated.match(/\}/g) || []).length;
74
+ const openBrackets = (truncated.match(/\[/g) || []).length - (truncated.match(/\]/g) || []).length;
75
+ truncated += "}".repeat(Math.max(0, openBraces));
76
+ truncated += "]".repeat(Math.max(0, openBrackets));
77
+ try {
78
+ const parsed = JSON.parse(truncated);
79
+ // Add truncation notice
80
+ if (typeof parsed === "object" && parsed !== null) {
81
+ parsed["_truncated"] = true;
82
+ parsed["_originalTokens"] = originalEstimate;
83
+ parsed["_budget"] = budget;
84
+ }
85
+ return { result: parsed, truncated: true, originalEstimate };
86
+ }
87
+ catch {
88
+ // If parsing fails, return minimal result
89
+ return {
90
+ result: {
91
+ _truncated: true,
92
+ _error: "Content truncated due to budget constraints",
93
+ _originalTokens: originalEstimate,
94
+ _budget: budget,
95
+ },
96
+ truncated: true,
97
+ originalEstimate,
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * Convert MCP error to LAFS error format
103
+ */
104
+ function convertMCPErrorToLAFS(mcpResult, operation) {
105
+ const content = mcpResult.content;
106
+ const errorText = content
107
+ .filter((item) => item.type === "text" && typeof item.text === "string")
108
+ .map((item) => item.text)
109
+ .join("\n");
110
+ // Determine error category based on error text
111
+ let category = "INTERNAL";
112
+ let code = "E_MCP_INTERNAL_ERROR";
113
+ let retryable = false;
114
+ let retryAfterMs = null;
115
+ const errorLower = errorText.toLowerCase();
116
+ if (errorLower.includes("not found") || errorLower.includes("doesn't exist") || errorLower.includes("does not exist")) {
117
+ category = "NOT_FOUND";
118
+ code = "E_MCP_NOT_FOUND";
119
+ }
120
+ else if (errorLower.includes("rate limit") || errorLower.includes("too many requests")) {
121
+ category = "RATE_LIMIT";
122
+ code = "E_MCP_RATE_LIMIT";
123
+ retryable = true;
124
+ retryAfterMs = 60000; // 1 minute default
125
+ }
126
+ else if (errorLower.includes("auth") || errorLower.includes("unauthorized") || errorLower.includes("forbidden")) {
127
+ category = "AUTH";
128
+ code = "E_MCP_AUTH_ERROR";
129
+ }
130
+ else if (errorLower.includes("permission") || errorLower.includes("access denied")) {
131
+ category = "PERMISSION";
132
+ code = "E_MCP_PERMISSION_DENIED";
133
+ }
134
+ else if (errorLower.includes("validation") || errorLower.includes("invalid")) {
135
+ category = "VALIDATION";
136
+ code = "E_MCP_VALIDATION_ERROR";
137
+ }
138
+ else if (errorLower.includes("timeout") || errorLower.includes("transient")) {
139
+ category = "TRANSIENT";
140
+ code = "E_MCP_TRANSIENT_ERROR";
141
+ retryable = true;
142
+ retryAfterMs = 5000; // 5 seconds
143
+ }
144
+ return {
145
+ code,
146
+ message: errorText || "MCP tool execution failed",
147
+ category,
148
+ retryable,
149
+ retryAfterMs,
150
+ details: {
151
+ operation,
152
+ mcpError: true,
153
+ contentTypes: content.map((c) => c.type),
154
+ },
155
+ };
156
+ }
157
+ /**
158
+ * Wrap MCP tool result in LAFS envelope
159
+ *
160
+ * @param mcpResult - The raw MCP CallToolResult
161
+ * @param operation - The operation name (tool name)
162
+ * @param budget - Optional token budget for response
163
+ * @returns LAFS-compliant envelope
164
+ */
165
+ export function wrapMCPResult(mcpResult, operation, budget) {
166
+ const requestId = randomUUID();
167
+ const timestamp = new Date().toISOString();
168
+ // Build base meta
169
+ const meta = {
170
+ specVersion: "1.0.0",
171
+ schemaVersion: "1.0.0",
172
+ timestamp,
173
+ operation,
174
+ requestId,
175
+ transport: "sdk",
176
+ strict: true,
177
+ mvi: "standard",
178
+ contextVersion: 1,
179
+ };
180
+ // Handle MCP error
181
+ if (mcpResult.isError) {
182
+ const error = convertMCPErrorToLAFS(mcpResult, operation);
183
+ return {
184
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
185
+ _meta: meta,
186
+ success: false,
187
+ result: null,
188
+ error,
189
+ };
190
+ }
191
+ // Extract result from MCP content
192
+ let result = extractResultFromContent(mcpResult.content);
193
+ let truncated = false;
194
+ let originalEstimate = 0;
195
+ let extensions;
196
+ // Apply budget enforcement if specified
197
+ if (budget !== undefined && budget > 0) {
198
+ const budgetResult = truncateToBudget(result, budget);
199
+ result = budgetResult.result;
200
+ truncated = budgetResult.truncated;
201
+ originalEstimate = budgetResult.originalEstimate;
202
+ // Put token estimate in extensions to comply with strict schema
203
+ extensions = {
204
+ "x-mcp-token-estimate": {
205
+ estimated: truncated ? budget : originalEstimate,
206
+ truncated,
207
+ originalEstimate: truncated ? originalEstimate : undefined,
208
+ },
209
+ };
210
+ }
211
+ return {
212
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
213
+ _meta: meta,
214
+ success: true,
215
+ result,
216
+ error: null,
217
+ _extensions: extensions,
218
+ };
219
+ }
220
+ /**
221
+ * Create a LAFS error envelope for MCP adapter errors
222
+ *
223
+ * @param message - Error message
224
+ * @param operation - The operation being performed
225
+ * @param category - Error category
226
+ * @returns LAFS error envelope
227
+ */
228
+ export function createAdapterErrorEnvelope(message, operation, category = "INTERNAL") {
229
+ const requestId = randomUUID();
230
+ const timestamp = new Date().toISOString();
231
+ const error = {
232
+ code: "E_MCP_ADAPTER_ERROR",
233
+ message,
234
+ category,
235
+ retryable: category === "TRANSIENT" || category === "RATE_LIMIT",
236
+ retryAfterMs: category === "RATE_LIMIT" ? 60000 : category === "TRANSIENT" ? 5000 : null,
237
+ details: {
238
+ operation,
239
+ adapterError: true,
240
+ },
241
+ };
242
+ return {
243
+ $schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
244
+ _meta: {
245
+ specVersion: "1.0.0",
246
+ schemaVersion: "1.0.0",
247
+ timestamp,
248
+ operation,
249
+ requestId,
250
+ transport: "sdk",
251
+ strict: true,
252
+ mvi: "standard",
253
+ contextVersion: 1,
254
+ },
255
+ success: false,
256
+ result: null,
257
+ error,
258
+ };
259
+ }
260
+ /**
261
+ * Type guard to check if content is TextContent
262
+ */
263
+ export function isTextContent(content) {
264
+ return (typeof content === "object" &&
265
+ content !== null &&
266
+ "type" in content &&
267
+ content.type === "text" &&
268
+ "text" in content &&
269
+ typeof content.text === "string");
270
+ }
271
+ /**
272
+ * Parse MCP text content as JSON if possible
273
+ */
274
+ export function parseMCPTextContent(content) {
275
+ try {
276
+ return JSON.parse(content.text);
277
+ }
278
+ catch {
279
+ return content.text;
280
+ }
281
+ }