@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.
- package/LICENSE +0 -0
- package/README.md +0 -0
- 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/budgetEnforcement.d.ts +84 -0
- package/dist/src/budgetEnforcement.js +328 -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/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/mcpAdapter.d.ts +28 -0
- package/dist/src/mcpAdapter.js +281 -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 +164 -0
- package/package.json +8 -3
- 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
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LAFS Discovery Example Server
|
|
3
|
+
*
|
|
4
|
+
* Run with: npx tsx examples/discovery-server.ts
|
|
5
|
+
* Or: npm run build && node dist/examples/discovery-server.js
|
|
6
|
+
*/
|
|
7
|
+
import express from "express";
|
|
8
|
+
import { discoveryMiddleware } from "../src/discovery.js";
|
|
9
|
+
const app = express();
|
|
10
|
+
const PORT = process.env.PORT || 3000;
|
|
11
|
+
/**
|
|
12
|
+
* LAFS-compliant envelope endpoint handler
|
|
13
|
+
* Demonstrates proper LAFS envelope processing
|
|
14
|
+
*/
|
|
15
|
+
app.post("/api/v1/envelope", express.json(), (req, res) => {
|
|
16
|
+
const envelope = req.body;
|
|
17
|
+
// Validate basic envelope structure
|
|
18
|
+
if (!envelope._meta) {
|
|
19
|
+
return res.status(400).json({
|
|
20
|
+
success: false,
|
|
21
|
+
error: {
|
|
22
|
+
code: "INVALID_ENVELOPE",
|
|
23
|
+
message: "Missing _meta field",
|
|
24
|
+
category: "VALIDATION",
|
|
25
|
+
retryable: false,
|
|
26
|
+
retryAfterMs: null,
|
|
27
|
+
details: { missing: ["_meta"] }
|
|
28
|
+
},
|
|
29
|
+
result: null
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// Process the request (simplified example)
|
|
33
|
+
const operation = envelope._meta.operation;
|
|
34
|
+
// Echo back with success
|
|
35
|
+
res.json({
|
|
36
|
+
$schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
|
|
37
|
+
_meta: {
|
|
38
|
+
specVersion: envelope._meta.specVersion || "1.0.0",
|
|
39
|
+
schemaVersion: envelope._meta.schemaVersion || "1.0.0",
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
operation: `${operation}:response`,
|
|
42
|
+
requestId: envelope._meta.requestId || crypto.randomUUID(),
|
|
43
|
+
transport: "http",
|
|
44
|
+
strict: envelope._meta.strict ?? true,
|
|
45
|
+
mvi: envelope._meta.mvi || "standard",
|
|
46
|
+
contextVersion: (envelope._meta.contextVersion || 0) + 1
|
|
47
|
+
},
|
|
48
|
+
success: true,
|
|
49
|
+
result: {
|
|
50
|
+
received: true,
|
|
51
|
+
operation: operation,
|
|
52
|
+
data: envelope.payload || null
|
|
53
|
+
},
|
|
54
|
+
error: null
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Context ledger endpoint
|
|
59
|
+
* Demonstrates context management capability
|
|
60
|
+
*/
|
|
61
|
+
app.get("/api/v1/context/:ledgerId", (req, res) => {
|
|
62
|
+
const ledgerId = req.params.ledgerId;
|
|
63
|
+
// Return mock context ledger
|
|
64
|
+
res.json({
|
|
65
|
+
$schema: "https://lafs.dev/schemas/v1/context-ledger.schema.json",
|
|
66
|
+
ledgerId,
|
|
67
|
+
version: 1,
|
|
68
|
+
createdAt: new Date().toISOString(),
|
|
69
|
+
updatedAt: new Date().toISOString(),
|
|
70
|
+
entries: [],
|
|
71
|
+
checksum: "sha256:mock",
|
|
72
|
+
maxEntries: 1000
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
app.post("/api/v1/context/:ledgerId/entries", express.json(), (req, res) => {
|
|
76
|
+
const ledgerId = req.params.ledgerId;
|
|
77
|
+
const entry = req.body;
|
|
78
|
+
res.json({
|
|
79
|
+
$schema: "https://lafs.dev/schemas/v1/envelope.schema.json",
|
|
80
|
+
_meta: {
|
|
81
|
+
specVersion: "1.0.0",
|
|
82
|
+
schemaVersion: "1.0.0",
|
|
83
|
+
timestamp: new Date().toISOString(),
|
|
84
|
+
operation: "context:append",
|
|
85
|
+
requestId: crypto.randomUUID(),
|
|
86
|
+
transport: "http",
|
|
87
|
+
strict: true,
|
|
88
|
+
mvi: "standard",
|
|
89
|
+
contextVersion: 2
|
|
90
|
+
},
|
|
91
|
+
success: true,
|
|
92
|
+
result: {
|
|
93
|
+
ledgerId,
|
|
94
|
+
entryId: crypto.randomUUID(),
|
|
95
|
+
committed: true,
|
|
96
|
+
timestamp: new Date().toISOString()
|
|
97
|
+
},
|
|
98
|
+
error: null
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Discovery configuration
|
|
103
|
+
* Advertises all LAFS capabilities and endpoints
|
|
104
|
+
*/
|
|
105
|
+
const discoveryConfig = {
|
|
106
|
+
service: {
|
|
107
|
+
name: "example-lafs-service",
|
|
108
|
+
version: "1.0.0",
|
|
109
|
+
description: "Example LAFS-compliant API service demonstrating discovery protocol"
|
|
110
|
+
},
|
|
111
|
+
capabilities: [
|
|
112
|
+
{
|
|
113
|
+
name: "envelope-processor",
|
|
114
|
+
version: "1.0.0",
|
|
115
|
+
description: "Process and validate LAFS envelopes",
|
|
116
|
+
operations: ["process", "validate", "transform"]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "context-ledger",
|
|
120
|
+
version: "1.0.0",
|
|
121
|
+
description: "Manage context ledgers for stateful operations",
|
|
122
|
+
operations: ["read", "append", "query"]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "pagination-provider",
|
|
126
|
+
version: "1.0.0",
|
|
127
|
+
description: "Provide cursor and offset pagination for list endpoints",
|
|
128
|
+
operations: ["cursor", "offset", "none"],
|
|
129
|
+
optional: true
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
endpoints: {
|
|
133
|
+
envelope: "/api/v1/envelope",
|
|
134
|
+
context: "/api/v1/context",
|
|
135
|
+
discovery: "https://lafs.dev/schemas/v1/discovery.schema.json"
|
|
136
|
+
},
|
|
137
|
+
cacheMaxAge: 3600,
|
|
138
|
+
lafsVersion: "1.0.0"
|
|
139
|
+
};
|
|
140
|
+
// Mount discovery middleware BEFORE other routes
|
|
141
|
+
// This ensures /.well-known/lafs.json is served at the root
|
|
142
|
+
app.use(discoveryMiddleware(discoveryConfig));
|
|
143
|
+
/**
|
|
144
|
+
* Health check endpoint
|
|
145
|
+
*/
|
|
146
|
+
app.get("/health", (req, res) => {
|
|
147
|
+
res.json({
|
|
148
|
+
status: "healthy",
|
|
149
|
+
service: discoveryConfig.service.name,
|
|
150
|
+
version: discoveryConfig.service.version,
|
|
151
|
+
timestamp: new Date().toISOString()
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
/**
|
|
155
|
+
* Error handling middleware
|
|
156
|
+
*/
|
|
157
|
+
app.use((err, req, res, next) => {
|
|
158
|
+
console.error("Error:", err);
|
|
159
|
+
res.status(500).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: {
|
|
162
|
+
code: "INTERNAL_ERROR",
|
|
163
|
+
message: err.message || "Internal server error",
|
|
164
|
+
category: "INTERNAL",
|
|
165
|
+
retryable: false,
|
|
166
|
+
retryAfterMs: null,
|
|
167
|
+
details: process.env.NODE_ENV === "development" ? { stack: err.stack } : {}
|
|
168
|
+
},
|
|
169
|
+
result: null
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* Start server
|
|
174
|
+
*/
|
|
175
|
+
const server = app.listen(PORT, () => {
|
|
176
|
+
console.log(`
|
|
177
|
+
╔════════════════════════════════════════════════════════╗
|
|
178
|
+
║ LAFS Discovery Server Running ║
|
|
179
|
+
╠════════════════════════════════════════════════════════╣
|
|
180
|
+
║ Service: ${discoveryConfig.service.name.padEnd(43)}║
|
|
181
|
+
║ Version: ${discoveryConfig.service.version.padEnd(43)}║
|
|
182
|
+
║ Port: ${String(PORT).padEnd(43)}║
|
|
183
|
+
╠════════════════════════════════════════════════════════╣
|
|
184
|
+
║ Endpoints: ║
|
|
185
|
+
║ GET /.well-known/lafs.json (Discovery document) ║
|
|
186
|
+
║ POST /api/v1/envelope (Envelope processor) ║
|
|
187
|
+
║ GET /api/v1/context/:id (Context ledger) ║
|
|
188
|
+
║ GET /health (Health check) ║
|
|
189
|
+
╚════════════════════════════════════════════════════════╝
|
|
190
|
+
|
|
191
|
+
Test with:
|
|
192
|
+
curl http://localhost:${PORT}/.well-known/lafs.json | jq
|
|
193
|
+
curl http://localhost:${PORT}/health | jq
|
|
194
|
+
curl -X POST http://localhost:${PORT}/api/v1/envelope \
|
|
195
|
+
-H "Content-Type: application/json" \
|
|
196
|
+
-d '{"_meta":{"operation":"test","requestId":"123"},"payload":{"hello":"world"}}'
|
|
197
|
+
`);
|
|
198
|
+
});
|
|
199
|
+
/**
|
|
200
|
+
* Graceful shutdown
|
|
201
|
+
*/
|
|
202
|
+
process.on("SIGTERM", () => {
|
|
203
|
+
console.log("\nShutting down gracefully...");
|
|
204
|
+
server.close(() => {
|
|
205
|
+
console.log("Server closed");
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
process.on("SIGINT", () => {
|
|
210
|
+
console.log("\nShutting down gracefully...");
|
|
211
|
+
server.close(() => {
|
|
212
|
+
console.log("Server closed");
|
|
213
|
+
process.exit(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
export default app;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP-LAFS Client Example
|
|
4
|
+
*
|
|
5
|
+
* A client that connects to the MCP-LAFS server and validates responses
|
|
6
|
+
* are LAFS-compliant. Demonstrates budget negotiation and envelope validation.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx ts-node examples/mcp-lafs-client.ts
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP-LAFS Client Example
|
|
4
|
+
*
|
|
5
|
+
* A client that connects to the MCP-LAFS server and validates responses
|
|
6
|
+
* are LAFS-compliant. Demonstrates budget negotiation and envelope validation.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx ts-node examples/mcp-lafs-client.ts
|
|
9
|
+
*/
|
|
10
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
12
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { wrapMCPResult } from "../src/mcpAdapter.js";
|
|
15
|
+
import { validateEnvelope } from "../src/validateEnvelope.js";
|
|
16
|
+
// Colors for console output
|
|
17
|
+
const colors = {
|
|
18
|
+
reset: "\x1b[0m",
|
|
19
|
+
green: "\x1b[32m",
|
|
20
|
+
red: "\x1b[31m",
|
|
21
|
+
yellow: "\x1b[33m",
|
|
22
|
+
blue: "\x1b[34m",
|
|
23
|
+
cyan: "\x1b[36m",
|
|
24
|
+
};
|
|
25
|
+
function logSuccess(message) {
|
|
26
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`);
|
|
27
|
+
}
|
|
28
|
+
function logError(message) {
|
|
29
|
+
console.log(`${colors.red}✗${colors.reset} ${message}`);
|
|
30
|
+
}
|
|
31
|
+
function logInfo(message) {
|
|
32
|
+
console.log(`${colors.blue}ℹ${colors.reset} ${message}`);
|
|
33
|
+
}
|
|
34
|
+
function logHeader(message) {
|
|
35
|
+
console.log(`\n${colors.cyan}${message}${colors.reset}`);
|
|
36
|
+
console.log("=".repeat(message.length));
|
|
37
|
+
}
|
|
38
|
+
// Embedded server setup for in-memory testing
|
|
39
|
+
// In production, this would connect to an external server process
|
|
40
|
+
async function createEmbeddedServer() {
|
|
41
|
+
const server = new Server({
|
|
42
|
+
name: "lafs-mcp-server-embedded",
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
}, {
|
|
45
|
+
capabilities: {
|
|
46
|
+
tools: {},
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
// Simulated database
|
|
50
|
+
const simulatedDatabase = new Map([
|
|
51
|
+
["1", { id: "1", name: "Product A", value: 100 }],
|
|
52
|
+
["2", { id: "2", name: "Product B", value: 200 }],
|
|
53
|
+
["3", { id: "3", name: "Product C", value: 300 }],
|
|
54
|
+
]);
|
|
55
|
+
// Tool handlers
|
|
56
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
57
|
+
tools: [
|
|
58
|
+
{
|
|
59
|
+
name: "weather",
|
|
60
|
+
description: "Get current weather for a location",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
location: { type: "string" },
|
|
65
|
+
units: { type: "string", enum: ["celsius", "fahrenheit"] },
|
|
66
|
+
_budget: { type: "number", minimum: 10, maximum: 10000 },
|
|
67
|
+
},
|
|
68
|
+
required: ["location"],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "calculator",
|
|
73
|
+
description: "Perform mathematical calculations",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: "object",
|
|
76
|
+
properties: {
|
|
77
|
+
operation: { type: "string", enum: ["add", "subtract", "multiply", "divide"] },
|
|
78
|
+
a: { type: "number" },
|
|
79
|
+
b: { type: "number" },
|
|
80
|
+
_budget: { type: "number", minimum: 10, maximum: 1000 },
|
|
81
|
+
},
|
|
82
|
+
required: ["operation", "a", "b"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "database_query",
|
|
87
|
+
description: "Query the simulated database",
|
|
88
|
+
inputSchema: {
|
|
89
|
+
type: "object",
|
|
90
|
+
properties: {
|
|
91
|
+
action: { type: "string", enum: ["get", "list", "search"] },
|
|
92
|
+
id: { type: "string" },
|
|
93
|
+
query: { type: "string" },
|
|
94
|
+
_budget: { type: "number", minimum: 10, maximum: 5000 },
|
|
95
|
+
},
|
|
96
|
+
required: ["action"],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
}));
|
|
101
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
102
|
+
const { name, arguments: args } = request.params;
|
|
103
|
+
const budget = typeof args?._budget === "number" ? args._budget : undefined;
|
|
104
|
+
try {
|
|
105
|
+
let result;
|
|
106
|
+
switch (name) {
|
|
107
|
+
case "weather": {
|
|
108
|
+
const location = String(args?.location ?? "Unknown");
|
|
109
|
+
const units = String(args?.units ?? "celsius");
|
|
110
|
+
result = {
|
|
111
|
+
location,
|
|
112
|
+
temperature: units === "fahrenheit" ? 72 : 22,
|
|
113
|
+
temperatureUnit: units,
|
|
114
|
+
conditions: "sunny",
|
|
115
|
+
humidity: 45,
|
|
116
|
+
windSpeed: 10,
|
|
117
|
+
forecast: [
|
|
118
|
+
{ day: "Today", high: 24, low: 18, condition: "sunny" },
|
|
119
|
+
{ day: "Tomorrow", high: 25, low: 19, condition: "partly cloudy" },
|
|
120
|
+
{ day: "Day after", high: 23, low: 17, condition: "clear" },
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case "calculator": {
|
|
126
|
+
const operation = String(args?.operation);
|
|
127
|
+
const a = Number(args?.a);
|
|
128
|
+
const b = Number(args?.b);
|
|
129
|
+
let calcResult;
|
|
130
|
+
switch (operation) {
|
|
131
|
+
case "add":
|
|
132
|
+
calcResult = a + b;
|
|
133
|
+
break;
|
|
134
|
+
case "subtract":
|
|
135
|
+
calcResult = a - b;
|
|
136
|
+
break;
|
|
137
|
+
case "multiply":
|
|
138
|
+
calcResult = a * b;
|
|
139
|
+
break;
|
|
140
|
+
case "divide":
|
|
141
|
+
if (b === 0)
|
|
142
|
+
throw new Error("Cannot divide by zero");
|
|
143
|
+
calcResult = a / b;
|
|
144
|
+
break;
|
|
145
|
+
default: throw new Error(`Unknown operation: ${operation}`);
|
|
146
|
+
}
|
|
147
|
+
result = {
|
|
148
|
+
operation,
|
|
149
|
+
expression: `${a} ${operation} ${b}`,
|
|
150
|
+
operands: { a, b },
|
|
151
|
+
result: calcResult,
|
|
152
|
+
};
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case "database_query": {
|
|
156
|
+
const action = String(args?.action);
|
|
157
|
+
switch (action) {
|
|
158
|
+
case "get": {
|
|
159
|
+
const id = String(args?.id);
|
|
160
|
+
const record = simulatedDatabase.get(id);
|
|
161
|
+
if (!record)
|
|
162
|
+
throw new Error(`Record ${id} not found`);
|
|
163
|
+
result = { action, record, found: true };
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "list": {
|
|
167
|
+
const records = Array.from(simulatedDatabase.values());
|
|
168
|
+
result = { action, records, count: records.length, total: records.length };
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
case "search": {
|
|
172
|
+
const query = String(args?.query ?? "").toLowerCase();
|
|
173
|
+
const records = Array.from(simulatedDatabase.values()).filter((r) => r.name.toLowerCase().includes(query));
|
|
174
|
+
result = { action, query, records, count: records.length, total: simulatedDatabase.size };
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
default:
|
|
178
|
+
throw new Error(`Unknown action: ${action}`);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
184
|
+
}
|
|
185
|
+
const mcpResult = {
|
|
186
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
187
|
+
isError: false,
|
|
188
|
+
};
|
|
189
|
+
const envelope = wrapMCPResult(mcpResult, `tools/${name}`, budget);
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: "text", text: JSON.stringify(envelope) }],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
196
|
+
const mcpResult = {
|
|
197
|
+
content: [{ type: "text", text: errorMessage }],
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
const envelope = wrapMCPResult(mcpResult, `tools/${name}`, budget);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text", text: JSON.stringify(envelope) }],
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
return server;
|
|
208
|
+
}
|
|
209
|
+
// Client class that connects and validates LAFS responses
|
|
210
|
+
class LAFSMCPClient {
|
|
211
|
+
client;
|
|
212
|
+
validations = [];
|
|
213
|
+
constructor() {
|
|
214
|
+
this.client = new Client({
|
|
215
|
+
name: "lafs-mcp-client",
|
|
216
|
+
version: "1.0.0",
|
|
217
|
+
}, {
|
|
218
|
+
capabilities: {},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async connect(server) {
|
|
222
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
223
|
+
await Promise.all([
|
|
224
|
+
this.client.connect(clientTransport),
|
|
225
|
+
server.connect(serverTransport),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
async listTools() {
|
|
229
|
+
const response = await this.client.listTools();
|
|
230
|
+
return response.tools.map((t) => t.name);
|
|
231
|
+
}
|
|
232
|
+
async callTool(name, args) {
|
|
233
|
+
const result = await this.client.callTool({
|
|
234
|
+
name,
|
|
235
|
+
arguments: args,
|
|
236
|
+
});
|
|
237
|
+
// Extract LAFS envelope from MCP response
|
|
238
|
+
const content = result.content;
|
|
239
|
+
const textContent = content.find((c) => c.type === "text");
|
|
240
|
+
if (!textContent || !("text" in textContent) || !textContent.text) {
|
|
241
|
+
throw new Error("No text content in MCP response");
|
|
242
|
+
}
|
|
243
|
+
const envelope = JSON.parse(textContent.text);
|
|
244
|
+
// Validate against LAFS schema
|
|
245
|
+
const validation = validateEnvelope(envelope);
|
|
246
|
+
this.validations.push({
|
|
247
|
+
tool: name,
|
|
248
|
+
valid: validation.valid,
|
|
249
|
+
errors: validation.errors,
|
|
250
|
+
});
|
|
251
|
+
return { envelope, validation };
|
|
252
|
+
}
|
|
253
|
+
getValidationSummary() {
|
|
254
|
+
return {
|
|
255
|
+
total: this.validations.length,
|
|
256
|
+
passed: this.validations.filter((v) => v.valid).length,
|
|
257
|
+
failed: this.validations.filter((v) => !v.valid).length,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
printValidationReport() {
|
|
261
|
+
console.log("\n" + "=".repeat(60));
|
|
262
|
+
console.log("LAFS VALIDATION REPORT");
|
|
263
|
+
console.log("=".repeat(60));
|
|
264
|
+
for (const validation of this.validations) {
|
|
265
|
+
if (validation.valid) {
|
|
266
|
+
logSuccess(`${validation.tool}: Valid LAFS envelope`);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
logError(`${validation.tool}: Invalid LAFS envelope`);
|
|
270
|
+
for (const error of validation.errors) {
|
|
271
|
+
console.log(` - ${error}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const summary = this.getValidationSummary();
|
|
276
|
+
console.log("\n" + "-".repeat(60));
|
|
277
|
+
console.log(`Total: ${summary.total} | Passed: ${summary.passed} | Failed: ${summary.failed}`);
|
|
278
|
+
console.log("=".repeat(60));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// Main demonstration
|
|
282
|
+
async function main() {
|
|
283
|
+
logHeader("LAFS-MCP Integration Demo");
|
|
284
|
+
logInfo("Starting embedded MCP server with LAFS envelope wrapping...");
|
|
285
|
+
// Create and start embedded server
|
|
286
|
+
const server = await createEmbeddedServer();
|
|
287
|
+
const client = new LAFSMCPClient();
|
|
288
|
+
await client.connect(server);
|
|
289
|
+
logSuccess("Connected to MCP server\n");
|
|
290
|
+
// List available tools
|
|
291
|
+
logHeader("Available Tools");
|
|
292
|
+
const tools = await client.listTools();
|
|
293
|
+
for (const tool of tools) {
|
|
294
|
+
console.log(` • ${tool}`);
|
|
295
|
+
}
|
|
296
|
+
// Test 1: Weather tool
|
|
297
|
+
logHeader("Test 1: Weather Tool (Standard)");
|
|
298
|
+
try {
|
|
299
|
+
const { envelope, validation } = await client.callTool("weather", {
|
|
300
|
+
location: "San Francisco",
|
|
301
|
+
units: "celsius",
|
|
302
|
+
});
|
|
303
|
+
if (validation.valid) {
|
|
304
|
+
logSuccess("Response is valid LAFS envelope");
|
|
305
|
+
console.log("\nResponse Metadata:");
|
|
306
|
+
console.log(` Spec Version: ${envelope._meta.specVersion}`);
|
|
307
|
+
console.log(` Operation: ${envelope._meta.operation}`);
|
|
308
|
+
console.log(` Success: ${envelope.success}`);
|
|
309
|
+
console.log(` Timestamp: ${envelope._meta.timestamp}`);
|
|
310
|
+
console.log("\nResult:");
|
|
311
|
+
console.log(JSON.stringify(envelope.result, null, 2));
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
logError("Response failed LAFS validation");
|
|
315
|
+
validation.errors.forEach((e) => console.log(` - ${e}`));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
logError(`Test failed: ${error instanceof Error ? error.message : error}`);
|
|
320
|
+
}
|
|
321
|
+
// Test 2: Calculator with budget
|
|
322
|
+
logHeader("Test 2: Calculator Tool (With Budget)");
|
|
323
|
+
try {
|
|
324
|
+
const { envelope, validation } = await client.callTool("calculator", {
|
|
325
|
+
operation: "multiply",
|
|
326
|
+
a: 42,
|
|
327
|
+
b: 100,
|
|
328
|
+
_budget: 50,
|
|
329
|
+
});
|
|
330
|
+
if (validation.valid) {
|
|
331
|
+
logSuccess("Response is valid LAFS envelope");
|
|
332
|
+
const metaWithBudget = envelope._meta;
|
|
333
|
+
if (metaWithBudget._tokenEstimate) {
|
|
334
|
+
console.log("\nBudget Information:");
|
|
335
|
+
console.log(` Estimated Tokens: ${metaWithBudget._tokenEstimate.estimated}`);
|
|
336
|
+
console.log(` Truncated: ${metaWithBudget._tokenEstimate.truncated ?? false}`);
|
|
337
|
+
if (metaWithBudget._tokenEstimate.originalEstimate) {
|
|
338
|
+
console.log(` Original Estimate: ${metaWithBudget._tokenEstimate.originalEstimate}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
console.log("\nResult:");
|
|
342
|
+
console.log(JSON.stringify(envelope.result, null, 2));
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
logError("Response failed LAFS validation");
|
|
346
|
+
validation.errors.forEach((e) => console.log(` - ${e}`));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
logError(`Test failed: ${error instanceof Error ? error.message : error}`);
|
|
351
|
+
}
|
|
352
|
+
// Test 3: Database query
|
|
353
|
+
logHeader("Test 3: Database Query Tool");
|
|
354
|
+
try {
|
|
355
|
+
const { envelope, validation } = await client.callTool("database_query", {
|
|
356
|
+
action: "list",
|
|
357
|
+
});
|
|
358
|
+
if (validation.valid) {
|
|
359
|
+
logSuccess("Response is valid LAFS envelope");
|
|
360
|
+
console.log("\nResult:");
|
|
361
|
+
console.log(JSON.stringify(envelope.result, null, 2));
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
logError("Response failed LAFS validation");
|
|
365
|
+
validation.errors.forEach((e) => console.log(` - ${e}`));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
logError(`Test failed: ${error instanceof Error ? error.message : error}`);
|
|
370
|
+
}
|
|
371
|
+
// Test 4: Error handling
|
|
372
|
+
logHeader("Test 4: Error Handling (Division by Zero)");
|
|
373
|
+
try {
|
|
374
|
+
const { envelope, validation } = await client.callTool("calculator", {
|
|
375
|
+
operation: "divide",
|
|
376
|
+
a: 10,
|
|
377
|
+
b: 0,
|
|
378
|
+
});
|
|
379
|
+
if (validation.valid) {
|
|
380
|
+
logSuccess("Error response is valid LAFS envelope");
|
|
381
|
+
console.log("\nError Details:");
|
|
382
|
+
console.log(` Success: ${envelope.success}`);
|
|
383
|
+
console.log(` Error Code: ${envelope.error?.code}`);
|
|
384
|
+
console.log(` Category: ${envelope.error?.category}`);
|
|
385
|
+
console.log(` Retryable: ${envelope.error?.retryable}`);
|
|
386
|
+
console.log(` Message: ${envelope.error?.message}`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
logError("Error response failed LAFS validation");
|
|
390
|
+
validation.errors.forEach((e) => console.log(` - ${e}`));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
logError(`Test failed: ${error instanceof Error ? error.message : error}`);
|
|
395
|
+
}
|
|
396
|
+
// Test 5: Database not found
|
|
397
|
+
logHeader("Test 5: Not Found Error");
|
|
398
|
+
try {
|
|
399
|
+
const { envelope, validation } = await client.callTool("database_query", {
|
|
400
|
+
action: "get",
|
|
401
|
+
id: "999",
|
|
402
|
+
});
|
|
403
|
+
if (validation.valid) {
|
|
404
|
+
logSuccess("Not found error is valid LAFS envelope");
|
|
405
|
+
console.log("\nError Details:");
|
|
406
|
+
console.log(` Success: ${envelope.success}`);
|
|
407
|
+
console.log(` Error Code: ${envelope.error?.code}`);
|
|
408
|
+
console.log(` Category: ${envelope.error?.category}`);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
logError("Error response failed LAFS validation");
|
|
412
|
+
validation.errors.forEach((e) => console.log(` - ${e}`));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
logError(`Test failed: ${error instanceof Error ? error.message : error}`);
|
|
417
|
+
}
|
|
418
|
+
// Print final validation report
|
|
419
|
+
client.printValidationReport();
|
|
420
|
+
logHeader("Demo Complete");
|
|
421
|
+
logInfo("All MCP tool responses are wrapped in LAFS-compliant envelopes");
|
|
422
|
+
logInfo("This proves LAFS complements MCP by adding structured metadata");
|
|
423
|
+
}
|
|
424
|
+
main().catch((error) => {
|
|
425
|
+
console.error("Fatal error:", error);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP-LAFS Server Example
|
|
4
|
+
*
|
|
5
|
+
* A working MCP server that wraps all tool responses in LAFS-compliant envelopes.
|
|
6
|
+
* Demonstrates how LAFS complements MCP by adding structured metadata and budget enforcement.
|
|
7
|
+
*
|
|
8
|
+
* Usage: npx ts-node examples/mcp-lafs-server.ts
|
|
9
|
+
*/
|
|
10
|
+
export {};
|