@iflow-mcp/andrewhopper-facts-server 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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/build/index.js ADDED
@@ -0,0 +1,341 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
5
+ import { PrismaStorageProvider } from './storage.js';
6
+ import { validateCriteria } from './validation.js';
7
+ import { StrictnessLevel, FactCategory } from './types.js';
8
+ class FactsServer {
9
+ constructor() {
10
+ this.storage = new PrismaStorageProvider();
11
+ this.server = new Server({
12
+ name: 'facts-server',
13
+ version: '0.1.0',
14
+ }, {
15
+ capabilities: {
16
+ tools: {
17
+ get_all_facts: {
18
+ description: 'Get all facts in the system',
19
+ inputSchema: {
20
+ type: 'object',
21
+ properties: {}
22
+ }
23
+ },
24
+ get_fact: {
25
+ description: 'Get a fact by ID',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ id: {
30
+ type: 'string',
31
+ description: 'The ID of the fact to retrieve'
32
+ }
33
+ },
34
+ required: ['id']
35
+ }
36
+ },
37
+ search_facts: {
38
+ description: 'Search facts by type, strictness, and version',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ type: {
43
+ type: 'string',
44
+ description: 'Filter by fact type'
45
+ },
46
+ strictness: {
47
+ type: 'string',
48
+ enum: Object.values(StrictnessLevel),
49
+ description: 'Filter by strictness level'
50
+ },
51
+ version: {
52
+ type: 'string',
53
+ description: 'Filter by version compatibility'
54
+ }
55
+ }
56
+ }
57
+ },
58
+ set_fact: {
59
+ description: 'Create or update a fact',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ id: { type: 'string' },
64
+ content: { type: 'string' },
65
+ strictness: {
66
+ type: 'string',
67
+ enum: Object.values(StrictnessLevel)
68
+ },
69
+ type: { type: 'string' },
70
+ category: {
71
+ type: 'string',
72
+ enum: Object.values(FactCategory),
73
+ description: 'The category this fact belongs to'
74
+ },
75
+ minVersion: { type: 'string' },
76
+ maxVersion: { type: 'string' },
77
+ conditions: {
78
+ type: 'array',
79
+ items: {
80
+ type: 'object',
81
+ properties: {
82
+ factId: { type: 'string' },
83
+ type: {
84
+ type: 'string',
85
+ enum: ['REQUIRES', 'CONFLICTS_WITH']
86
+ }
87
+ },
88
+ required: ['factId', 'type']
89
+ }
90
+ },
91
+ acceptanceCriteria: {
92
+ type: 'array',
93
+ items: {
94
+ type: 'object',
95
+ properties: {
96
+ id: { type: 'string' },
97
+ description: { type: 'string' },
98
+ validationType: {
99
+ type: 'string',
100
+ enum: ['MANUAL', 'AUTOMATED']
101
+ },
102
+ validationScript: { type: 'string' }
103
+ },
104
+ required: ['id', 'description', 'validationType']
105
+ }
106
+ }
107
+ },
108
+ required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion']
109
+ }
110
+ },
111
+ validate_criteria: {
112
+ description: 'Validate content against fact acceptance criteria',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ factId: {
117
+ type: 'string',
118
+ description: 'ID of the fact to validate against'
119
+ },
120
+ content: {
121
+ type: 'string',
122
+ description: 'Content to validate'
123
+ }
124
+ },
125
+ required: ['factId', 'content']
126
+ }
127
+ }
128
+ }
129
+ }
130
+ });
131
+ this.setupHandlers();
132
+ }
133
+ setupHandlers() {
134
+ // Handler for listing available tools
135
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
136
+ tools: [
137
+ {
138
+ name: 'get_all_facts',
139
+ description: 'Get all facts in the system',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {}
143
+ }
144
+ },
145
+ {
146
+ name: 'get_fact',
147
+ description: 'Get a fact by ID',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ id: {
152
+ type: 'string',
153
+ description: 'The ID of the fact to retrieve'
154
+ }
155
+ },
156
+ required: ['id']
157
+ }
158
+ },
159
+ {
160
+ name: 'search_facts',
161
+ description: 'Search facts by type, strictness, and version',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ type: {
166
+ type: 'string',
167
+ description: 'Filter by fact type'
168
+ },
169
+ strictness: {
170
+ type: 'string',
171
+ enum: Object.values(StrictnessLevel),
172
+ description: 'Filter by strictness level'
173
+ },
174
+ version: {
175
+ type: 'string',
176
+ description: 'Filter by version compatibility'
177
+ }
178
+ }
179
+ }
180
+ },
181
+ {
182
+ name: 'set_fact',
183
+ description: 'Create or update a fact',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ id: { type: 'string' },
188
+ content: { type: 'string' },
189
+ strictness: {
190
+ type: 'string',
191
+ enum: Object.values(StrictnessLevel)
192
+ },
193
+ type: { type: 'string' },
194
+ category: {
195
+ type: 'string',
196
+ enum: Object.values(FactCategory),
197
+ description: 'The category this fact belongs to'
198
+ },
199
+ minVersion: { type: 'string' },
200
+ maxVersion: { type: 'string' },
201
+ conditions: {
202
+ type: 'array',
203
+ items: {
204
+ type: 'object',
205
+ properties: {
206
+ factId: { type: 'string' },
207
+ type: {
208
+ type: 'string',
209
+ enum: ['REQUIRES', 'CONFLICTS_WITH']
210
+ }
211
+ },
212
+ required: ['factId', 'type']
213
+ }
214
+ },
215
+ acceptanceCriteria: {
216
+ type: 'array',
217
+ items: {
218
+ type: 'object',
219
+ properties: {
220
+ id: { type: 'string' },
221
+ description: { type: 'string' },
222
+ validationType: {
223
+ type: 'string',
224
+ enum: ['MANUAL', 'AUTOMATED']
225
+ },
226
+ validationScript: { type: 'string' }
227
+ },
228
+ required: ['id', 'description', 'validationType']
229
+ }
230
+ }
231
+ },
232
+ required: ['id', 'content', 'strictness', 'type', 'category', 'minVersion', 'maxVersion']
233
+ }
234
+ },
235
+ {
236
+ name: 'validate_criteria',
237
+ description: 'Validate content against fact acceptance criteria',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ factId: {
242
+ type: 'string',
243
+ description: 'ID of the fact to validate against'
244
+ },
245
+ content: {
246
+ type: 'string',
247
+ description: 'Content to validate'
248
+ }
249
+ },
250
+ required: ['factId', 'content']
251
+ }
252
+ }
253
+ ]
254
+ }));
255
+ // Handler for executing tools
256
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
257
+ switch (request.params.name) {
258
+ case 'get_all_facts': {
259
+ const facts = await this.storage.searchFacts({});
260
+ return {
261
+ content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }]
262
+ };
263
+ }
264
+ case 'get_fact': {
265
+ const args = request.params.arguments;
266
+ const fact = await this.storage.getFact(args.id);
267
+ if (!fact) {
268
+ return {
269
+ content: [{ type: 'text', text: `Fact not found: ${args.id}` }],
270
+ isError: true
271
+ };
272
+ }
273
+ return {
274
+ content: [{ type: 'text', text: JSON.stringify(fact, null, 2) }]
275
+ };
276
+ }
277
+ case 'search_facts': {
278
+ const args = request.params.arguments;
279
+ const facts = await this.storage.searchFacts(args);
280
+ return {
281
+ content: [{ type: 'text', text: JSON.stringify(facts, null, 2) }]
282
+ };
283
+ }
284
+ case 'set_fact': {
285
+ const args = request.params.arguments;
286
+ try {
287
+ await this.storage.setFact(args.id, args.content, args.strictness, args.type, args.category, args.minVersion, args.maxVersion, args.conditions || [], args.acceptanceCriteria || []);
288
+ return {
289
+ content: [{ type: 'text', text: `Fact ${args.id} saved successfully` }]
290
+ };
291
+ }
292
+ catch (error) {
293
+ return {
294
+ content: [{ type: 'text', text: `Error saving fact: ${error instanceof Error ? error.message : 'Unknown error'}` }],
295
+ isError: true
296
+ };
297
+ }
298
+ }
299
+ case 'validate_criteria': {
300
+ const args = request.params.arguments;
301
+ const fact = await this.storage.getFact(args.factId);
302
+ if (!fact) {
303
+ return {
304
+ content: [{ type: 'text', text: `Fact not found: ${args.factId}` }],
305
+ isError: true
306
+ };
307
+ }
308
+ try {
309
+ const results = await validateCriteria(args.content, fact.acceptanceCriteria);
310
+ return {
311
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
312
+ };
313
+ }
314
+ catch (error) {
315
+ return {
316
+ content: [{ type: 'text', text: `Validation error: ${error instanceof Error ? error.message : 'Unknown error'}` }],
317
+ isError: true
318
+ };
319
+ }
320
+ }
321
+ }
322
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
323
+ });
324
+ }
325
+ async run() {
326
+ const transport = new StdioServerTransport();
327
+ await this.server.connect(transport);
328
+ console.error('Facts MCP server running on stdio');
329
+ // Handle cleanup on shutdown
330
+ process.on('SIGINT', async () => {
331
+ await this.storage.close();
332
+ process.exit(0);
333
+ });
334
+ process.on('SIGTERM', async () => {
335
+ await this.storage.close();
336
+ process.exit(0);
337
+ });
338
+ }
339
+ }
340
+ const server = new FactsServer();
341
+ server.run().catch(console.error);
@@ -0,0 +1,13 @@
1
+ import { ServerRequest, ServerResponse, ServerInfo, ServerCapabilities, ServerTransport } from './types.js';
2
+ export declare class Server {
3
+ private info;
4
+ private capabilities;
5
+ private transport;
6
+ private handlers;
7
+ constructor(info: ServerInfo, capabilities: {
8
+ capabilities: ServerCapabilities;
9
+ });
10
+ connect(transport: ServerTransport): Promise<void>;
11
+ close(): Promise<void>;
12
+ setHandler(method: string, handler: (request: ServerRequest) => Promise<ServerResponse>): void;
13
+ }
@@ -0,0 +1,31 @@
1
+ export class Server {
2
+ constructor(info, capabilities) {
3
+ this.info = info;
4
+ this.capabilities = capabilities;
5
+ this.transport = null;
6
+ this.handlers = new Map();
7
+ }
8
+ async connect(transport) {
9
+ this.transport = transport;
10
+ transport.onRequest(async (request) => {
11
+ const handler = this.handlers.get(request.method);
12
+ if (!handler) {
13
+ return {
14
+ content: [{ type: 'text', text: `Unknown method: ${request.method}` }],
15
+ isError: true
16
+ };
17
+ }
18
+ return handler(request);
19
+ });
20
+ await transport.connect();
21
+ }
22
+ async close() {
23
+ if (this.transport) {
24
+ await this.transport.disconnect();
25
+ this.transport = null;
26
+ }
27
+ }
28
+ setHandler(method, handler) {
29
+ this.handlers.set(method, handler);
30
+ }
31
+ }
@@ -0,0 +1,8 @@
1
+ import { ServerRequest, ServerResponse, ServerTransport } from './types.js';
2
+ export declare class StdioServerTransport implements ServerTransport {
3
+ private requestHandler;
4
+ private readline;
5
+ connect(): Promise<void>;
6
+ disconnect(): Promise<void>;
7
+ onRequest(handler: (request: ServerRequest) => Promise<ServerResponse>): void;
8
+ }
@@ -0,0 +1,34 @@
1
+ import { createInterface } from 'readline';
2
+ export class StdioServerTransport {
3
+ constructor() {
4
+ this.requestHandler = null;
5
+ this.readline = createInterface({
6
+ input: process.stdin,
7
+ output: process.stdout
8
+ });
9
+ }
10
+ async connect() {
11
+ this.readline.on('line', async (line) => {
12
+ try {
13
+ const request = JSON.parse(line);
14
+ if (this.requestHandler) {
15
+ const response = await this.requestHandler(request);
16
+ console.log(JSON.stringify(response));
17
+ }
18
+ }
19
+ catch (error) {
20
+ console.error('Error processing request:', error);
21
+ console.log(JSON.stringify({
22
+ content: [{ type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` }],
23
+ isError: true
24
+ }));
25
+ }
26
+ });
27
+ }
28
+ async disconnect() {
29
+ this.readline.close();
30
+ }
31
+ onRequest(handler) {
32
+ this.requestHandler = handler;
33
+ }
34
+ }
@@ -0,0 +1,30 @@
1
+ export interface ServerRequest {
2
+ method: string;
3
+ params?: Record<string, unknown>;
4
+ }
5
+ export interface ServerResponse {
6
+ content: Array<{
7
+ type: string;
8
+ text: string;
9
+ }>;
10
+ isError?: boolean;
11
+ }
12
+ export interface ServerCapabilities {
13
+ tools?: Record<string, {
14
+ description: string;
15
+ inputSchema: {
16
+ type: string;
17
+ properties: Record<string, unknown>;
18
+ required?: string[];
19
+ };
20
+ }>;
21
+ }
22
+ export interface ServerInfo {
23
+ name: string;
24
+ version: string;
25
+ }
26
+ export interface ServerTransport {
27
+ connect(): Promise<void>;
28
+ disconnect(): Promise<void>;
29
+ onRequest(handler: (request: ServerRequest) => Promise<ServerResponse>): void;
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { StorageProvider, StorageSearchResult, StrictnessLevel, FactCategory, Condition, AcceptanceCriterion } from './types.js';
2
+ export declare class PrismaStorageProvider implements StorageProvider {
3
+ private prisma;
4
+ constructor();
5
+ setFact(id: string, content: string, strictness: StrictnessLevel, type: string, category: FactCategory, minVersion: string, maxVersion: string, conditions: Condition[], acceptanceCriteria: AcceptanceCriterion[], contentEmbedding?: string): Promise<void>;
6
+ getFact(id: string): Promise<StorageSearchResult | null>;
7
+ searchFacts(options: {
8
+ type?: string;
9
+ category?: FactCategory;
10
+ strictness?: StrictnessLevel;
11
+ version?: string;
12
+ embedding?: string;
13
+ similarityThreshold?: number;
14
+ }): Promise<StorageSearchResult[]>;
15
+ deleteFact(id: string): Promise<boolean>;
16
+ close(): Promise<void>;
17
+ }
@@ -0,0 +1,193 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ export class PrismaStorageProvider {
3
+ constructor() {
4
+ this.prisma = new PrismaClient();
5
+ }
6
+ async setFact(id, content, strictness, type, category, minVersion, maxVersion, conditions, acceptanceCriteria, contentEmbedding) {
7
+ const data = {
8
+ id,
9
+ content,
10
+ strictness,
11
+ type,
12
+ category,
13
+ minVersion,
14
+ maxVersion,
15
+ content_embedding: contentEmbedding,
16
+ conditions: {
17
+ create: conditions.map(condition => ({
18
+ type: condition.type,
19
+ targetId: condition.factId
20
+ }))
21
+ },
22
+ acceptanceCriteria: {
23
+ create: acceptanceCriteria.map(criterion => ({
24
+ id: criterion.id,
25
+ description: criterion.description,
26
+ validationType: criterion.validationType,
27
+ validationScript: criterion.validationScript
28
+ }))
29
+ }
30
+ };
31
+ await this.prisma.fact.upsert({
32
+ where: { id },
33
+ create: data,
34
+ update: {
35
+ ...data,
36
+ conditions: {
37
+ deleteMany: {},
38
+ create: conditions.map(condition => ({
39
+ type: condition.type,
40
+ targetId: condition.factId
41
+ }))
42
+ },
43
+ acceptanceCriteria: {
44
+ deleteMany: {},
45
+ create: acceptanceCriteria.map(criterion => ({
46
+ id: criterion.id,
47
+ description: criterion.description,
48
+ validationType: criterion.validationType,
49
+ validationScript: criterion.validationScript
50
+ }))
51
+ }
52
+ }
53
+ });
54
+ }
55
+ async getFact(id) {
56
+ const fact = await this.prisma.fact.findUnique({
57
+ where: { id },
58
+ include: {
59
+ conditions: true,
60
+ acceptanceCriteria: true
61
+ }
62
+ });
63
+ if (!fact) {
64
+ return null;
65
+ }
66
+ return {
67
+ id: fact.id,
68
+ content: fact.content,
69
+ strictness: fact.strictness,
70
+ type: fact.type,
71
+ category: fact.category,
72
+ minVersion: fact.minVersion,
73
+ maxVersion: fact.maxVersion,
74
+ conditions: fact.conditions.map(c => ({
75
+ factId: c.targetId,
76
+ type: c.type
77
+ })),
78
+ acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({
79
+ id: ac.id,
80
+ description: ac.description,
81
+ validationType: ac.validationType,
82
+ validationScript: ac.validationScript || undefined
83
+ })),
84
+ createdAt: fact.createdAt.toISOString(),
85
+ updatedAt: fact.updatedAt.toISOString(),
86
+ applicable: fact.applicable
87
+ };
88
+ }
89
+ async searchFacts(options) {
90
+ let facts;
91
+ if (options.embedding) {
92
+ // Use raw query for vector similarity search
93
+ const threshold = options.similarityThreshold || 0.8;
94
+ const whereConditions = [];
95
+ const params = [options.embedding, threshold];
96
+ if (options.type) {
97
+ whereConditions.push('f.type = ?');
98
+ params.push(options.type);
99
+ }
100
+ if (options.category) {
101
+ whereConditions.push('f.category = ?');
102
+ params.push(options.category);
103
+ }
104
+ if (options.strictness) {
105
+ whereConditions.push('f.strictness = ?');
106
+ params.push(options.strictness);
107
+ }
108
+ if (options.version) {
109
+ whereConditions.push('f.minVersion <= ? AND f.maxVersion >= ?');
110
+ params.push(options.version, options.version);
111
+ }
112
+ const whereClause = whereConditions.length
113
+ ? 'AND ' + whereConditions.join(' AND ')
114
+ : '';
115
+ const rawResults = await this.prisma.$queryRawUnsafe(`SELECT f.*,
116
+ vector_similarity(f.content_embedding, ?) as similarity
117
+ FROM "Fact" f
118
+ WHERE vector_similarity(f.content_embedding, ?) > ?
119
+ ${whereClause}
120
+ ORDER BY similarity DESC`, options.embedding, options.embedding, threshold, ...params);
121
+ // Fetch full fact data including relations
122
+ const factPromises = rawResults.map(result => this.prisma.fact.findUnique({
123
+ where: { id: result.id },
124
+ include: {
125
+ conditions: true,
126
+ acceptanceCriteria: true
127
+ }
128
+ }));
129
+ facts = (await Promise.all(factPromises)).filter((fact) => fact !== null);
130
+ }
131
+ else {
132
+ const where = {};
133
+ if (options.type) {
134
+ where.type = options.type;
135
+ }
136
+ if (options.category) {
137
+ where.category = options.category;
138
+ }
139
+ if (options.strictness) {
140
+ where.strictness = options.strictness;
141
+ }
142
+ if (options.version) {
143
+ where.AND = [
144
+ { minVersion: { lte: options.version } },
145
+ { maxVersion: { gte: options.version } }
146
+ ];
147
+ }
148
+ facts = await this.prisma.fact.findMany({
149
+ where,
150
+ include: {
151
+ conditions: true,
152
+ acceptanceCriteria: true
153
+ }
154
+ });
155
+ }
156
+ return facts.map(fact => ({
157
+ id: fact.id,
158
+ content: fact.content,
159
+ strictness: fact.strictness,
160
+ type: fact.type,
161
+ category: fact.category,
162
+ minVersion: fact.minVersion,
163
+ maxVersion: fact.maxVersion,
164
+ conditions: fact.conditions.map(c => ({
165
+ factId: c.targetId,
166
+ type: c.type
167
+ })),
168
+ acceptanceCriteria: fact.acceptanceCriteria.map(ac => ({
169
+ id: ac.id,
170
+ description: ac.description,
171
+ validationType: ac.validationType,
172
+ validationScript: ac.validationScript || undefined
173
+ })),
174
+ createdAt: fact.createdAt.toISOString(),
175
+ updatedAt: fact.updatedAt.toISOString(),
176
+ applicable: fact.applicable
177
+ }));
178
+ }
179
+ async deleteFact(id) {
180
+ try {
181
+ await this.prisma.fact.delete({
182
+ where: { id }
183
+ });
184
+ return true;
185
+ }
186
+ catch (error) {
187
+ return false;
188
+ }
189
+ }
190
+ async close() {
191
+ await this.prisma.$disconnect();
192
+ }
193
+ }