@cleocode/lafs-protocol 0.5.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +0 -0
- package/README.md +7 -3
- package/dist/examples/discovery-server.d.ts +8 -0
- package/dist/examples/discovery-server.js +216 -0
- package/dist/examples/mcp-lafs-client.d.ts +10 -0
- package/dist/examples/mcp-lafs-client.js +427 -0
- package/dist/examples/mcp-lafs-server.d.ts +10 -0
- package/dist/examples/mcp-lafs-server.js +358 -0
- package/dist/schemas/v1/envelope.schema.json +0 -0
- package/dist/schemas/v1/error-registry.json +0 -0
- package/dist/src/a2a/bridge.d.ts +129 -0
- package/dist/src/a2a/bridge.js +173 -0
- package/dist/src/a2a/index.d.ts +36 -0
- package/dist/src/a2a/index.js +36 -0
- package/dist/src/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -0
- package/dist/src/circuit-breaker/index.d.ts +121 -0
- package/dist/src/circuit-breaker/index.js +249 -0
- package/dist/src/cli.d.ts +0 -0
- package/dist/src/cli.js +0 -0
- package/dist/src/conformance.d.ts +0 -0
- package/dist/src/conformance.js +0 -0
- package/dist/src/discovery.d.ts +127 -0
- package/dist/src/discovery.js +304 -0
- package/dist/src/errorRegistry.d.ts +0 -0
- package/dist/src/errorRegistry.js +0 -0
- package/dist/src/flagSemantics.d.ts +0 -0
- package/dist/src/flagSemantics.js +0 -0
- package/dist/src/health/index.d.ts +105 -0
- package/dist/src/health/index.js +211 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.js +10 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -0
- package/dist/src/shutdown/index.d.ts +69 -0
- package/dist/src/shutdown/index.js +160 -0
- package/dist/src/tokenEstimator.d.ts +87 -0
- package/dist/src/tokenEstimator.js +238 -0
- package/dist/src/types.d.ts +25 -0
- package/dist/src/types.js +0 -0
- package/dist/src/validateEnvelope.d.ts +0 -0
- package/dist/src/validateEnvelope.js +0 -0
- package/lafs.md +167 -0
- package/package.json +10 -4
- package/schemas/v1/context-ledger.schema.json +0 -0
- package/schemas/v1/discovery.schema.json +132 -0
- package/schemas/v1/envelope.schema.json +0 -0
- package/schemas/v1/error-registry.json +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
export interface HealthCheckConfig {
|
|
7
|
+
path?: string;
|
|
8
|
+
checks?: HealthCheckFunction[];
|
|
9
|
+
}
|
|
10
|
+
export type HealthCheckFunction = () => Promise<HealthCheckResult> | HealthCheckResult;
|
|
11
|
+
export interface HealthCheckResult {
|
|
12
|
+
name: string;
|
|
13
|
+
status: 'ok' | 'warning' | 'error';
|
|
14
|
+
message?: string;
|
|
15
|
+
duration?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HealthStatus {
|
|
18
|
+
status: 'healthy' | 'degraded' | 'unhealthy';
|
|
19
|
+
timestamp: string;
|
|
20
|
+
version: string;
|
|
21
|
+
uptime: number;
|
|
22
|
+
checks: HealthCheckResult[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Health check middleware for Express applications
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import express from 'express';
|
|
30
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
31
|
+
*
|
|
32
|
+
* const app = express();
|
|
33
|
+
*
|
|
34
|
+
* // Basic health check
|
|
35
|
+
* app.use('/health', healthCheck());
|
|
36
|
+
*
|
|
37
|
+
* // Custom health checks
|
|
38
|
+
* app.use('/health', healthCheck({
|
|
39
|
+
* checks: [
|
|
40
|
+
* async () => ({
|
|
41
|
+
* name: 'database',
|
|
42
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
43
|
+
* })
|
|
44
|
+
* ]
|
|
45
|
+
* }));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function healthCheck(config?: HealthCheckConfig): (req: any, res: any) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Create a database health check
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
55
|
+
* checkConnection: async () => await db.ping()
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* app.use('/health', healthCheck({
|
|
59
|
+
* checks: [dbCheck]
|
|
60
|
+
* }));
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function createDatabaseHealthCheck(config: {
|
|
64
|
+
checkConnection: () => Promise<boolean>;
|
|
65
|
+
name?: string;
|
|
66
|
+
}): HealthCheckFunction;
|
|
67
|
+
/**
|
|
68
|
+
* Create an external service health check
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
73
|
+
* name: 'payment-api',
|
|
74
|
+
* url: 'https://api.payment.com/health',
|
|
75
|
+
* timeout: 5000
|
|
76
|
+
* });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
export declare function createExternalServiceHealthCheck(config: {
|
|
80
|
+
name: string;
|
|
81
|
+
url: string;
|
|
82
|
+
timeout?: number;
|
|
83
|
+
}): HealthCheckFunction;
|
|
84
|
+
/**
|
|
85
|
+
* Liveness probe - basic check that service is running
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* app.get('/health/live', livenessProbe());
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function livenessProbe(): (req: any, res: any) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```typescript
|
|
98
|
+
* app.get('/health/ready', readinessProbe({
|
|
99
|
+
* checks: [dbCheck, cacheCheck]
|
|
100
|
+
* }));
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function readinessProbe(config?: {
|
|
104
|
+
checks?: HealthCheckFunction[];
|
|
105
|
+
}): (req: any, res: any) => Promise<void>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Health Check Module
|
|
3
|
+
*
|
|
4
|
+
* Provides health check endpoints for monitoring and orchestration
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Health check middleware for Express applications
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import express from 'express';
|
|
12
|
+
* import { healthCheck } from '@cleocode/lafs-protocol/health';
|
|
13
|
+
*
|
|
14
|
+
* const app = express();
|
|
15
|
+
*
|
|
16
|
+
* // Basic health check
|
|
17
|
+
* app.use('/health', healthCheck());
|
|
18
|
+
*
|
|
19
|
+
* // Custom health checks
|
|
20
|
+
* app.use('/health', healthCheck({
|
|
21
|
+
* checks: [
|
|
22
|
+
* async () => ({
|
|
23
|
+
* name: 'database',
|
|
24
|
+
* status: await checkDatabase() ? 'ok' : 'error'
|
|
25
|
+
* })
|
|
26
|
+
* ]
|
|
27
|
+
* }));
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function healthCheck(config = {}) {
|
|
31
|
+
const { path = '/health', checks = [] } = config;
|
|
32
|
+
const startTime = Date.now();
|
|
33
|
+
return async (req, res) => {
|
|
34
|
+
const timestamp = new Date().toISOString();
|
|
35
|
+
const checkResults = [];
|
|
36
|
+
// Run all health checks
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await check();
|
|
41
|
+
result.duration = Date.now() - start;
|
|
42
|
+
checkResults.push(result);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
checkResults.push({
|
|
46
|
+
name: 'unknown',
|
|
47
|
+
status: 'error',
|
|
48
|
+
message: error instanceof Error ? error.message : 'Check failed',
|
|
49
|
+
duration: Date.now() - start
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Add default checks
|
|
54
|
+
checkResults.push({
|
|
55
|
+
name: 'envelopeValidation',
|
|
56
|
+
status: 'ok'
|
|
57
|
+
});
|
|
58
|
+
checkResults.push({
|
|
59
|
+
name: 'tokenBudgets',
|
|
60
|
+
status: 'ok'
|
|
61
|
+
});
|
|
62
|
+
// Determine overall status
|
|
63
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
64
|
+
const hasWarnings = checkResults.some(c => c.status === 'warning');
|
|
65
|
+
const status = hasErrors
|
|
66
|
+
? 'unhealthy'
|
|
67
|
+
: hasWarnings
|
|
68
|
+
? 'degraded'
|
|
69
|
+
: 'healthy';
|
|
70
|
+
const health = {
|
|
71
|
+
status,
|
|
72
|
+
timestamp,
|
|
73
|
+
version: process.env.npm_package_version || '1.1.0',
|
|
74
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
75
|
+
checks: checkResults
|
|
76
|
+
};
|
|
77
|
+
const statusCode = status === 'healthy' ? 200 : status === 'degraded' ? 200 : 503;
|
|
78
|
+
res.status(statusCode).json(health);
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Create a database health check
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```typescript
|
|
86
|
+
* const dbCheck = createDatabaseHealthCheck({
|
|
87
|
+
* checkConnection: async () => await db.ping()
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* app.use('/health', healthCheck({
|
|
91
|
+
* checks: [dbCheck]
|
|
92
|
+
* }));
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function createDatabaseHealthCheck(config) {
|
|
96
|
+
return async () => {
|
|
97
|
+
try {
|
|
98
|
+
const isConnected = await config.checkConnection();
|
|
99
|
+
return {
|
|
100
|
+
name: config.name || 'database',
|
|
101
|
+
status: isConnected ? 'ok' : 'error',
|
|
102
|
+
message: isConnected ? 'Connected' : 'Connection failed'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
name: config.name || 'database',
|
|
108
|
+
status: 'error',
|
|
109
|
+
message: error instanceof Error ? error.message : 'Database check failed'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create an external service health check
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const apiCheck = createExternalServiceHealthCheck({
|
|
120
|
+
* name: 'payment-api',
|
|
121
|
+
* url: 'https://api.payment.com/health',
|
|
122
|
+
* timeout: 5000
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export function createExternalServiceHealthCheck(config) {
|
|
127
|
+
return async () => {
|
|
128
|
+
const start = Date.now();
|
|
129
|
+
try {
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const timeout = setTimeout(() => controller.abort(), config.timeout || 5000);
|
|
132
|
+
const response = await fetch(config.url, {
|
|
133
|
+
signal: controller.signal
|
|
134
|
+
});
|
|
135
|
+
clearTimeout(timeout);
|
|
136
|
+
return {
|
|
137
|
+
name: config.name,
|
|
138
|
+
status: response.ok ? 'ok' : 'error',
|
|
139
|
+
message: response.ok ? 'Service healthy' : `HTTP ${response.status}`,
|
|
140
|
+
duration: Date.now() - start
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
name: config.name,
|
|
146
|
+
status: 'error',
|
|
147
|
+
message: error instanceof Error ? error.message : 'Service unreachable',
|
|
148
|
+
duration: Date.now() - start
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Liveness probe - basic check that service is running
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* app.get('/health/live', livenessProbe());
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export function livenessProbe() {
|
|
162
|
+
return (req, res) => {
|
|
163
|
+
res.status(200).json({
|
|
164
|
+
status: 'alive',
|
|
165
|
+
timestamp: new Date().toISOString()
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Readiness probe - check that service is ready to accept traffic
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* app.get('/health/ready', readinessProbe({
|
|
175
|
+
* checks: [dbCheck, cacheCheck]
|
|
176
|
+
* }));
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
179
|
+
export function readinessProbe(config = {}) {
|
|
180
|
+
return async (req, res) => {
|
|
181
|
+
const checkResults = [];
|
|
182
|
+
if (config.checks) {
|
|
183
|
+
for (const check of config.checks) {
|
|
184
|
+
try {
|
|
185
|
+
const result = await check();
|
|
186
|
+
checkResults.push(result);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
checkResults.push({
|
|
190
|
+
name: 'unknown',
|
|
191
|
+
status: 'error',
|
|
192
|
+
message: error instanceof Error ? error.message : 'Check failed'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const hasErrors = checkResults.some(c => c.status === 'error');
|
|
198
|
+
if (hasErrors) {
|
|
199
|
+
res.status(503).json({
|
|
200
|
+
status: 'not ready',
|
|
201
|
+
checks: checkResults
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
res.status(200).json({
|
|
206
|
+
status: 'ready',
|
|
207
|
+
checks: checkResults
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -3,3 +3,11 @@ 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";
|
|
10
|
+
export * from "./health/index.js";
|
|
11
|
+
export * from "./shutdown/index.js";
|
|
12
|
+
export * from "./circuit-breaker/index.js";
|
|
13
|
+
export * from "./a2a/index.js";
|
package/dist/src/index.js
CHANGED
|
@@ -3,3 +3,13 @@ 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";
|
|
10
|
+
// Operations & Reliability
|
|
11
|
+
export * from "./health/index.js";
|
|
12
|
+
export * from "./shutdown/index.js";
|
|
13
|
+
export * from "./circuit-breaker/index.js";
|
|
14
|
+
// A2A Integration
|
|
15
|
+
export * from "./a2a/index.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
|
+
}
|