Repository navigation
Expand file tree
/
Copy pathserver.js
More file actions
95 lines (81 loc) 路 3.52 KB
/
Copy pathserver.js
File metadata and controls
95 lines (81 loc) 路 3.52 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import { pathToFileURL } from "node:url";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const REQUEST_TIMEOUT_MILLISECONDS = 60_000;
const MAXIMUM_CHOICE_OPTIONS = 255;
const MAXIMUM_SCORE_LEVELS = 10;
const contentSchema = z.union([z.string(), z.record(z.unknown()), z.array(z.unknown())]);
const questionSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("noul"),
instructions: contentSchema,
criteria: z.object({ true: contentSchema.optional(), false: contentSchema.optional() }).optional(),
}),
z.object({
type: z.literal("choice"),
instructions: contentSchema,
criteria: z.record(contentSchema.nullable()).refine(
(options) => {
const optionCount = Object.keys(options).length;
return optionCount >= 1 && optionCount <= MAXIMUM_CHOICE_OPTIONS;
},
`Choice requires 1 to ${MAXIMUM_CHOICE_OPTIONS} options.`,
),
}),
z.object({
type: z.literal("score"),
instructions: contentSchema,
criteria: z.array(contentSchema).min(2).max(MAXIMUM_SCORE_LEVELS),
}),
]);
export function createServer(apiKey, sendRequest = fetch) {
if (!apiKey?.trim()) {
throw new Error("Set TYPESAFE_API_KEY before starting the server.");
}
const server = new McpServer({ name: "typesafe", version: "0.1.0" });
server.registerTool("evaluate", {
description: "Evaluate evidence with TypeSafe's Jev model. Batch independent questions over the same state. " +
"Use noul for yes/no probability, choice for one named option, or score for ordered levels. " +
"Write complete questions in instructions; question IDs are not sent to the model. " +
"Supply evidence, not your verdict. Answers are judgements, not proof.",
inputSchema: {
state: contentSchema.describe("Text or structured evidence to evaluate. Encode large integer IDs as strings."),
questions: z.record(questionSchema).refine((questions) => Object.keys(questions).length > 0, "Provide at least one question."),
model: z.string().min(1).default("jev-latest"),
},
annotations: { readOnlyHint: true, openWorldHint: true },
}, async (input, { signal }) => {
try {
const response = await sendRequest("https://api.typesafe.ai/v1/systemone", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(input),
signal: AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MILLISECONDS)]),
redirect: "error",
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`TypeSafe returned HTTP ${response.status}.`);
}
const result = await response.json();
for (const [id, question] of Object.entries(input.questions)) {
if (result?.answers?.[id]?.type !== question.type) {
throw new Error("TypeSafe returned a missing or mismatched answer.");
}
}
return { content: [{ type: "text", text: JSON.stringify(result) }] };
} catch (error) {
return { isError: true, content: [{ type: "text", text: String(error.message).replaceAll(apiKey, "[redacted]") }] };
}
});
return server;
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
await createServer(process.env.TYPESAFE_API_KEY).connect(new StdioServerTransport());
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}