@rsconcept/rstool-mcp 0.1.6 → 0.3.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/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,340 @@
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: "apply_schema_patch",
114
131
  description:
115
- 'List diagnostics across the session, optionally filtered by constituentId / severity / classes.',
132
+ "Preferred schema edit path. Batch patch with inferred ids and cstType, dependency ordering, and compact summary.",
116
133
  inputSchema: {
117
- type: 'object',
134
+ type: "object",
118
135
  properties: {
119
136
  sessionId,
120
- filters: {
121
- type: 'object',
122
- description: 'Optional ListDiagnosticsFilters object.',
123
- additionalProperties: true
124
- }
137
+ initial: {
138
+ type: "object",
139
+ properties: {
140
+ alias: { type: "string" },
141
+ title: { type: "string" },
142
+ comment: { type: "string" },
143
+ },
144
+ additionalProperties: true,
145
+ },
146
+ items: { type: "array", items: agentPatchSchema },
147
+ mode: { type: "string", enum: ["atomic", "best_effort"] },
148
+ commitMessage: { type: "string" },
125
149
  },
126
- required: ['sessionId']
150
+ required: ["items"],
127
151
  },
128
152
  invoke: (tool, args) =>
129
- tool.listDiagnostics(String(args.sessionId), args.filters as never)
153
+ tool.applySchemaPatch(
154
+ omitSessionId(args) as never,
155
+ optionalSessionId(args),
156
+ ),
130
157
  },
131
158
  {
132
- name: 'commit_step',
133
- description: 'Record a session revision with an optional human-readable message.',
159
+ name: "get_session_state",
160
+ description:
161
+ "Return session state. detail=summary (default): compact metadata, aliases, diagnostics. detail=full: complete SessionState clone.",
134
162
  inputSchema: {
135
- type: 'object',
163
+ type: "object",
136
164
  properties: {
137
165
  sessionId,
138
- message: { type: 'string' }
166
+ detail: { type: "string", enum: ["summary", "full"] },
139
167
  },
140
- required: ['sessionId']
141
168
  },
142
169
  invoke: (tool, args) =>
143
- tool.commitStep(String(args.sessionId), args.message as string | undefined)
170
+ tool.getSessionState(
171
+ (args.detail as "summary" | "full" | undefined) ?? "summary",
172
+ optionalSessionId(args),
173
+ ),
144
174
  },
145
175
  {
146
- name: 'export_session',
147
- description: 'Serialize the session to a JSON string suitable for import_session.',
176
+ name: "analyze_expression",
177
+ description:
178
+ "Parse and type-check a scratch expression without saving it. Does not record diagnostics unless recordDiagnostics=true.",
148
179
  inputSchema: {
149
- type: 'object',
150
- properties: { sessionId },
151
- required: ['sessionId']
180
+ type: "object",
181
+ properties: {
182
+ sessionId,
183
+ expression: { type: "string" },
184
+ cstType: { type: "string" },
185
+ recordDiagnostics: { type: "boolean" },
186
+ },
187
+ required: ["expression", "cstType"],
152
188
  },
153
- invoke: (tool, args) => tool.exportSession(String(args.sessionId))
189
+ invoke: (tool, args) =>
190
+ tool.analyzeExpression(
191
+ omitSessionId(args) as never,
192
+ optionalSessionId(args),
193
+ ),
154
194
  },
155
195
  {
156
- name: 'import_session',
196
+ name: "list_diagnostics",
157
197
  description:
158
- 'Import a session previously produced by export_session. Returns a fresh SessionHandle pointing at the restored state.',
198
+ "List active diagnostics for the session (one record set per constituent, not a historical log).",
159
199
  inputSchema: {
160
- type: 'object',
200
+ type: "object",
161
201
  properties: {
162
- payload: { type: 'string', description: 'JSON payload from export_session.' }
202
+ sessionId,
203
+ constituentId: { type: "number" },
163
204
  },
164
- required: ['payload']
165
205
  },
166
- invoke: (tool, args) => tool.importSession(String(args.payload))
206
+ invoke: (tool, args) => {
207
+ const constituentId = args.constituentId;
208
+ const filters =
209
+ typeof constituentId === "number" ? { constituentId } : undefined;
210
+ return tool.listDiagnostics(filters, optionalSessionId(args));
211
+ },
167
212
  },
168
213
  {
169
- name: 'set_constituenta_value',
214
+ name: "commit_step",
170
215
  description:
171
- 'Bind or assign a value to a single interpretable constituent (basic, constant, structure). Inferrable constituents (term, axiom, statement) cannot be set directly.',
216
+ "Record a session revision with an optional human-readable message.",
172
217
  inputSchema: {
173
- type: 'object',
218
+ type: "object",
174
219
  properties: {
175
220
  sessionId,
176
- input: {
177
- type: 'object',
178
- description: 'SetConstituentaValueInput. Required keys: target (id), value.',
179
- additionalProperties: true
180
- }
221
+ message: { type: "string" },
181
222
  },
182
- required: ['sessionId', 'input']
183
223
  },
184
224
  invoke: (tool, args) =>
185
- tool.setConstituentaValue(String(args.sessionId), args.input as never)
225
+ tool.commitStep(
226
+ args.message as string | undefined,
227
+ optionalSessionId(args),
228
+ ),
186
229
  },
187
230
  {
188
- name: 'set_constituenta_values',
189
- description: 'Batch variant of set_constituenta_value: assign values to multiple interpretable constituents.',
231
+ name: "export_session",
232
+ description:
233
+ "Serialize the session to a JSON string suitable for import_data with kind=session.",
190
234
  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']
235
+ type: "object",
236
+ properties: { sessionId },
201
237
  },
202
- invoke: (tool, args) =>
203
- tool.setConstituentaValues(String(args.sessionId), args.input as never)
238
+ invoke: (tool, args) => tool.exportSession(optionalSessionId(args)),
204
239
  },
205
240
  {
206
- name: 'clear_constituenta_values',
207
- description: 'Clear current values for one or more constituents and reset dependents to NOT_PROCESSED.',
241
+ name: "export_portal",
242
+ description:
243
+ "Export Portal Load-from-JSON payload. kind=schema|model; format=json (default string) or object.",
208
244
  inputSchema: {
209
- type: 'object',
245
+ type: "object",
210
246
  properties: {
211
247
  sessionId,
212
- input: {
213
- type: 'object',
214
- description: 'ClearConstituentaValuesInput. Required keys: targets (list of ids).',
215
- additionalProperties: true
216
- }
248
+ kind: { type: "string", enum: ["schema", "model"] },
249
+ format: { type: "string", enum: ["json", "object"] },
217
250
  },
218
- required: ['sessionId', 'input']
251
+ required: ["kind"],
219
252
  },
220
253
  invoke: (tool, args) =>
221
- tool.clearConstituentaValues(String(args.sessionId), args.input as never)
254
+ tool.exportPortal(omitSessionId(args) as never, optionalSessionId(args)),
222
255
  },
223
256
  {
224
- name: 'get_model_state',
225
- description: 'Return the SessionModelState — current values and evaluation statuses for every constituent.',
257
+ name: "import_data",
258
+ description:
259
+ "Import a session from export_session JSON, Portal schema JSON, or GET /api/rsforms/:id/details. kind=auto (default) detects the shape.",
226
260
  inputSchema: {
227
- type: 'object',
228
- properties: { sessionId },
229
- required: ['sessionId']
261
+ type: "object",
262
+ properties: {
263
+ payload: {
264
+ description:
265
+ "JSON string or parsed object (session export, Portal schema, or rsform details).",
266
+ },
267
+ kind: {
268
+ type: "string",
269
+ enum: ["auto", "session", "portal-schema", "portal-details"],
270
+ },
271
+ },
272
+ required: ["payload"],
230
273
  },
231
- invoke: (tool, args) => tool.getModelState(String(args.sessionId))
274
+ invoke: (tool, args) =>
275
+ tool.importData(args.payload as string | object, args.kind as never),
232
276
  },
233
277
  {
234
- name: 'evaluate_expression',
278
+ name: "set_model_values",
235
279
  description:
236
- 'Evaluate a scratch RSLang expression against the current bindings without storing it. Returns value + evaluation status.',
280
+ "Set and/or clear interpretable model bindings. Pass set[] for bindings and clear[] for constituent ids to reset.",
237
281
  inputSchema: {
238
- type: 'object',
282
+ type: "object",
239
283
  properties: {
240
284
  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' }
285
+ set: {
286
+ type: "array",
287
+ items: {
288
+ type: "object",
289
+ properties: {
290
+ target: { type: "number" },
291
+ type: { type: "string" },
292
+ value: {},
293
+ },
294
+ required: ["target", "value"],
248
295
  },
249
- required: ['expression'],
250
- additionalProperties: true
251
- }
296
+ },
297
+ clear: { type: "array", items: { type: "number" } },
252
298
  },
253
- required: ['sessionId', 'input']
254
299
  },
255
300
  invoke: (tool, args) =>
256
- tool.evaluateExpression(String(args.sessionId), args.input as never)
301
+ tool.setModelValues(
302
+ omitSessionId(args) as never,
303
+ optionalSessionId(args),
304
+ ),
257
305
  },
258
306
  {
259
- name: 'evaluate_constituenta',
260
- description: 'Evaluate a stored derived constituent (term, axiom, statement) using the current bindings.',
307
+ name: "get_model_state",
308
+ description: "Return the SessionModelState.",
261
309
  inputSchema: {
262
- type: 'object',
310
+ type: "object",
311
+ properties: { sessionId },
312
+ },
313
+ invoke: (tool, args) => tool.getModelState(optionalSessionId(args)),
314
+ },
315
+ {
316
+ name: "evaluate",
317
+ description:
318
+ "Evaluate a scratch expression (expression + cstType) or a stored constituent (constituentId).",
319
+ inputSchema: {
320
+ type: "object",
263
321
  properties: {
264
322
  sessionId,
265
- input: {
266
- type: 'object',
267
- description: 'EvaluateConstituentaInput. Required keys: target (id).',
268
- additionalProperties: true
269
- }
323
+ expression: { type: "string" },
324
+ cstType: { type: "string" },
325
+ constituentId: { type: "number" },
270
326
  },
271
- required: ['sessionId', 'input']
272
327
  },
273
328
  invoke: (tool, args) =>
274
- tool.evaluateConstituenta(String(args.sessionId), args.input as never)
329
+ tool.evaluate(omitSessionId(args) as never, optionalSessionId(args)),
275
330
  },
276
331
  {
277
- name: 'recalculate_model',
278
- description: 'Recompute every interpretable / inferrable constituent using current bindings. Returns the updated SessionModelState.',
332
+ name: "recalculate_model",
333
+ description: "Recompute every inferrable constituent.",
279
334
  inputSchema: {
280
- type: 'object',
335
+ type: "object",
281
336
  properties: { sessionId },
282
- required: ['sessionId']
283
337
  },
284
- invoke: (tool, args) => tool.recalculateModel(String(args.sessionId))
285
- }
338
+ invoke: (tool, args) => tool.recalculateModel(optionalSessionId(args)),
339
+ },
286
340
  ];