Skip to content

Commit a2b6091

Browse files
committed
feat(bridge): add JSON Schema export to HostFunctionSchema and HostParam
Enable MCP tool registration by generating JSON Schema from host function definitions. HostParam gains toJsonSchema() and an optional jsonSchemaOverride for complex types (nested objects, enums). HostFunctionSchema.toJsonSchema() produces MCP-compatible inputSchema with properties and required fields.
1 parent b38532f commit a2b6091

3 files changed

Lines changed: 249 additions & 0 deletions

File tree

packages/dart_monty_bridge/lib/src/bridge/host_function_schema.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,32 @@ class HostFunctionSchema {
2828
/// Keyword args overlay by name.
2929
final List<HostParam> params;
3030

31+
/// Returns a JSON Schema object describing this function's input.
32+
///
33+
/// Produces a schema compatible with MCP tool `inputSchema`:
34+
/// ```json
35+
/// {
36+
/// "type": "object",
37+
/// "properties": { ... },
38+
/// "required": ["param1"]
39+
/// }
40+
/// ```
41+
Map<String, Object?> toJsonSchema() {
42+
final properties = <String, Object?>{};
43+
final required = <String>[];
44+
45+
for (final param in params) {
46+
properties[param.name] = param.toJsonSchema();
47+
if (param.isRequired) required.add(param.name);
48+
}
49+
50+
return {
51+
'type': 'object',
52+
'properties': properties,
53+
if (required.isNotEmpty) 'required': required,
54+
};
55+
}
56+
3157
/// Maps positional + keyword args from [pending] to a named parameter map.
3258
///
3359
/// 1. Positional args are matched to [params] by order.

packages/dart_monty_bridge/lib/src/bridge/host_param.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class HostParam {
1111
this.isRequired = true,
1212
this.description,
1313
this.defaultValue,
14+
this.jsonSchemaOverride,
1415
});
1516

1617
/// Parameter name (used as the key in the validated args map).
@@ -28,6 +29,29 @@ class HostParam {
2829
/// Default value when the argument is absent and not required.
2930
final Object? defaultValue;
3031

32+
/// Optional full JSON Schema override for this parameter.
33+
///
34+
/// When set, [toJsonSchema] returns this map directly instead of
35+
/// generating from [type] and [description]. Use this for complex
36+
/// schemas (nested objects, enums, arrays with item types) that
37+
/// [HostParamType] cannot express.
38+
final Map<String, Object?>? jsonSchemaOverride;
39+
40+
/// Returns a JSON Schema property definition for this parameter.
41+
///
42+
/// If [jsonSchemaOverride] is set, returns it directly. Otherwise
43+
/// generates a schema from [type] and [description].
44+
Map<String, Object?> toJsonSchema() {
45+
if (jsonSchemaOverride != null) return jsonSchemaOverride!;
46+
47+
final schema = <String, Object?>{
48+
'type': type.jsonSchemaType,
49+
};
50+
if (description != null) schema['description'] = description;
51+
52+
return schema;
53+
}
54+
3155
/// Validates and optionally coerces [value].
3256
///
3357
/// Returns the validated (possibly coerced) value.
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import 'package:dart_monty_bridge/dart_monty_bridge.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group('HostParam.toJsonSchema', () {
6+
test('generates schema from type and description', () {
7+
const param = HostParam(
8+
name: 'query',
9+
type: HostParamType.string,
10+
description: 'Search query',
11+
);
12+
13+
expect(param.toJsonSchema(), {
14+
'type': 'string',
15+
'description': 'Search query',
16+
});
17+
});
18+
19+
test('omits description when null', () {
20+
const param = HostParam(
21+
name: 'count',
22+
type: HostParamType.integer,
23+
);
24+
25+
expect(param.toJsonSchema(), {'type': 'integer'});
26+
});
27+
28+
test('maps all HostParamType values', () {
29+
const cases = {
30+
HostParamType.string: 'string',
31+
HostParamType.integer: 'integer',
32+
HostParamType.number: 'number',
33+
HostParamType.boolean: 'boolean',
34+
HostParamType.list: 'array',
35+
HostParamType.map: 'object',
36+
HostParamType.any: 'string',
37+
};
38+
39+
for (final entry in cases.entries) {
40+
final param = HostParam(name: 'x', type: entry.key);
41+
expect(
42+
param.toJsonSchema()['type'],
43+
entry.value,
44+
reason: '${entry.key} should map to ${entry.value}',
45+
);
46+
}
47+
});
48+
49+
test('uses jsonSchemaOverride when set', () {
50+
const override = {
51+
'type': 'object',
52+
'properties': {
53+
'x': {'type': 'number'},
54+
'y': {'type': 'number'},
55+
},
56+
'required': ['x', 'y'],
57+
};
58+
59+
const param = HostParam(
60+
name: 'point',
61+
type: HostParamType.map,
62+
description: 'A 2D point',
63+
jsonSchemaOverride: override,
64+
);
65+
66+
expect(param.toJsonSchema(), override);
67+
});
68+
69+
test('jsonSchemaOverride takes precedence over type and description', () {
70+
const param = HostParam(
71+
name: 'color',
72+
type: HostParamType.string,
73+
description: 'ignored',
74+
jsonSchemaOverride: {
75+
'type': 'string',
76+
'enum': ['red', 'green', 'blue'],
77+
},
78+
);
79+
80+
final schema = param.toJsonSchema();
81+
expect(schema['enum'], ['red', 'green', 'blue']);
82+
expect(schema.containsKey('description'), isFalse);
83+
});
84+
});
85+
86+
group('HostFunctionSchema.toJsonSchema', () {
87+
test('generates empty object schema for no params', () {
88+
const schema = HostFunctionSchema(
89+
name: 'ping',
90+
description: 'Health check',
91+
);
92+
93+
expect(schema.toJsonSchema(), {
94+
'type': 'object',
95+
'properties': <String, Object?>{},
96+
});
97+
});
98+
99+
test('generates schema with required and optional params', () {
100+
const schema = HostFunctionSchema(
101+
name: 'search',
102+
description: 'Search documents',
103+
params: [
104+
HostParam(
105+
name: 'query',
106+
type: HostParamType.string,
107+
description: 'Search query',
108+
),
109+
HostParam(
110+
name: 'limit',
111+
type: HostParamType.integer,
112+
description: 'Max results',
113+
isRequired: false,
114+
defaultValue: 10,
115+
),
116+
HostParam(
117+
name: 'verbose',
118+
type: HostParamType.boolean,
119+
isRequired: false,
120+
),
121+
],
122+
);
123+
124+
expect(schema.toJsonSchema(), {
125+
'type': 'object',
126+
'properties': {
127+
'query': {'type': 'string', 'description': 'Search query'},
128+
'limit': {'type': 'integer', 'description': 'Max results'},
129+
'verbose': {'type': 'boolean'},
130+
},
131+
'required': ['query'],
132+
});
133+
});
134+
135+
test('omits required key when all params are optional', () {
136+
const schema = HostFunctionSchema(
137+
name: 'configure',
138+
description: 'Set options',
139+
params: [
140+
HostParam(
141+
name: 'timeout',
142+
type: HostParamType.number,
143+
isRequired: false,
144+
),
145+
],
146+
);
147+
148+
final jsonSchema = schema.toJsonSchema();
149+
expect(jsonSchema.containsKey('required'), isFalse);
150+
});
151+
152+
test('includes all required params in required list', () {
153+
const schema = HostFunctionSchema(
154+
name: 'create',
155+
description: 'Create resource',
156+
params: [
157+
HostParam(name: 'name', type: HostParamType.string),
158+
HostParam(name: 'tags', type: HostParamType.list),
159+
HostParam(name: 'meta', type: HostParamType.map),
160+
],
161+
);
162+
163+
expect(schema.toJsonSchema()['required'], ['name', 'tags', 'meta']);
164+
});
165+
166+
test('respects jsonSchemaOverride on individual params', () {
167+
const schema = HostFunctionSchema(
168+
name: 'draw',
169+
description: 'Draw a shape',
170+
params: [
171+
HostParam(
172+
name: 'shape',
173+
type: HostParamType.string,
174+
jsonSchemaOverride: {
175+
'type': 'string',
176+
'enum': ['circle', 'square', 'triangle'],
177+
'description': 'Shape type',
178+
},
179+
),
180+
HostParam(
181+
name: 'radius',
182+
type: HostParamType.number,
183+
isRequired: false,
184+
description: 'Shape radius',
185+
),
186+
],
187+
);
188+
189+
final jsonSchema = schema.toJsonSchema();
190+
final properties =
191+
jsonSchema['properties']! as Map<String, Object?>;
192+
final shapeSchema = properties['shape']! as Map<String, Object?>;
193+
expect(shapeSchema['enum'], ['circle', 'square', 'triangle']);
194+
195+
final radiusSchema = properties['radius']! as Map<String, Object?>;
196+
expect(radiusSchema, {'type': 'number', 'description': 'Shape radius'});
197+
});
198+
});
199+
}

0 commit comments

Comments
 (0)