@rsconcept/rstool-mcp 0.1.6 → 0.3.1

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/src/server.ts CHANGED
@@ -1,16 +1,18 @@
1
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import {
3
3
  CallToolRequestSchema,
4
- ListToolsRequestSchema
5
- } from '@modelcontextprotocol/sdk/types.js';
4
+ ListToolsRequestSchema,
5
+ } from "@modelcontextprotocol/sdk/types.js";
6
6
 
7
- import { RSToolAgent } from '@rsconcept/rstool';
7
+ import { RSToolAgent } from "@rsconcept/rstool";
8
8
 
9
- import { TOOL_DEFINITIONS, type ToolDefinition } from './tools';
9
+ import { TOOL_DEFINITIONS, type ToolDefinition } from "./tools";
10
10
 
11
11
  export interface BuildRSToolMcpServerOptions {
12
12
  /** Pre-built RSToolAgent. Defaults to a fresh in-memory instance. */
13
13
  tool?: RSToolAgent;
14
+ /** Persist sessions to this directory (also reads RSTOOL_PERSISTENCE_DIR when set). */
15
+ persistenceDir?: string;
14
16
  /** Server name advertised to MCP clients. */
15
17
  name?: string;
16
18
  /** Server version advertised to MCP clients. */
@@ -33,59 +35,68 @@ export interface BuildRSToolMcpServerOptions {
33
35
  * await server.connect(new StdioServerTransport());
34
36
  * ```
35
37
  */
36
- export function buildRSToolMcpServer(options: BuildRSToolMcpServerOptions = {}): McpServer {
37
- const tool = options.tool ?? new RSToolAgent();
38
+ export function buildRSToolMcpServer(
39
+ options: BuildRSToolMcpServerOptions = {},
40
+ ): McpServer {
41
+ const persistenceDir =
42
+ options.persistenceDir ?? process.env.RSTOOL_PERSISTENCE_DIR;
43
+ const tool =
44
+ options.tool ??
45
+ new RSToolAgent(persistenceDir ? { persistenceDir } : undefined);
38
46
  const server = new McpServer(
39
47
  {
40
- name: options.name ?? 'rstool-mcp',
41
- version: options.version ?? '0.1.0'
48
+ name: options.name ?? "rstool-mcp",
49
+ version: options.version ?? "0.1.0",
42
50
  },
43
51
  {
44
52
  capabilities: {
45
- tools: {}
46
- }
47
- }
53
+ tools: {},
54
+ },
55
+ },
48
56
  );
49
57
 
50
58
  const definitionByName = new Map<string, ToolDefinition>(
51
- TOOL_DEFINITIONS.map(definition => [definition.name, definition])
59
+ TOOL_DEFINITIONS.map((definition) => [definition.name, definition]),
52
60
  );
53
61
 
54
62
  server.server.setRequestHandler(ListToolsRequestSchema, () =>
55
63
  Promise.resolve({
56
- tools: TOOL_DEFINITIONS.map(definition => ({
64
+ tools: TOOL_DEFINITIONS.map((definition) => ({
57
65
  name: definition.name,
58
66
  description: definition.description,
59
- inputSchema: definition.inputSchema
60
- }))
61
- })
67
+ inputSchema: definition.inputSchema,
68
+ })),
69
+ }),
62
70
  );
63
71
 
64
- server.server.setRequestHandler(CallToolRequestSchema, async request => {
72
+ server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
65
73
  const definition = definitionByName.get(request.params.name);
66
74
  if (!definition) {
67
75
  return {
68
76
  isError: true,
69
77
  content: [
70
78
  {
71
- type: 'text' as const,
72
- text: `Unknown rstool tool: ${request.params.name}`
73
- }
74
- ]
79
+ type: "text" as const,
80
+ text: `Unknown rstool tool: ${request.params.name}`,
81
+ },
82
+ ],
75
83
  };
76
84
  }
77
85
 
78
86
  try {
79
- const result = await definition.invoke(tool, request.params.arguments ?? {});
87
+ const result = await definition.invoke(
88
+ tool,
89
+ request.params.arguments ?? {},
90
+ );
80
91
  const text =
81
- typeof result === 'string' ? result : JSON.stringify(result, null, 2);
92
+ typeof result === "string" ? result : JSON.stringify(result, null, 2);
82
93
  return {
83
94
  content: [
84
95
  {
85
- type: 'text' as const,
86
- text
87
- }
88
- ]
96
+ type: "text" as const,
97
+ text,
98
+ },
99
+ ],
89
100
  };
90
101
  } catch (error) {
91
102
  const message = error instanceof Error ? error.message : String(error);
@@ -93,10 +104,10 @@ export function buildRSToolMcpServer(options: BuildRSToolMcpServerOptions = {}):
93
104
  isError: true,
94
105
  content: [
95
106
  {
96
- type: 'text' as const,
97
- text: `rstool error in ${definition.name}: ${message}`
98
- }
99
- ]
107
+ type: "text" as const,
108
+ text: `rstool error in ${definition.name}: ${message}`,
109
+ },
110
+ ],
100
111
  };
101
112
  }
102
113
  });
package/src/tools.ts CHANGED
@@ -1,286 +1,350 @@
1
1
  /**
2
2
  * MCP tool definitions for the @rsconcept/rstool contract.
3
- *
4
- * Schemas are intentionally permissive (additionalProperties: true) where the rstool input
5
- * is structurally rich (e.g. ConstituentaDraft, SessionState). The wrapped RSToolAgent
6
- * validates inputs at runtime and returns deterministic error responses.
7
3
  */
8
4
 
9
- import { type RSToolAgent } from '@rsconcept/rstool';
5
+ import { type RSToolAgent } from "@rsconcept/rstool";
10
6
 
11
7
  export interface ToolDefinition {
12
8
  name: string;
13
9
  description: string;
14
10
  inputSchema: {
15
- type: 'object';
11
+ type: "object";
16
12
  properties: Record<string, unknown>;
17
13
  required?: string[];
18
14
  additionalProperties?: boolean;
19
15
  };
20
- invoke: (tool: RSToolAgent, args: Record<string, unknown>) => unknown | Promise<unknown>;
16
+ invoke: (
17
+ tool: RSToolAgent,
18
+ args: Record<string, unknown>,
19
+ ) => unknown | Promise<unknown>;
21
20
  }
22
21
 
23
22
  const sessionId = {
24
- type: 'string',
25
- description: 'Session identifier returned from create_session or import_session.'
23
+ type: "string",
24
+ description: "Session id. Omit to use the current active session.",
26
25
  };
27
26
 
28
- export const TOOL_DEFINITIONS: ToolDefinition[] = [
29
- {
30
- name: 'ping',
31
- description: 'Liveness check; returns {pong: true} and the active rstool contract version.',
32
- inputSchema: { type: 'object', properties: {}, additionalProperties: false },
33
- invoke: tool => ({ pong: true, contractVersion: tool.contractVersion })
27
+ const agentPatchSchema = {
28
+ type: "object",
29
+ description:
30
+ "Agent-friendly constituent patch. id and cstType are optional; cstType is inferred from alias prefixes X/C/S/D/A/F/P.",
31
+ properties: {
32
+ id: { type: "number" },
33
+ alias: { type: "string" },
34
+ cstType: { type: "string" },
35
+ definitionFormal: { type: "string" },
36
+ term: { type: "string" },
37
+ definitionText: { type: "string" },
38
+ convention: { type: "string" },
34
39
  },
40
+ required: ["alias"],
41
+ };
42
+
43
+ function optionalSessionId(args: Record<string, unknown>): string | undefined {
44
+ const value = args.sessionId;
45
+ return typeof value === "string" && value.length > 0 ? value : undefined;
46
+ }
47
+
48
+ function omitSessionId(args: Record<string, unknown>): Record<string, unknown> {
49
+ const { sessionId: _sessionId, ...rest } = args;
50
+ return rest;
51
+ }
52
+
53
+ export const TOOL_DEFINITIONS: ToolDefinition[] = [
35
54
  {
36
- name: 'list_methods',
37
- description: 'List all rstool methods exposed as MCP tools.',
38
- inputSchema: { type: 'object', properties: {}, additionalProperties: false },
39
- invoke: () => TOOL_DEFINITIONS.map(definition => definition.name)
55
+ name: "ping",
56
+ description:
57
+ "Liveness check; returns {pong: true} and the active rstool contract version.",
58
+ inputSchema: {
59
+ type: "object",
60
+ properties: {},
61
+ additionalProperties: false,
62
+ },
63
+ invoke: (tool) => ({ pong: true, contractVersion: tool.contractVersion }),
40
64
  },
41
65
  {
42
- name: 'create_session',
43
- description:
44
- 'Create a fresh in-memory rstool session. Returns a SessionHandle with sessionId and initial revision.',
66
+ name: "list_methods",
67
+ description: "List all rstool methods exposed as MCP tools.",
45
68
  inputSchema: {
46
- type: 'object',
47
- properties: {
48
- initial: {
49
- type: 'object',
50
- description: 'Optional partial SessionState seed (alias, title, constituents, etc.).',
51
- additionalProperties: true
52
- }
53
- },
54
- additionalProperties: false
69
+ type: "object",
70
+ properties: {},
71
+ additionalProperties: false,
55
72
  },
56
- invoke: (tool, args) => tool.createSession(args.initial as never)
73
+ invoke: () => TOOL_DEFINITIONS.map((definition) => definition.name),
57
74
  },
58
75
  {
59
- name: 'add_or_update_constituenta',
76
+ name: "ensure_session",
60
77
  description:
61
- 'Upsert a single constituent in the session. Provide alias, cstType, definitionFormal (may be ""), and optional term/definitionText/convention.',
78
+ "Return the current active session, or create one if none exists. Optional initial seed is used only when creating.",
62
79
  inputSchema: {
63
- type: 'object',
80
+ type: "object",
64
81
  properties: {
65
- sessionId,
66
- input: {
67
- type: 'object',
68
- description: 'AddOrUpdateConstituentaInput. Required keys: alias, cstType, definitionFormal.',
69
- additionalProperties: true
70
- }
82
+ initial: {
83
+ type: "object",
84
+ properties: {
85
+ alias: { type: "string" },
86
+ title: { type: "string" },
87
+ comment: { type: "string" },
88
+ },
89
+ additionalProperties: true,
90
+ },
71
91
  },
72
- required: ['sessionId', 'input']
92
+ additionalProperties: false,
73
93
  },
74
- invoke: (tool, args) =>
75
- tool.addOrUpdateConstituenta(String(args.sessionId), args.input as never)
94
+ invoke: (tool, args) => tool.ensureSession(args.initial as never),
76
95
  },
77
96
  {
78
- name: 'analyze_expression',
97
+ name: "create_session",
79
98
  description:
80
- 'Run parser + semantic analysis on a scratch RSLang expression in the session context. Returns typification, valueClass, success flag and diagnostics.',
99
+ "Create a fresh in-memory rstool session and set it as the current active session.",
81
100
  inputSchema: {
82
- type: 'object',
101
+ type: "object",
83
102
  properties: {
84
- sessionId,
85
- input: {
86
- type: 'object',
87
- description: 'AnalyzeExpressionInput. Required keys: expression, cstType.',
103
+ initial: {
104
+ type: "object",
105
+ description:
106
+ "Optional partial SessionState seed (alias, title, comment).",
88
107
  properties: {
89
- expression: { type: 'string' },
90
- cstType: { type: 'string' },
91
- alias: { type: 'string' }
108
+ alias: { type: "string" },
109
+ title: { type: "string" },
110
+ comment: { type: "string" },
92
111
  },
93
- required: ['expression', 'cstType'],
94
- additionalProperties: true
95
- }
112
+ additionalProperties: false,
113
+ },
96
114
  },
97
- required: ['sessionId', 'input']
115
+ additionalProperties: false,
98
116
  },
99
- invoke: (tool, args) =>
100
- tool.analyzeExpression(String(args.sessionId), args.input as never)
117
+ invoke: (tool, args) => tool.createSession(args.initial as never),
101
118
  },
102
119
  {
103
- name: 'get_form_state',
104
- description: 'Return the full SessionState (constituents, revision, alias, title, etc.).',
120
+ name: "set_current_session",
121
+ description: "Set the active session by id.",
105
122
  inputSchema: {
106
- type: 'object',
123
+ type: "object",
107
124
  properties: { sessionId },
108
- required: ['sessionId']
125
+ required: ["sessionId"],
109
126
  },
110
- invoke: (tool, args) => tool.getFormState(String(args.sessionId))
127
+ invoke: (tool, args) => tool.setCurrentSession(String(args.sessionId)),
111
128
  },
112
129
  {
113
- name: 'list_diagnostics',
130
+ name: "get_current_session",
131
+ description: "Return the current active session, or null if none exists.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: {},
135
+ additionalProperties: false,
136
+ },
137
+ invoke: (tool) => tool.getCurrentSession(),
138
+ },
139
+ {
140
+ name: "apply_schema_patch",
114
141
  description:
115
- 'List diagnostics across the session, optionally filtered by constituentId / severity / classes.',
142
+ "Preferred schema edit path. Batch patch with inferred ids and cstType, dependency ordering, and compact summary.",
116
143
  inputSchema: {
117
- type: 'object',
144
+ type: "object",
118
145
  properties: {
119
146
  sessionId,
120
- filters: {
121
- type: 'object',
122
- description: 'Optional ListDiagnosticsFilters object.',
123
- additionalProperties: true
124
- }
147
+ initial: {
148
+ type: "object",
149
+ properties: {
150
+ alias: { type: "string" },
151
+ title: { type: "string" },
152
+ comment: { type: "string" },
153
+ },
154
+ additionalProperties: true,
155
+ },
156
+ items: { type: "array", items: agentPatchSchema },
157
+ mode: { type: "string", enum: ["atomic", "best_effort"] },
158
+ commitMessage: { type: "string" },
125
159
  },
126
- required: ['sessionId']
160
+ required: ["items"],
127
161
  },
128
162
  invoke: (tool, args) =>
129
- tool.listDiagnostics(String(args.sessionId), args.filters as never)
163
+ tool.applySchemaPatch(
164
+ omitSessionId(args) as never,
165
+ optionalSessionId(args),
166
+ ),
130
167
  },
131
168
  {
132
- name: 'commit_step',
133
- description: 'Record a session revision with an optional human-readable message.',
169
+ name: "get_session_state",
170
+ description:
171
+ "Return session state. detail=summary (default): compact metadata, aliases, diagnostics. detail=full: complete SessionState clone.",
134
172
  inputSchema: {
135
- type: 'object',
173
+ type: "object",
136
174
  properties: {
137
175
  sessionId,
138
- message: { type: 'string' }
176
+ detail: { type: "string", enum: ["summary", "full"] },
139
177
  },
140
- required: ['sessionId']
141
178
  },
142
179
  invoke: (tool, args) =>
143
- tool.commitStep(String(args.sessionId), args.message as string | undefined)
180
+ tool.getSessionState(
181
+ (args.detail as "summary" | "full" | undefined) ?? "summary",
182
+ optionalSessionId(args),
183
+ ),
144
184
  },
145
185
  {
146
- name: 'export_session',
147
- description: 'Serialize the session to a JSON string suitable for import_session.',
186
+ name: "analyze_expression",
187
+ description:
188
+ "Parse and type-check a scratch expression without saving it. Does not record diagnostics unless recordDiagnostics=true.",
148
189
  inputSchema: {
149
- type: 'object',
150
- properties: { sessionId },
151
- required: ['sessionId']
190
+ type: "object",
191
+ properties: {
192
+ sessionId,
193
+ expression: { type: "string" },
194
+ cstType: { type: "string" },
195
+ recordDiagnostics: { type: "boolean" },
196
+ },
197
+ required: ["expression", "cstType"],
152
198
  },
153
- invoke: (tool, args) => tool.exportSession(String(args.sessionId))
199
+ invoke: (tool, args) =>
200
+ tool.analyzeExpression(
201
+ omitSessionId(args) as never,
202
+ optionalSessionId(args),
203
+ ),
154
204
  },
155
205
  {
156
- name: 'import_session',
206
+ name: "list_diagnostics",
157
207
  description:
158
- 'Import a session previously produced by export_session. Returns a fresh SessionHandle pointing at the restored state.',
208
+ "List active diagnostics for the session (one record set per constituent, not a historical log).",
159
209
  inputSchema: {
160
- type: 'object',
210
+ type: "object",
161
211
  properties: {
162
- payload: { type: 'string', description: 'JSON payload from export_session.' }
212
+ sessionId,
213
+ constituentId: { type: "number" },
163
214
  },
164
- required: ['payload']
165
215
  },
166
- invoke: (tool, args) => tool.importSession(String(args.payload))
216
+ invoke: (tool, args) => {
217
+ const constituentId = args.constituentId;
218
+ const filters =
219
+ typeof constituentId === "number" ? { constituentId } : undefined;
220
+ return tool.listDiagnostics(filters, optionalSessionId(args));
221
+ },
167
222
  },
168
223
  {
169
- name: 'set_constituenta_value',
224
+ name: "commit_step",
170
225
  description:
171
- 'Bind or assign a value to a single interpretable constituent (basic, constant, structure). Inferrable constituents (term, axiom, statement) cannot be set directly.',
226
+ "Record a session revision with an optional human-readable message.",
172
227
  inputSchema: {
173
- type: 'object',
228
+ type: "object",
174
229
  properties: {
175
230
  sessionId,
176
- input: {
177
- type: 'object',
178
- description: 'SetConstituentaValueInput. Required keys: target (id), value.',
179
- additionalProperties: true
180
- }
231
+ message: { type: "string" },
181
232
  },
182
- required: ['sessionId', 'input']
183
233
  },
184
234
  invoke: (tool, args) =>
185
- tool.setConstituentaValue(String(args.sessionId), args.input as never)
235
+ tool.commitStep(
236
+ args.message as string | undefined,
237
+ optionalSessionId(args),
238
+ ),
186
239
  },
187
240
  {
188
- name: 'set_constituenta_values',
189
- description: 'Batch variant of set_constituenta_value: assign values to multiple interpretable constituents.',
241
+ name: "export_session",
242
+ description:
243
+ "Serialize the session to a JSON string suitable for import_data with kind=session.",
190
244
  inputSchema: {
191
- type: 'object',
192
- properties: {
193
- sessionId,
194
- input: {
195
- type: 'object',
196
- description: 'SetConstituentaValuesInput. Required keys: values (list of {target, value}).',
197
- additionalProperties: true
198
- }
199
- },
200
- required: ['sessionId', 'input']
245
+ type: "object",
246
+ properties: { sessionId },
201
247
  },
202
- invoke: (tool, args) =>
203
- tool.setConstituentaValues(String(args.sessionId), args.input as never)
248
+ invoke: (tool, args) => tool.exportSession(optionalSessionId(args)),
204
249
  },
205
250
  {
206
- name: 'clear_constituenta_values',
207
- description: 'Clear current values for one or more constituents and reset dependents to NOT_PROCESSED.',
251
+ name: "export_portal",
252
+ description:
253
+ "Export Portal Load-from-JSON payload. kind=schema|model; format=json (default string) or object.",
208
254
  inputSchema: {
209
- type: 'object',
255
+ type: "object",
210
256
  properties: {
211
257
  sessionId,
212
- input: {
213
- type: 'object',
214
- description: 'ClearConstituentaValuesInput. Required keys: targets (list of ids).',
215
- additionalProperties: true
216
- }
258
+ kind: { type: "string", enum: ["schema", "model"] },
259
+ format: { type: "string", enum: ["json", "object"] },
217
260
  },
218
- required: ['sessionId', 'input']
261
+ required: ["kind"],
219
262
  },
220
263
  invoke: (tool, args) =>
221
- tool.clearConstituentaValues(String(args.sessionId), args.input as never)
264
+ tool.exportPortal(omitSessionId(args) as never, optionalSessionId(args)),
222
265
  },
223
266
  {
224
- name: 'get_model_state',
225
- description: 'Return the SessionModelState — current values and evaluation statuses for every constituent.',
267
+ name: "import_data",
268
+ description:
269
+ "Import a session from export_session JSON, Portal schema JSON, or GET /api/rsforms/:id/details. kind=auto (default) detects the shape.",
226
270
  inputSchema: {
227
- type: 'object',
228
- properties: { sessionId },
229
- required: ['sessionId']
271
+ type: "object",
272
+ properties: {
273
+ payload: {
274
+ description:
275
+ "JSON string or parsed object (session export, Portal schema, or rsform details).",
276
+ },
277
+ kind: {
278
+ type: "string",
279
+ enum: ["auto", "session", "portal-schema", "portal-details"],
280
+ },
281
+ },
282
+ required: ["payload"],
230
283
  },
231
- invoke: (tool, args) => tool.getModelState(String(args.sessionId))
284
+ invoke: (tool, args) =>
285
+ tool.importData(args.payload as string | object, args.kind as never),
232
286
  },
233
287
  {
234
- name: 'evaluate_expression',
288
+ name: "set_model_values",
235
289
  description:
236
- 'Evaluate a scratch RSLang expression against the current bindings without storing it. Returns value + evaluation status.',
290
+ "Set and/or clear interpretable model bindings. Pass set[] for bindings and clear[] for constituent ids to reset.",
237
291
  inputSchema: {
238
- type: 'object',
292
+ type: "object",
239
293
  properties: {
240
294
  sessionId,
241
- input: {
242
- type: 'object',
243
- description: 'EvaluateExpressionInput. Required keys: expression. Optional: cstType, alias.',
244
- properties: {
245
- expression: { type: 'string' },
246
- cstType: { type: 'string' },
247
- alias: { type: 'string' }
295
+ set: {
296
+ type: "array",
297
+ items: {
298
+ type: "object",
299
+ properties: {
300
+ target: { type: "number" },
301
+ type: { type: "string" },
302
+ value: {},
303
+ },
304
+ required: ["target", "value"],
248
305
  },
249
- required: ['expression'],
250
- additionalProperties: true
251
- }
306
+ },
307
+ clear: { type: "array", items: { type: "number" } },
252
308
  },
253
- required: ['sessionId', 'input']
254
309
  },
255
310
  invoke: (tool, args) =>
256
- tool.evaluateExpression(String(args.sessionId), args.input as never)
311
+ tool.setModelValues(
312
+ omitSessionId(args) as never,
313
+ optionalSessionId(args),
314
+ ),
257
315
  },
258
316
  {
259
- name: 'evaluate_constituenta',
260
- description: 'Evaluate a stored derived constituent (term, axiom, statement) using the current bindings.',
317
+ name: "get_model_state",
318
+ description: "Return the SessionModelState.",
261
319
  inputSchema: {
262
- type: 'object',
320
+ type: "object",
321
+ properties: { sessionId },
322
+ },
323
+ invoke: (tool, args) => tool.getModelState(optionalSessionId(args)),
324
+ },
325
+ {
326
+ name: "evaluate",
327
+ description:
328
+ "Evaluate a scratch expression (expression + cstType) or a stored constituent (constituentId).",
329
+ inputSchema: {
330
+ type: "object",
263
331
  properties: {
264
332
  sessionId,
265
- input: {
266
- type: 'object',
267
- description: 'EvaluateConstituentaInput. Required keys: target (id).',
268
- additionalProperties: true
269
- }
333
+ expression: { type: "string" },
334
+ cstType: { type: "string" },
335
+ constituentId: { type: "number" },
270
336
  },
271
- required: ['sessionId', 'input']
272
337
  },
273
338
  invoke: (tool, args) =>
274
- tool.evaluateConstituenta(String(args.sessionId), args.input as never)
339
+ tool.evaluate(omitSessionId(args) as never, optionalSessionId(args)),
275
340
  },
276
341
  {
277
- name: 'recalculate_model',
278
- description: 'Recompute every interpretable / inferrable constituent using current bindings. Returns the updated SessionModelState.',
342
+ name: "recalculate_model",
343
+ description: "Recompute every inferrable constituent.",
279
344
  inputSchema: {
280
- type: 'object',
345
+ type: "object",
281
346
  properties: { sessionId },
282
- required: ['sessionId']
283
347
  },
284
- invoke: (tool, args) => tool.recalculateModel(String(args.sessionId))
285
- }
348
+ invoke: (tool, args) => tool.recalculateModel(optionalSessionId(args)),
349
+ },
286
350
  ];