Jeremiah Lowin commited on
Commit
112de41
·
1 Parent(s): 5050a56

ensure all json schemas are compressed

Browse files
src/fastmcp/prompts/prompt.py CHANGED
@@ -13,7 +13,7 @@ from mcp.types import PromptArgument as MCPPromptArgument
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
15
  from fastmcp.server.dependencies import get_context
16
- from fastmcp.utilities.json_schema import prune_params
17
  from fastmcp.utilities.logging import get_logger
18
  from fastmcp.utilities.types import (
19
  _convert_set_defaults,
@@ -115,7 +115,11 @@ class Prompt(BaseModel):
115
 
116
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
117
  if context_kwarg:
118
- parameters = prune_params(parameters, params=[context_kwarg])
 
 
 
 
119
 
120
  # Convert parameters to PromptArguments
121
  arguments: list[PromptArgument] = []
 
13
  from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, validate_call
14
 
15
  from fastmcp.server.dependencies import get_context
16
+ from fastmcp.utilities.json_schema import compress_schema
17
  from fastmcp.utilities.logging import get_logger
18
  from fastmcp.utilities.types import (
19
  _convert_set_defaults,
 
115
 
116
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
117
  if context_kwarg:
118
+ prune_params = [context_kwarg]
119
+ else:
120
+ prune_params = None
121
+
122
+ parameters = compress_schema(parameters, prune_params=prune_params)
123
 
124
  # Convert parameters to PromptArguments
125
  arguments: list[PromptArgument] = []
src/fastmcp/resources/template.py CHANGED
@@ -21,6 +21,7 @@ from pydantic import (
21
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.server.dependencies import get_context
 
24
  from fastmcp.utilities.types import (
25
  _convert_set_defaults,
26
  find_kwarg_by_type,
@@ -150,6 +151,10 @@ class ResourceTemplate(BaseModel):
150
  # Get schema from TypeAdapter - will fail if function isn't properly typed
151
  parameters = TypeAdapter(fn).json_schema()
152
 
 
 
 
 
153
  # ensure the arguments are properly cast
154
  fn = validate_call(fn)
155
 
 
21
 
22
  from fastmcp.resources.types import FunctionResource, Resource
23
  from fastmcp.server.dependencies import get_context
24
+ from fastmcp.utilities.json_schema import compress_schema
25
  from fastmcp.utilities.types import (
26
  _convert_set_defaults,
27
  find_kwarg_by_type,
 
151
  # Get schema from TypeAdapter - will fail if function isn't properly typed
152
  parameters = TypeAdapter(fn).json_schema()
153
 
154
+ # compress the schema
155
+ prune_params = [context_kwarg] if context_kwarg else None
156
+ parameters = compress_schema(parameters, prune_params=prune_params)
157
+
158
  # ensure the arguments are properly cast
159
  fn = validate_call(fn)
160
 
src/fastmcp/tools/tool.py CHANGED
@@ -12,7 +12,7 @@ from pydantic import BaseModel, BeforeValidator, Field
12
 
13
  import fastmcp
14
  from fastmcp.server.dependencies import get_context
15
- from fastmcp.utilities.json_schema import prune_params
16
  from fastmcp.utilities.logging import get_logger
17
  from fastmcp.utilities.types import (
18
  Image,
@@ -81,7 +81,11 @@ class Tool(BaseModel):
81
 
82
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
83
  if context_kwarg:
84
- schema = prune_params(schema, params=[context_kwarg])
 
 
 
 
85
 
86
  return cls(
87
  fn=fn,
 
12
 
13
  import fastmcp
14
  from fastmcp.server.dependencies import get_context
15
+ from fastmcp.utilities.json_schema import compress_schema
16
  from fastmcp.utilities.logging import get_logger
17
  from fastmcp.utilities.types import (
18
  Image,
 
81
 
82
  context_kwarg = find_kwarg_by_type(fn, kwarg_type=Context)
83
  if context_kwarg:
84
+ prune_params = [context_kwarg]
85
+ else:
86
+ prune_params = None
87
+
88
+ schema = compress_schema(schema, prune_params=prune_params)
89
 
90
  return cls(
91
  fn=fn,
tests/utilities/test_json_schema.py CHANGED
@@ -1,110 +1,246 @@
1
- from fastmcp.utilities.json_schema import _prune_param, prune_params
2
-
3
-
4
- def test_prune_param_nonexistent():
5
- """Test pruning a parameter that doesn't exist."""
6
- schema = {"properties": {"foo": {"type": "string"}}}
7
- result = _prune_param(schema, "bar")
8
- assert result == schema # Schema should be unchanged
9
-
10
-
11
- def test_prune_param_exists():
12
- """Test pruning a parameter that exists."""
13
- schema = {"properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}}}
14
- result = _prune_param(schema, "bar")
15
- assert result["properties"] == {"foo": {"type": "string"}}
16
-
17
-
18
- def test_prune_param_last_property():
19
- """Test pruning the only/last parameter, should leave empty properties object."""
20
- schema = {"properties": {"foo": {"type": "string"}}}
21
- result = _prune_param(schema, "foo")
22
- assert "properties" in result
23
- assert result["properties"] == {}
24
-
25
-
26
- def test_prune_param_from_required():
27
- """Test pruning a parameter that's in the required list."""
28
- schema = {
29
- "properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}},
30
- "required": ["foo", "bar"],
31
- }
32
- result = _prune_param(schema, "bar")
33
- assert result["required"] == ["foo"]
34
-
35
-
36
- def test_prune_param_last_required():
37
- """Test pruning the last required parameter, should remove required field."""
38
- schema = {
39
- "properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}},
40
- "required": ["foo"],
41
- }
42
- result = _prune_param(schema, "foo")
43
- assert "required" not in result
44
-
45
-
46
- def test_prune_param_with_refs():
47
- """Test pruning a parameter that has references in $defs."""
48
- schema = {
49
- "properties": {
50
- "foo": {"$ref": "#/$defs/foo_def"},
51
- "bar": {"$ref": "#/$defs/bar_def"},
52
- },
53
- "$defs": {
54
- "foo_def": {"type": "string"},
55
- "bar_def": {"type": "integer"},
56
- },
57
- }
58
- result = _prune_param(schema, "bar")
59
- assert "bar_def" not in result["$defs"]
60
- assert "foo_def" in result["$defs"]
61
-
62
-
63
- def test_prune_param_all_refs():
64
- """Test pruning all parameters with refs, should remove $defs."""
65
- schema = {
66
- "properties": {
67
- "foo": {"$ref": "#/$defs/foo_def"},
68
- },
69
- "$defs": {
70
- "foo_def": {"type": "string"},
71
- },
72
- }
73
- result = _prune_param(schema, "foo")
74
- assert "$defs" not in result
75
-
76
-
77
- def test_prune_params_multiple():
78
- """Test pruning multiple parameters at once."""
79
- schema = {
80
- "properties": {
81
- "foo": {"type": "string"},
82
- "bar": {"type": "integer"},
83
- "baz": {"type": "boolean"},
84
- },
85
- "required": ["foo", "bar"],
86
- }
87
- result = prune_params(schema, ["foo", "baz"])
88
- assert result["properties"] == {"bar": {"type": "integer"}}
89
- assert result["required"] == ["bar"]
90
-
91
-
92
- def test_prune_params_nested_refs():
93
- """Test pruning with nested references."""
94
- schema = {
95
- "properties": {
96
- "foo": {
97
- "type": "object",
98
- "properties": {"nested": {"$ref": "#/$defs/nested_def"}},
99
  },
100
- "bar": {"$ref": "#/$defs/bar_def"},
101
- },
102
- "$defs": {
103
- "nested_def": {"type": "string"},
104
- "bar_def": {"type": "integer"},
105
- },
106
- }
107
- # Removing foo should keep nested_def as it's not referenced anymore
108
- result = _prune_param(schema, "foo")
109
- assert "nested_def" not in result["$defs"]
110
- assert "bar_def" in result["$defs"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastmcp.utilities.json_schema import (
2
+ _prune_additional_properties,
3
+ _prune_param,
4
+ _prune_unused_defs,
5
+ compress_schema,
6
+ )
7
+
8
+
9
+ class TestPruneParam:
10
+ """Tests for the _prune_param function."""
11
+
12
+ def test_nonexistent(self):
13
+ """Test pruning a parameter that doesn't exist."""
14
+ schema = {"properties": {"foo": {"type": "string"}}}
15
+ result = _prune_param(schema, "bar")
16
+ assert result == schema # Schema should be unchanged
17
+
18
+ def test_exists(self):
19
+ """Test pruning a parameter that exists."""
20
+ schema = {"properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}}}
21
+ result = _prune_param(schema, "bar")
22
+ assert result["properties"] == {"foo": {"type": "string"}}
23
+
24
+ def test_last_property(self):
25
+ """Test pruning the only/last parameter, should leave empty properties object."""
26
+ schema = {"properties": {"foo": {"type": "string"}}}
27
+ result = _prune_param(schema, "foo")
28
+ assert "properties" in result
29
+ assert result["properties"] == {}
30
+
31
+ def test_from_required(self):
32
+ """Test pruning a parameter that's in the required list."""
33
+ schema = {
34
+ "properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}},
35
+ "required": ["foo", "bar"],
36
+ }
37
+ result = _prune_param(schema, "bar")
38
+ assert result["required"] == ["foo"]
39
+
40
+ def test_last_required(self):
41
+ """Test pruning the last required parameter, should remove required field."""
42
+ schema = {
43
+ "properties": {"foo": {"type": "string"}, "bar": {"type": "integer"}},
44
+ "required": ["foo"],
45
+ }
46
+ result = _prune_param(schema, "foo")
47
+ assert "required" not in result
48
+
49
+
50
+ class TestPruneUnusedDefs:
51
+ """Tests for the _prune_unused_defs function."""
52
+
53
+ def test_removes_unreferenced_defs(self):
54
+ """Test that unreferenced definitions are removed."""
55
+ schema = {
56
+ "properties": {
57
+ "foo": {"$ref": "#/$defs/foo_def"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  },
59
+ "$defs": {
60
+ "foo_def": {"type": "string"},
61
+ "unused_def": {"type": "integer"},
62
+ },
63
+ }
64
+ result = _prune_unused_defs(schema)
65
+ assert "foo_def" in result["$defs"]
66
+ assert "unused_def" not in result["$defs"]
67
+
68
+ def test_nested_references_kept(self):
69
+ """Test that definitions referenced via nesting are kept."""
70
+ schema = {
71
+ "properties": {
72
+ "foo": {"$ref": "#/$defs/foo_def"},
73
+ },
74
+ "$defs": {
75
+ "foo_def": {
76
+ "type": "object",
77
+ "properties": {"nested": {"$ref": "#/$defs/nested_def"}},
78
+ },
79
+ "nested_def": {"type": "string"},
80
+ "unused_def": {"type": "integer"},
81
+ },
82
+ }
83
+ result = _prune_unused_defs(schema)
84
+ assert "foo_def" in result["$defs"]
85
+ assert "nested_def" in result["$defs"]
86
+ assert "unused_def" not in result["$defs"]
87
+
88
+ def test_array_references_kept(self):
89
+ """Test that definitions referenced in array items are kept."""
90
+ schema = {
91
+ "properties": {
92
+ "items": {"type": "array", "items": {"$ref": "#/$defs/item_def"}},
93
+ },
94
+ "$defs": {
95
+ "item_def": {"type": "string"},
96
+ "unused_def": {"type": "integer"},
97
+ },
98
+ }
99
+ result = _prune_unused_defs(schema)
100
+ assert "item_def" in result["$defs"]
101
+ assert "unused_def" not in result["$defs"]
102
+
103
+ def test_removes_defs_field_when_empty(self):
104
+ """Test that $defs field is removed when all definitions are unused."""
105
+ schema = {
106
+ "properties": {
107
+ "foo": {"type": "string"},
108
+ },
109
+ "$defs": {
110
+ "unused_def": {"type": "integer"},
111
+ },
112
+ }
113
+ result = _prune_unused_defs(schema)
114
+ assert "$defs" not in result
115
+
116
+
117
+ class TestPruneAdditionalProperties:
118
+ """Tests for the _prune_additional_properties function."""
119
+
120
+ def test_removes_when_false(self):
121
+ """Test that additionalProperties is removed when it's false."""
122
+ schema = {
123
+ "type": "object",
124
+ "properties": {"foo": {"type": "string"}},
125
+ "additionalProperties": False,
126
+ }
127
+ result = _prune_additional_properties(schema)
128
+ assert "additionalProperties" not in result
129
+
130
+ def test_keeps_when_true(self):
131
+ """Test that additionalProperties is kept when it's true."""
132
+ schema = {
133
+ "type": "object",
134
+ "properties": {"foo": {"type": "string"}},
135
+ "additionalProperties": True,
136
+ }
137
+ result = _prune_additional_properties(schema)
138
+ assert "additionalProperties" in result
139
+ assert result["additionalProperties"] is True
140
+
141
+ def test_keeps_when_object(self):
142
+ """Test that additionalProperties is kept when it's an object schema."""
143
+ schema = {
144
+ "type": "object",
145
+ "properties": {"foo": {"type": "string"}},
146
+ "additionalProperties": {"type": "string"},
147
+ }
148
+ result = _prune_additional_properties(schema)
149
+ assert "additionalProperties" in result
150
+ assert result["additionalProperties"] == {"type": "string"}
151
+
152
+
153
+ class TestCompressSchema:
154
+ """Tests for the compress_schema function."""
155
+
156
+ def test_prune_params(self):
157
+ """Test pruning parameters with compress_schema."""
158
+ schema = {
159
+ "properties": {
160
+ "foo": {"type": "string"},
161
+ "bar": {"type": "integer"},
162
+ "baz": {"type": "boolean"},
163
+ },
164
+ "required": ["foo", "bar"],
165
+ }
166
+ result = compress_schema(schema, prune_params=["foo", "baz"])
167
+ assert result["properties"] == {"bar": {"type": "integer"}}
168
+ assert result["required"] == ["bar"]
169
+
170
+ def test_prune_defs(self):
171
+ """Test pruning unused definitions with compress_schema."""
172
+ schema = {
173
+ "properties": {
174
+ "foo": {"$ref": "#/$defs/foo_def"},
175
+ "bar": {"type": "integer"},
176
+ },
177
+ "$defs": {
178
+ "foo_def": {"type": "string"},
179
+ "unused_def": {"type": "number"},
180
+ },
181
+ }
182
+ result = compress_schema(schema)
183
+ assert "foo_def" in result["$defs"]
184
+ assert "unused_def" not in result["$defs"]
185
+
186
+ def test_disable_prune_defs(self):
187
+ """Test disabling pruning of unused definitions."""
188
+ schema = {
189
+ "properties": {
190
+ "foo": {"$ref": "#/$defs/foo_def"},
191
+ "bar": {"type": "integer"},
192
+ },
193
+ "$defs": {
194
+ "foo_def": {"type": "string"},
195
+ "unused_def": {"type": "number"},
196
+ },
197
+ }
198
+ result = compress_schema(schema, prune_defs=False)
199
+ assert "foo_def" in result["$defs"]
200
+ assert "unused_def" in result["$defs"]
201
+
202
+ def test_pruning_additional_properties(self):
203
+ """Test pruning additionalProperties when False."""
204
+ schema = {
205
+ "type": "object",
206
+ "properties": {"foo": {"type": "string"}},
207
+ "additionalProperties": False,
208
+ }
209
+ result = compress_schema(schema)
210
+ assert "additionalProperties" not in result
211
+
212
+ def test_disable_pruning_additional_properties(self):
213
+ """Test disabling pruning of additionalProperties."""
214
+ schema = {
215
+ "type": "object",
216
+ "properties": {"foo": {"type": "string"}},
217
+ "additionalProperties": False,
218
+ }
219
+ result = compress_schema(schema, prune_additional_properties=False)
220
+ assert "additionalProperties" in result
221
+ assert result["additionalProperties"] is False
222
+
223
+ def test_combined_operations(self):
224
+ """Test all pruning operations together."""
225
+ schema = {
226
+ "type": "object",
227
+ "properties": {
228
+ "keep": {"type": "string"},
229
+ "remove": {"$ref": "#/$defs/remove_def"},
230
+ },
231
+ "required": ["keep", "remove"],
232
+ "additionalProperties": False,
233
+ "$defs": {
234
+ "remove_def": {"type": "string"},
235
+ "unused_def": {"type": "number"},
236
+ },
237
+ }
238
+ result = compress_schema(schema, prune_params=["remove"])
239
+ # Check that parameter was removed
240
+ assert "remove" not in result["properties"]
241
+ # Check that required list was updated
242
+ assert result["required"] == ["keep"]
243
+ # Check that unused definitions were removed
244
+ assert "$defs" not in result # Both defs should be gone
245
+ # Check that additionalProperties was removed
246
+ assert "additionalProperties" not in result
tests/utilities/test_typeadapter.py CHANGED
@@ -13,7 +13,7 @@ import annotated_types
13
  import pytest
14
  from pydantic import BaseModel, Field
15
 
16
- from fastmcp.utilities.json_schema import prune_params
17
  from fastmcp.utilities.types import get_cached_typeadapter
18
 
19
 
@@ -175,7 +175,7 @@ def test_skip_names():
175
  # Get schema and prune parameters
176
  type_adapter = get_cached_typeadapter(func_with_many_params)
177
  schema = type_adapter.json_schema()
178
- pruned_schema = prune_params(schema, params=["skip_this", "also_skip"])
179
 
180
  # Check that only the desired parameters remain
181
  assert "keep_this" in pruned_schema["properties"]
 
13
  import pytest
14
  from pydantic import BaseModel, Field
15
 
16
+ from fastmcp.utilities.json_schema import compress_schema
17
  from fastmcp.utilities.types import get_cached_typeadapter
18
 
19
 
 
175
  # Get schema and prune parameters
176
  type_adapter = get_cached_typeadapter(func_with_many_params)
177
  schema = type_adapter.json_schema()
178
+ pruned_schema = compress_schema(schema, prune_params=["skip_this", "also_skip"])
179
 
180
  # Check that only the desired parameters remain
181
  assert "keep_this" in pruned_schema["properties"]