jevx-mcp 0.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 ADDED
@@ -0,0 +1,5 @@
1
+ MIT License
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Jevx MCP
2
+
3
+ Run **[Jev AI](https://jevx.org)** typed decisions from any MCP client — Claude, Cursor, Cline, Codex. Give it a state and the questions you branch on, get back a choice, a score or a yes/no, with a calibrated probability on every option.
4
+
5
+ Jev AI is a zero-shot decision model from TypeSafe: text or JSON in, typed values out — no dataset, fine-tune or retrain when the label set changes. Jevx is an independent playground and API for Jev AI and is not affiliated with TypeSafe AI.
6
+
7
+ ## Tools
8
+
9
+ | Tool | What it does |
10
+ |---|---|
11
+ | `decide` | Send a state and up to 16 typed questions, get back choices, scores and noul values with probabilities |
12
+ | `validate_questions` | Check a question's shape locally, free and without an API key, before spending credits on a malformed request |
13
+ | `open_in_jevx` | The URL to try or continue a decision in the browser playground |
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npx jevx-mcp
19
+ ```
20
+
21
+ ### Claude Desktop / Claude Code
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "jevx": {
27
+ "command": "npx",
28
+ "args": ["-y", "jevx-mcp"],
29
+ "env": { "JEVX_API_KEY": "sk-..." }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ Create a key at [jevx.org/settings/apikeys](https://jevx.org/settings/apikeys?utm_source=mcp). There is no anonymous decisions endpoint — a key is required, same as running the playground signed out.
36
+
37
+ ### Cursor / Cline
38
+
39
+ Point the client at `npx -y jevx-mcp` (stdio transport) and set `JEVX_API_KEY` in its environment.
40
+
41
+ ## Question shapes
42
+
43
+ | type | criteria | returns |
44
+ |---|---|---|
45
+ | `choice` | object of option to meaning, two or more options | `choice`, `probabilities`, `confidence` |
46
+ | `score` | ordered array, low to high | `score`, `probabilities`, `confidence` |
47
+ | `noul` | exactly the keys `true` and `false` | value between 0 and 1, where exactly `0.5` means the model declined |
48
+
49
+ The API answers HTTP 200 with a non-zero `code` on a refusal (bad key, malformed request); `decide` and `validate_questions` both surface that as a normal tool error, not a silent success.
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,53 @@
1
+ export type ChoiceQuestion = {
2
+ type: 'choice';
3
+ instructions: string;
4
+ criteria: Record<string, string>;
5
+ };
6
+ export type NoulQuestion = {
7
+ type: 'noul';
8
+ instructions: string;
9
+ criteria: {
10
+ true: string;
11
+ false: string;
12
+ };
13
+ };
14
+ export type ScoreQuestion = {
15
+ type: 'score';
16
+ instructions: string;
17
+ criteria: string[];
18
+ };
19
+ export type Question = ChoiceQuestion | NoulQuestion | ScoreQuestion;
20
+ export type Answer = {
21
+ type: 'choice';
22
+ choice: string;
23
+ probabilities: Record<string, number>;
24
+ confidence: number;
25
+ } | {
26
+ type: 'noul';
27
+ noul: number;
28
+ } | {
29
+ type: 'score';
30
+ score: number;
31
+ legend?: Record<string, string>;
32
+ probabilities: Record<string, number>;
33
+ confidence: number;
34
+ };
35
+ export type DecisionResult = {
36
+ answers: Record<string, Answer>;
37
+ model: string;
38
+ id: string;
39
+ charged: number;
40
+ usage: {
41
+ input_tokens: number;
42
+ output_tokens: number;
43
+ cost: number;
44
+ };
45
+ };
46
+ export declare class JevxError extends Error {
47
+ }
48
+ export declare const credentialsHint = "Set JEVX_API_KEY to run this \u2014 create one at https://jevx.org/settings/apikeys. There is no anonymous decisions endpoint: an account is required, same as running the playground signed out.";
49
+ export declare function hasApiKey(): boolean;
50
+ /** Local shape check, the same rules the API enforces. Returns problems, empty when valid. */
51
+ export declare function validateQuestions(questions: Record<string, unknown>): string[];
52
+ /** Run one typed decision against jevx.org. Throws JevxError on a bad key or an API refusal. */
53
+ export declare function decide(state: string | Record<string, unknown> | unknown[], questions: Record<string, Question>, model?: string): Promise<DecisionResult>;
package/dist/client.js ADDED
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The whole of what this server needs from jevx.org: one POST. Zero
3
+ * dependencies — plain fetch against the same endpoint the site's own
4
+ * playground uses, so an MCP run is priced and charged exactly like a run on
5
+ * the site.
6
+ */
7
+ const BASE_URL = (process.env.JEVX_BASE_URL?.replace(/\/$/, '')) || 'https://jevx.org';
8
+ export class JevxError extends Error {
9
+ }
10
+ export const credentialsHint = 'Set JEVX_API_KEY to run this — create one at https://jevx.org/settings/apikeys. There is no anonymous decisions endpoint: an account is required, same as running the playground signed out.';
11
+ export function hasApiKey() {
12
+ return Boolean(process.env.JEVX_API_KEY?.trim());
13
+ }
14
+ /** Local shape check, the same rules the API enforces. Returns problems, empty when valid. */
15
+ export function validateQuestions(questions) {
16
+ const errors = [];
17
+ const names = Object.keys(questions ?? {});
18
+ if (!names.length)
19
+ errors.push('questions: add at least one question');
20
+ if (names.length > 16)
21
+ errors.push(`questions: ${names.length} is over the limit of 16 per request`);
22
+ for (const name of names) {
23
+ const q = questions[name];
24
+ const p = `questions.${name}`;
25
+ if (!q || typeof q !== 'object') {
26
+ errors.push(`${p}: expected an object`);
27
+ continue;
28
+ }
29
+ if (q.type !== 'choice' && q.type !== 'noul' && q.type !== 'score') {
30
+ errors.push(`${p}.type: expected choice | noul | score`);
31
+ continue;
32
+ }
33
+ if (typeof q.instructions !== 'string' || !q.instructions.trim())
34
+ errors.push(`${p}.instructions: required text`);
35
+ const c = q.criteria;
36
+ if (c === undefined || c === null) {
37
+ errors.push(`${p}.criteria: required`);
38
+ }
39
+ else if (q.type === 'score') {
40
+ if (!Array.isArray(c) || c.length < 2)
41
+ errors.push(`${p}.criteria: expected an array of two or more steps, low to high`);
42
+ }
43
+ else if (typeof c !== 'object' || Array.isArray(c)) {
44
+ errors.push(`${p}.criteria: expected an object`);
45
+ }
46
+ else {
47
+ const keys = Object.keys(c);
48
+ if (q.type === 'noul') {
49
+ if (keys.length !== 2 || !keys.includes('true') || !keys.includes('false')) {
50
+ errors.push(`${p}.criteria: noul takes exactly the keys "true" and "false" (writing yes/no is the common mistake)`);
51
+ }
52
+ }
53
+ else if (keys.length < 2) {
54
+ errors.push(`${p}.criteria: choice needs two or more options`);
55
+ }
56
+ }
57
+ }
58
+ return errors;
59
+ }
60
+ /** Run one typed decision against jevx.org. Throws JevxError on a bad key or an API refusal. */
61
+ export async function decide(state, questions, model) {
62
+ const key = process.env.JEVX_API_KEY?.trim();
63
+ if (!key)
64
+ throw new JevxError(credentialsHint);
65
+ const res = await fetch(`${BASE_URL}/api/decisions/run`, {
66
+ method: 'POST',
67
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${key}` },
68
+ body: JSON.stringify({ state, questions, ...(model ? { model } : {}) }),
69
+ });
70
+ const text = await res.text();
71
+ let json;
72
+ try {
73
+ json = JSON.parse(text);
74
+ }
75
+ catch {
76
+ throw new JevxError(`jevx.org returned non-JSON (HTTP ${res.status})`);
77
+ }
78
+ // The API answers HTTP 200 with `code: -1` for refusals (bad key, malformed
79
+ // request). Reading the status code alone reports success on a failure.
80
+ if (json.code !== 0)
81
+ throw new JevxError(json.message || `request failed (HTTP ${res.status})`);
82
+ if (!json.data || json.data.error)
83
+ throw new JevxError(json.data?.error || 'empty response');
84
+ return json.data;
85
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * jevx-mcp — run Jev AI typed decisions from any MCP client.
4
+ *
5
+ * Every call goes through jevx.org's public decisions API with the caller's
6
+ * own API key, so a run costs the same credits and is billed exactly like a
7
+ * run made in the Jevx playground.
8
+ */
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
+ import { z } from 'zod';
12
+ import { credentialsHint, decide, hasApiKey, JevxError, validateQuestions, } from './client.js';
13
+ const server = new McpServer({ name: 'jevx-mcp', version: '0.1.0' });
14
+ const text = (body) => ({ content: [{ type: 'text', text: body }] });
15
+ const fail = (body) => ({ ...text(body), isError: true });
16
+ const questionSchema = z
17
+ .record(z.string(), z.object({
18
+ type: z.enum(['choice', 'noul', 'score']).describe('choice = pick one of several options; noul = yes/no with exactly the keys true and false; score = one step on an ordered scale'),
19
+ instructions: z.string().describe('Plain-language instructions for this one question.'),
20
+ criteria: z
21
+ .union([z.record(z.string(), z.string()), z.array(z.string())])
22
+ .describe('choice: object of option to meaning (2+ options). noul: object with exactly the keys "true" and "false". score: ordered array, low to high.'),
23
+ }))
24
+ .describe('Up to 16 named questions, all judged against the same state.');
25
+ server.registerTool('validate_questions', {
26
+ title: 'Check a Jev AI question shape',
27
+ description: 'Run the same shape checks the Jev API enforces, without spending credits or needing an API key. Use this first when writing questions by hand — a malformed question otherwise comes back as a deeply nested validation error.',
28
+ inputSchema: { questions: questionSchema },
29
+ }, async ({ questions }) => {
30
+ const problems = validateQuestions(questions);
31
+ if (!problems.length)
32
+ return text('No shape problems found. Every question has a type, instructions and criteria in the shape the API accepts.');
33
+ return fail(`${problems.length} problem(s):\n` + problems.map((p) => `- ${p}`).join('\n'));
34
+ });
35
+ server.registerTool('decide', {
36
+ title: 'Run a Jev AI decision',
37
+ description: 'Send a state (text or JSON) and up to 16 typed questions to Jev AI on jevx.org, and get back the choice, score or yes/no for each — with a calibrated probability on every option. Needs JEVX_API_KEY. Branch your own code on the returned probabilities, not just the top label; a noul of exactly 0.5 means the model declined to answer.',
38
+ inputSchema: {
39
+ state: z.union([z.string(), z.record(z.string(), z.unknown()), z.array(z.unknown())]).describe('The thing being judged: plain text, or a JSON object/array.'),
40
+ questions: questionSchema,
41
+ model: z.string().optional().describe('Pin a dated model id for reproducible answers; omit to use the floating alias.'),
42
+ },
43
+ }, async ({ state, questions, model }) => {
44
+ if (!hasApiKey())
45
+ return fail(credentialsHint);
46
+ const problems = validateQuestions(questions);
47
+ if (problems.length)
48
+ return fail(`Fix the question shape first:\n` + problems.map((p) => `- ${p}`).join('\n'));
49
+ try {
50
+ const result = await decide(state, questions, model);
51
+ const lines = Object.entries(result.answers).map(([name, a]) => {
52
+ if (a.type === 'choice')
53
+ return `- ${name}: ${a.choice} (confidence ${a.confidence}) — probabilities ${JSON.stringify(a.probabilities)}`;
54
+ if (a.type === 'score')
55
+ return `- ${name}: ${a.score} (confidence ${a.confidence}) — probabilities ${JSON.stringify(a.probabilities)}`;
56
+ return `- ${name}: ${a.noul}${a.noul === 0.5 ? ' (declined to answer)' : ''}`;
57
+ });
58
+ return text(`Model: ${result.model}\nCharged: ${result.charged} credits\n\n${lines.join('\n')}\n\nRequest id: ${result.id}`);
59
+ }
60
+ catch (e) {
61
+ const message = e instanceof JevxError ? e.message : String(e);
62
+ return fail(`Decision refused: ${message}`);
63
+ }
64
+ });
65
+ server.registerTool('open_in_jevx', {
66
+ title: 'Open the Jevx playground',
67
+ description: 'The URL to try or continue a decision in the browser playground.',
68
+ inputSchema: {},
69
+ }, async () => text('https://jevx.org/?utm_source=mcp'));
70
+ const transport = new StdioServerTransport();
71
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "jevx-mcp",
3
+ "version": "0.1.0",
4
+ "mcpName": "org.jevx/jevx-mcp",
5
+ "description": "MCP server for Jevx (jevx.org) — run Jev AI typed decisions (choice, score, noul with probabilities) from any MCP client, on your own Jevx API key.",
6
+ "keywords": ["mcp", "model-context-protocol", "jev-ai", "decisions", "classification", "zero-shot", "typesafe", "jevx"],
7
+ "homepage": "https://jevx.org",
8
+ "bugs": { "url": "https://github.com/hanshs474/jevx-mcp/issues" },
9
+ "repository": { "type": "git", "url": "git+https://github.com/hanshs474/jevx-mcp.git" },
10
+ "license": "MIT",
11
+ "author": "Jevx (https://jevx.org)",
12
+ "type": "module",
13
+ "bin": { "jevx-mcp": "dist/index.js" },
14
+ "files": ["dist", "README.md", "LICENSE"],
15
+ "scripts": { "build": "tsc && chmod +x dist/index.js", "start": "node dist/index.js", "prepublishOnly": "npm run build", "test": "node --test test/decide.test.js" },
16
+ "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.23.8" },
17
+ "devDependencies": { "@types/node": "^22.0.0", "typescript": "^5.6.0" },
18
+ "engines": { "node": ">=18" }
19
+ }