Skip to content

Commit ce6ea9b

Browse files
committed
Perform unified schema validation for tool parameters
1 parent f115f4d commit ce6ea9b

File tree

4 files changed

+159
-0
lines changed

4 files changed

+159
-0
lines changed

src/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ SRC += dsltest.c
1717
SRC += prompts.c
1818
SRC += plugin.c
1919
SRC += jsonrpc.c
20+
SRC += validation.c
2021
INC += utils.inc.c
2122
INC += r2api.inc.c
2223
OBJS:=$(subst .c,.o,$(SRC))

src/tools.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <r_core.h>
44
#include "r2mcp.h"
55
#include "tools.h"
6+
#include "validation.h"
67
#include "utils.inc.c"
78
#include "jsonrpc.h"
89

@@ -1020,6 +1021,25 @@ char *tools_call(ServerState *ss, const char *tool_name, RJson *tool_args) {
10201021
goto cleanup;
10211022
}
10221023

1024+
// Find the tool spec and validate arguments against schema
1025+
ToolSpec *found_tool = NULL;
1026+
for (size_t i = 0; tool_specs[i].name; i++) {
1027+
ToolSpec *t = &tool_specs[i];
1028+
if (!strcmp (tool_name, t->name)) {
1029+
found_tool = t;
1030+
break;
1031+
}
1032+
}
1033+
1034+
if (found_tool && found_tool->schema_json) {
1035+
ValidationResult vr = validate_arguments (tool_args, found_tool->schema_json);
1036+
if (!vr.valid) {
1037+
result = jsonrpc_error_response (-32602, vr.error_message, NULL, NULL);
1038+
free (vr.error_message);
1039+
goto cleanup;
1040+
}
1041+
}
1042+
10231043
// Dispatch to tool functions
10241044
for (size_t i = 0; tool_specs[i].name; i++) {
10251045
ToolSpec *t = &tool_specs[i];

src/validation.c

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/* r2mcp - MIT - Copyright 2025-2026 - pancake */
2+
3+
#include "validation.h"
4+
#include "jsonrpc.h"
5+
6+
/* Check if a parameter exists and is a string */
7+
ValidationResult validate_required_string(RJson *args, const char *param_name) {
8+
const RJson *field = r_json_get (args, param_name);
9+
if (!field || field->type != R_JSON_STRING) {
10+
char *msg = r_str_newf ("Missing required parameter '%s' (expected string)", param_name);
11+
return (ValidationResult){ false, msg };
12+
}
13+
return (ValidationResult){ true, NULL };
14+
}
15+
16+
/* Check if a parameter exists and is numeric (integer or double) */
17+
ValidationResult validate_required_number(RJson *args, const char *param_name) {
18+
const RJson *field = r_json_get (args, param_name);
19+
if (!field || (field->type != R_JSON_INTEGER && field->type != R_JSON_DOUBLE)) {
20+
char *msg = r_str_newf ("Missing required parameter '%s' (expected number)", param_name);
21+
return (ValidationResult){ false, msg };
22+
}
23+
return (ValidationResult){ true, NULL };
24+
}
25+
26+
/* Check if a parameter exists and is a boolean */
27+
ValidationResult validate_required_boolean(RJson *args, const char *param_name) {
28+
const RJson *field = r_json_get (args, param_name);
29+
if (!field || field->type != R_JSON_BOOLEAN) {
30+
char *msg = r_str_newf ("Missing required parameter '%s' (expected boolean)", param_name);
31+
return (ValidationResult){ false, msg };
32+
}
33+
return (ValidationResult){ true, NULL };
34+
}
35+
36+
/* Check if a parameter exists with any valid type */
37+
ValidationResult validate_required_param(RJson *args, const char *param_name) {
38+
const RJson *field = r_json_get (args, param_name);
39+
if (!field) {
40+
char *msg = r_str_newf ("Missing required parameter '%s'", param_name);
41+
return (ValidationResult){ false, msg };
42+
}
43+
return (ValidationResult){ true, NULL };
44+
}
45+
46+
/* Parse JSON Schema to extract required properties */
47+
static RList *parse_required_properties(const char *schema_json) {
48+
if (!schema_json || !*schema_json) {
49+
return NULL;
50+
}
51+
52+
/* We need a non-modifiable copy for parsing */
53+
char *schema_copy = strdup (schema_json);
54+
if (!schema_copy) {
55+
return NULL;
56+
}
57+
58+
RJson *schema = r_json_parse (schema_copy);
59+
if (!schema || schema->type != R_JSON_OBJECT) {
60+
free (schema_copy);
61+
return NULL;
62+
}
63+
64+
RList *required = NULL;
65+
const RJson *required_json = r_json_get (schema, "required");
66+
if (required_json && required_json->type == R_JSON_ARRAY) {
67+
required = r_list_newf (free);
68+
const RJson *item = required_json->children.first;
69+
while (item) {
70+
if (item->type == R_JSON_STRING && item->str_value) {
71+
r_list_append (required, strdup (item->str_value));
72+
}
73+
item = item->next;
74+
}
75+
}
76+
77+
r_json_free (schema);
78+
free (schema_copy);
79+
return required;
80+
}
81+
82+
/* Validate arguments against JSON Schema */
83+
ValidationResult validate_arguments(RJson *args, const char *schema_json) {
84+
if (!schema_json || !*schema_json) {
85+
/* No schema defined - allow all arguments */
86+
return (ValidationResult){ true, NULL };
87+
}
88+
89+
/* Parse schema to get required properties */
90+
RList *required = parse_required_properties (schema_json);
91+
if (!required) {
92+
/* Schema couldn't be parsed - allow all arguments */
93+
return (ValidationResult){ true, NULL };
94+
}
95+
96+
/* Check each required parameter exists */
97+
RListIter *it;
98+
const char *param;
99+
r_list_foreach (required, it, param) {
100+
const RJson *field = r_json_get (args, param);
101+
if (!field) {
102+
char *msg = r_str_newf ("Missing required parameter '%s'", param);
103+
r_list_free (required);
104+
return (ValidationResult){ false, msg };
105+
}
106+
}
107+
108+
r_list_free (required);
109+
return (ValidationResult){ true, NULL };
110+
}

src/validation.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* r2mcp - MIT - Copyright 2025-2026 - pancake */
2+
#ifndef VALIDATION_H
3+
#define VALIDATION_H
4+
5+
#include <r_core.h>
6+
#include "r2mcp.h"
7+
8+
/* Validation result - caller must free error_message if not NULL */
9+
typedef struct {
10+
bool valid;
11+
char *error_message;
12+
} ValidationResult;
13+
14+
/* Validate tool arguments against the JSON schema in ToolSpec.
15+
* Returns ValidationResult with valid=true if args match schema.
16+
* error_message is set if validation fails (caller must free). */
17+
ValidationResult validate_arguments(RJson *args, const char *schema_json);
18+
19+
/* Helper: Check if a required string parameter exists and is a string */
20+
ValidationResult validate_required_string(RJson *args, const char *param_name);
21+
22+
/* Helper: Check if a required numeric parameter exists and is numeric */
23+
ValidationResult validate_required_number(RJson *args, const char *param_name);
24+
25+
/* Helper: Check if a required boolean parameter exists and is a boolean */
26+
ValidationResult validate_required_boolean(RJson *args, const char *param_name);
27+
28+
#endif

0 commit comments

Comments
 (0)