fairspec-mcp-server 0.3.0__tar.gz
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.
- fairspec_mcp_server-0.3.0/.gitignore +239 -0
- fairspec_mcp_server-0.3.0/PKG-INFO +20 -0
- fairspec_mcp_server-0.3.0/README.md +3 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/__init__.py +2 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/server_unit.py +93 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/start_unit.py +29 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/limit.py +34 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/tool.py +38 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/models/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/models/tool.py +15 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/server.py +41 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/_shared/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/_shared/schemas.py +20 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/_test/validate_unit.py +26 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/validate.py +22 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/infer_unit.py +20 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/validate_unit.py +31 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/infer.py +19 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/validate.py +25 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/infer_unit.py +23 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/query_unit.py +39 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/validate_unit.py +53 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/infer.py +17 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/query.py +38 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/validate.py +25 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/_test/preview_unit.py +33 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/preview.py +27 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/_test/infer_unit.py +29 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/infer.py +28 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/query_unit.py +85 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/validate_unit.py +98 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/query.py +39 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/validate.py +59 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/__init__.py +0 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/infer_unit.py +42 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/validate_unit.py +33 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/infer.py +28 -0
- fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/validate.py +27 -0
- fairspec_mcp_server-0.3.0/pyproject.toml +39 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Git worktrees (agent isolation)
|
|
2
|
+
/.worktrees/
|
|
3
|
+
|
|
4
|
+
# Byte-compiled / optimized / DLL files
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.py[codz]
|
|
7
|
+
*$py.class
|
|
8
|
+
|
|
9
|
+
# C extensions
|
|
10
|
+
*.so
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
.Python
|
|
14
|
+
build/
|
|
15
|
+
develop-eggs/
|
|
16
|
+
dist/
|
|
17
|
+
downloads/
|
|
18
|
+
eggs/
|
|
19
|
+
.eggs/
|
|
20
|
+
lib/
|
|
21
|
+
lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
share/python-wheels/
|
|
27
|
+
*.egg-info/
|
|
28
|
+
.installed.cfg
|
|
29
|
+
*.egg
|
|
30
|
+
MANIFEST
|
|
31
|
+
|
|
32
|
+
# PyInstaller
|
|
33
|
+
# Usually these files are written by a python script from a template
|
|
34
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
35
|
+
*.manifest
|
|
36
|
+
*.spec
|
|
37
|
+
|
|
38
|
+
# Installer logs
|
|
39
|
+
pip-log.txt
|
|
40
|
+
pip-delete-this-directory.txt
|
|
41
|
+
|
|
42
|
+
# Unit test / coverage reports
|
|
43
|
+
htmlcov/
|
|
44
|
+
.tox/
|
|
45
|
+
.nox/
|
|
46
|
+
.coverage
|
|
47
|
+
.coverage.*
|
|
48
|
+
.cache
|
|
49
|
+
nosetests.xml
|
|
50
|
+
coverage.xml
|
|
51
|
+
*.cover
|
|
52
|
+
*.py.cover
|
|
53
|
+
.hypothesis/
|
|
54
|
+
.pytest_cache/
|
|
55
|
+
cover/
|
|
56
|
+
|
|
57
|
+
# Translations
|
|
58
|
+
*.mo
|
|
59
|
+
*.pot
|
|
60
|
+
|
|
61
|
+
# Django stuff:
|
|
62
|
+
*.log
|
|
63
|
+
local_settings.py
|
|
64
|
+
db.sqlite3
|
|
65
|
+
db.sqlite3-journal
|
|
66
|
+
|
|
67
|
+
# Flask stuff:
|
|
68
|
+
instance/
|
|
69
|
+
.webassets-cache
|
|
70
|
+
|
|
71
|
+
# Scrapy stuff:
|
|
72
|
+
.scrapy
|
|
73
|
+
|
|
74
|
+
# Sphinx documentation
|
|
75
|
+
docs/_build/
|
|
76
|
+
|
|
77
|
+
# PyBuilder
|
|
78
|
+
.pybuilder/
|
|
79
|
+
target/
|
|
80
|
+
|
|
81
|
+
# Jupyter Notebook
|
|
82
|
+
.ipynb_checkpoints
|
|
83
|
+
|
|
84
|
+
# IPython
|
|
85
|
+
profile_default/
|
|
86
|
+
ipython_config.py
|
|
87
|
+
|
|
88
|
+
# pyenv
|
|
89
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
90
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
91
|
+
# .python-version
|
|
92
|
+
|
|
93
|
+
# pipenv
|
|
94
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
95
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
96
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
97
|
+
# install all needed dependencies.
|
|
98
|
+
# Pipfile.lock
|
|
99
|
+
|
|
100
|
+
# UV
|
|
101
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
102
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
103
|
+
# commonly ignored for libraries.
|
|
104
|
+
# uv.lock
|
|
105
|
+
|
|
106
|
+
# poetry
|
|
107
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
108
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
109
|
+
# commonly ignored for libraries.
|
|
110
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
111
|
+
# poetry.lock
|
|
112
|
+
# poetry.toml
|
|
113
|
+
|
|
114
|
+
# pdm
|
|
115
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
116
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
117
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
118
|
+
# pdm.lock
|
|
119
|
+
# pdm.toml
|
|
120
|
+
.pdm-python
|
|
121
|
+
.pdm-build/
|
|
122
|
+
|
|
123
|
+
# pixi
|
|
124
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
125
|
+
# pixi.lock
|
|
126
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
127
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
128
|
+
.pixi
|
|
129
|
+
|
|
130
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
131
|
+
__pypackages__/
|
|
132
|
+
|
|
133
|
+
# Celery stuff
|
|
134
|
+
celerybeat-schedule
|
|
135
|
+
celerybeat.pid
|
|
136
|
+
|
|
137
|
+
# Redis
|
|
138
|
+
*.rdb
|
|
139
|
+
*.aof
|
|
140
|
+
*.pid
|
|
141
|
+
|
|
142
|
+
# RabbitMQ
|
|
143
|
+
mnesia/
|
|
144
|
+
rabbitmq/
|
|
145
|
+
rabbitmq-data/
|
|
146
|
+
|
|
147
|
+
# ActiveMQ
|
|
148
|
+
activemq-data/
|
|
149
|
+
|
|
150
|
+
# SageMath parsed files
|
|
151
|
+
*.sage.py
|
|
152
|
+
|
|
153
|
+
# Environments
|
|
154
|
+
.env
|
|
155
|
+
.envrc
|
|
156
|
+
.venv
|
|
157
|
+
env/
|
|
158
|
+
venv/
|
|
159
|
+
ENV/
|
|
160
|
+
env.bak/
|
|
161
|
+
venv.bak/
|
|
162
|
+
|
|
163
|
+
# Spyder project settings
|
|
164
|
+
.spyderproject
|
|
165
|
+
.spyproject
|
|
166
|
+
|
|
167
|
+
# Rope project settings
|
|
168
|
+
.ropeproject
|
|
169
|
+
|
|
170
|
+
# mkdocs documentation
|
|
171
|
+
/site
|
|
172
|
+
|
|
173
|
+
# mypy
|
|
174
|
+
.mypy_cache/
|
|
175
|
+
.dmypy.json
|
|
176
|
+
dmypy.json
|
|
177
|
+
|
|
178
|
+
# Pyre type checker
|
|
179
|
+
.pyre/
|
|
180
|
+
|
|
181
|
+
# pytype static type analyzer
|
|
182
|
+
.pytype/
|
|
183
|
+
|
|
184
|
+
# Cython debug symbols
|
|
185
|
+
cython_debug/
|
|
186
|
+
|
|
187
|
+
# PyCharm
|
|
188
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
189
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
190
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
191
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
192
|
+
# .idea/
|
|
193
|
+
|
|
194
|
+
# Abstra
|
|
195
|
+
# Abstra is an AI-powered process automation framework.
|
|
196
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
197
|
+
# Learn more at https://abstra.io/docs
|
|
198
|
+
.abstra/
|
|
199
|
+
|
|
200
|
+
# Visual Studio Code
|
|
201
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
202
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
203
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
204
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
205
|
+
# .vscode/
|
|
206
|
+
|
|
207
|
+
# Ruff stuff:
|
|
208
|
+
.ruff_cache/
|
|
209
|
+
|
|
210
|
+
# PyPI configuration file
|
|
211
|
+
.pypirc
|
|
212
|
+
|
|
213
|
+
# Marimo
|
|
214
|
+
marimo/_static/
|
|
215
|
+
marimo/_lsp/
|
|
216
|
+
__marimo__/
|
|
217
|
+
|
|
218
|
+
# Streamlit
|
|
219
|
+
.streamlit/secrets.toml
|
|
220
|
+
|
|
221
|
+
# Node
|
|
222
|
+
node_modules/
|
|
223
|
+
jspm_packages/
|
|
224
|
+
.lock-wscript
|
|
225
|
+
build/Release
|
|
226
|
+
.node_repl_history
|
|
227
|
+
*.tgz
|
|
228
|
+
.npm
|
|
229
|
+
*.so
|
|
230
|
+
|
|
231
|
+
# Livemark
|
|
232
|
+
.livemark/build/
|
|
233
|
+
.tanstack/
|
|
234
|
+
.content-collections/
|
|
235
|
+
|
|
236
|
+
# User
|
|
237
|
+
/.claude/
|
|
238
|
+
/.cursor/
|
|
239
|
+
/coverage.json
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fairspec-mcp-server
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Fairspec Python is a fast data management framework built on top of the Fairspec standard and Polars DataFrames
|
|
5
|
+
Project-URL: homepage, https://fairspec.org/python/
|
|
6
|
+
Project-URL: repository, https://github.com/datisthq/fairspec
|
|
7
|
+
Author: Evgeny Karev
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: ai,data,dataframe,fair,fairspec,jsonschema,llm,mcp,polars,python,quality,tableschema,validation
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: fairspec-library
|
|
15
|
+
Requires-Dist: mcp>=2.3
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# fairspec-mcp-server
|
|
19
|
+
|
|
20
|
+
Fairspec Python is a fast data management framework built on top of the Fairspec standard and Polars DataFrames. It supports various formats like CSV, JSON, and Parquet and integrates with data platforms such as CKAN, Zenodo, and GitHub. For more information, please read the [project's documentation](https://fairspec.org/python/).
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# fairspec-mcp-server
|
|
2
|
+
|
|
3
|
+
Fairspec Python is a fast data management framework built on top of the Fairspec standard and Polars DataFrames. It supports various formats like CSV, JSON, and Parquet and integrates with data platforms such as CKAN, Zenodo, and GitHub. For more information, please read the [project's documentation](https://fairspec.org/python/).
|
|
File without changes
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from fairspec_library import write_temp_file
|
|
3
|
+
from mcp import Client
|
|
4
|
+
|
|
5
|
+
from ..server import server
|
|
6
|
+
|
|
7
|
+
pytestmark = pytest.mark.anyio
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def anyio_backend():
|
|
12
|
+
return "asyncio"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestServer:
|
|
16
|
+
async def test_should_list_tools_with_typescript_names(self):
|
|
17
|
+
async with Client(server) as client:
|
|
18
|
+
result = await client.list_tools()
|
|
19
|
+
|
|
20
|
+
assert sorted(tool.name for tool in result.tools) == sorted(
|
|
21
|
+
[
|
|
22
|
+
"inferDataSchemaTool",
|
|
23
|
+
"inferDatasetTool",
|
|
24
|
+
"inferFileDialectTool",
|
|
25
|
+
"inferTableSchemaTool",
|
|
26
|
+
"previewFileTool",
|
|
27
|
+
"queryDatasetTool",
|
|
28
|
+
"queryTableTool",
|
|
29
|
+
"validateDataSchemaTool",
|
|
30
|
+
"validateDatasetTool",
|
|
31
|
+
"validateDataTool",
|
|
32
|
+
"validateTableSchemaTool",
|
|
33
|
+
"validateTableTool",
|
|
34
|
+
]
|
|
35
|
+
)
|
|
36
|
+
for tool in result.tools:
|
|
37
|
+
assert tool.description
|
|
38
|
+
assert tool.output_schema
|
|
39
|
+
|
|
40
|
+
async def test_should_call_tool_with_structured_output(self, capfd):
|
|
41
|
+
path = write_temp_file("id,name\n1,alice\n2,bob\n", format="csv")
|
|
42
|
+
|
|
43
|
+
async with Client(server) as client:
|
|
44
|
+
result = await client.call_tool(
|
|
45
|
+
"inferTableSchemaTool", {"resource": {"data": path}}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert not result.is_error
|
|
49
|
+
assert capfd.readouterr().out == ""
|
|
50
|
+
assert result.structured_content == {
|
|
51
|
+
"tableSchema": {
|
|
52
|
+
"properties": {"id": {"type": "integer"}, "name": {"type": "string"}}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async def test_should_report_tool_failure_as_error(self):
|
|
57
|
+
async with Client(server) as client:
|
|
58
|
+
result = await client.call_tool(
|
|
59
|
+
"queryTableTool", {"resource": {}, "query": "SELECT * FROM self"}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert result.is_error
|
|
63
|
+
|
|
64
|
+
async def test_should_report_real_message_of_failed_query(self):
|
|
65
|
+
path = write_temp_file("id,name\n1,alice\n", format="csv")
|
|
66
|
+
|
|
67
|
+
async with Client(server) as client:
|
|
68
|
+
result = await client.call_tool(
|
|
69
|
+
"queryTableTool",
|
|
70
|
+
{"resource": {"data": path}, "query": "SELEC nonsense"},
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
assert result.is_error
|
|
74
|
+
assert "sql" in str(result.content).lower()
|
|
75
|
+
|
|
76
|
+
async def test_should_report_real_message_of_missing_file(self):
|
|
77
|
+
async with Client(server) as client:
|
|
78
|
+
result = await client.call_tool(
|
|
79
|
+
"validateDatasetTool", {"source": "missing/dataset.json"}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert result.is_error
|
|
83
|
+
assert "missing" in str(result.content)
|
|
84
|
+
|
|
85
|
+
async def test_should_reject_response_over_budget(self):
|
|
86
|
+
async with Client(server) as client:
|
|
87
|
+
result = await client.call_tool(
|
|
88
|
+
"inferDatasetTool",
|
|
89
|
+
{"dataset": {"resources": [{"data": [{"text": "x" * 60_000}]}]}},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
assert result.is_error
|
|
93
|
+
assert "exceeds 50000 characters" in str(result.content)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from fairspec_library import get_host_policy, set_host_policy
|
|
5
|
+
|
|
6
|
+
from ..server import start_server
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.fixture(autouse=True)
|
|
10
|
+
def reset_host_policy(monkeypatch):
|
|
11
|
+
monkeypatch.delenv("FAIRSPEC_HOST_POLICY", raising=False)
|
|
12
|
+
yield
|
|
13
|
+
set_host_policy(None)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestStartServer:
|
|
17
|
+
def test_should_block_non_public_hosts_by_default(self):
|
|
18
|
+
with patch("fairspec_mcp_server.server.server.run") as run:
|
|
19
|
+
start_server()
|
|
20
|
+
|
|
21
|
+
run.assert_called_once_with("stdio")
|
|
22
|
+
assert get_host_policy() == "public"
|
|
23
|
+
|
|
24
|
+
def test_should_keep_host_policy_from_env(self, monkeypatch):
|
|
25
|
+
monkeypatch.setenv("FAIRSPEC_HOST_POLICY", "any")
|
|
26
|
+
with patch("fairspec_mcp_server.server.server.run"):
|
|
27
|
+
start_server()
|
|
28
|
+
|
|
29
|
+
assert get_host_policy() == "any"
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
from fairspec_library import Report
|
|
4
|
+
from pydantic_core import to_json
|
|
5
|
+
|
|
6
|
+
DEFAULT_LIMIT = 100
|
|
7
|
+
MAX_LIMIT = 1000
|
|
8
|
+
MAX_RESPONSE_CHARS = 50_000
|
|
9
|
+
|
|
10
|
+
# room for the object a trimmed list is wrapped in
|
|
11
|
+
MAX_ITEMS_CHARS = MAX_RESPONSE_CHARS - 1_000
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def limit_items[T](items: Sequence[T], *, limit: int) -> tuple[list[T], bool]:
|
|
15
|
+
"""Cut a list to `limit` items, then to as many as fit the response budget.
|
|
16
|
+
|
|
17
|
+
Pass one item more than the limit to learn whether the list was truncated.
|
|
18
|
+
"""
|
|
19
|
+
chars = 0
|
|
20
|
+
count = 0
|
|
21
|
+
for item in items[:limit]:
|
|
22
|
+
chars += len(to_json(item, by_alias=True, exclude_none=True)) + 1
|
|
23
|
+
if chars > MAX_ITEMS_CHARS:
|
|
24
|
+
break
|
|
25
|
+
count += 1
|
|
26
|
+
|
|
27
|
+
return list(items[:count]), count < len(items)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def limit_report(report: Report, *, max_errors: int | None) -> tuple[Report, bool]:
|
|
31
|
+
"""Cut a report's errors to `max_errors`, then to the response budget."""
|
|
32
|
+
limit = max_errors if max_errors is not None else DEFAULT_LIMIT
|
|
33
|
+
errors, truncated = limit_items(report.errors, limit=limit)
|
|
34
|
+
return Report(valid=report.valid, errors=errors), truncated
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
5
|
+
from mcp.shared.exceptions import MCPError
|
|
6
|
+
from mcp.types import CallToolResult, TextContent
|
|
7
|
+
|
|
8
|
+
from ..models.tool import ToolResult
|
|
9
|
+
from .limit import MAX_RESPONSE_CHARS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def guard_tool[**P](tool: Callable[P, ToolResult]) -> Callable[P, CallToolResult]:
|
|
13
|
+
"""Pass a tool's failure to the client with its message, and cap its response size."""
|
|
14
|
+
|
|
15
|
+
# MCPServer reads the signature through `__wrapped__`, so the schemas stay the tool's
|
|
16
|
+
@functools.wraps(tool)
|
|
17
|
+
def guarded_tool(*args: P.args, **kwargs: P.kwargs) -> CallToolResult:
|
|
18
|
+
try:
|
|
19
|
+
result = tool(*args, **kwargs)
|
|
20
|
+
except (ToolError, MCPError):
|
|
21
|
+
raise
|
|
22
|
+
except Exception as exc:
|
|
23
|
+
# The SDK hides the message of anything but a ToolError
|
|
24
|
+
raise ToolError(str(exc)) from exc
|
|
25
|
+
|
|
26
|
+
text = result.model_dump_json(by_alias=True)
|
|
27
|
+
if len(text) > MAX_RESPONSE_CHARS:
|
|
28
|
+
raise ToolError(
|
|
29
|
+
f"The response exceeds {MAX_RESPONSE_CHARS} characters; narrow the input"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# The SDK would indent the text copy, doubling its size against the budget
|
|
33
|
+
return CallToolResult(
|
|
34
|
+
content=[TextContent(type="text", text=text)],
|
|
35
|
+
structured_content=result.model_dump(mode="json", by_alias=True),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return guarded_tool
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fairspec_library import FairspecModel
|
|
2
|
+
from pydantic import SerializationInfo, SerializerFunctionWrapHandler, model_serializer
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ToolResult(FairspecModel):
|
|
6
|
+
"""The structured output of a tool, serialized without unset optional fields."""
|
|
7
|
+
|
|
8
|
+
# mcp dumps tool output without exclude_none, so every unset field would reach the client as null
|
|
9
|
+
@model_serializer(mode="wrap")
|
|
10
|
+
def serialize_tool_result(
|
|
11
|
+
self, handler: SerializerFunctionWrapHandler, info: SerializationInfo
|
|
12
|
+
):
|
|
13
|
+
if info.exclude_none:
|
|
14
|
+
return handler(self)
|
|
15
|
+
return self.model_dump(mode=info.mode, by_alias=info.by_alias, exclude_none=True)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
|
|
4
|
+
from fairspec_library import set_host_policy
|
|
5
|
+
from mcp.server.mcpserver import MCPServer
|
|
6
|
+
|
|
7
|
+
from .helpers.tool import guard_tool
|
|
8
|
+
from .tools.data.validate import validate_data_tool
|
|
9
|
+
from .tools.data_schema.infer import infer_data_schema_tool
|
|
10
|
+
from .tools.data_schema.validate import validate_data_schema_tool
|
|
11
|
+
from .tools.dataset.infer import infer_dataset_tool
|
|
12
|
+
from .tools.dataset.query import query_dataset_tool
|
|
13
|
+
from .tools.dataset.validate import validate_dataset_tool
|
|
14
|
+
from .tools.file.preview import preview_file_tool
|
|
15
|
+
from .tools.file_dialect.infer import infer_file_dialect_tool
|
|
16
|
+
from .tools.table.query import query_table_tool
|
|
17
|
+
from .tools.table.validate import validate_table_tool
|
|
18
|
+
from .tools.table_schema.infer import infer_table_schema_tool
|
|
19
|
+
from .tools.table_schema.validate import validate_table_schema_tool
|
|
20
|
+
|
|
21
|
+
server = MCPServer(name="fairspec", version=version("fairspec-mcp-server"))
|
|
22
|
+
|
|
23
|
+
server.add_tool(guard_tool(infer_data_schema_tool), name="inferDataSchemaTool")
|
|
24
|
+
server.add_tool(guard_tool(infer_dataset_tool), name="inferDatasetTool")
|
|
25
|
+
server.add_tool(guard_tool(infer_file_dialect_tool), name="inferFileDialectTool")
|
|
26
|
+
server.add_tool(guard_tool(infer_table_schema_tool), name="inferTableSchemaTool")
|
|
27
|
+
server.add_tool(guard_tool(preview_file_tool), name="previewFileTool")
|
|
28
|
+
server.add_tool(guard_tool(query_dataset_tool), name="queryDatasetTool")
|
|
29
|
+
server.add_tool(guard_tool(query_table_tool), name="queryTableTool")
|
|
30
|
+
server.add_tool(guard_tool(validate_data_schema_tool), name="validateDataSchemaTool")
|
|
31
|
+
server.add_tool(guard_tool(validate_dataset_tool), name="validateDatasetTool")
|
|
32
|
+
server.add_tool(guard_tool(validate_data_tool), name="validateDataTool")
|
|
33
|
+
server.add_tool(guard_tool(validate_table_schema_tool), name="validateTableSchemaTool")
|
|
34
|
+
server.add_tool(guard_tool(validate_table_tool), name="validateTableTool")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def start_server() -> None:
|
|
38
|
+
"""Serve over stdio; remote fetches reach only public hosts unless FAIRSPEC_HOST_POLICY is set."""
|
|
39
|
+
if not os.environ.get("FAIRSPEC_HOST_POLICY"):
|
|
40
|
+
set_host_policy("public")
|
|
41
|
+
server.run("stdio")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
|
|
5
|
+
from ...helpers.limit import DEFAULT_LIMIT, MAX_LIMIT
|
|
6
|
+
|
|
7
|
+
# Tool parameters carry their wire name (`maxErrors`): the SDK passes an alias as the
|
|
8
|
+
# keyword, so a snake_case parameter cannot be aliased to match the TypeScript server
|
|
9
|
+
MaxErrors = Annotated[
|
|
10
|
+
int | None,
|
|
11
|
+
Field(
|
|
12
|
+
ge=1,
|
|
13
|
+
le=MAX_LIMIT,
|
|
14
|
+
description=f"Maximum number of errors to return (default {DEFAULT_LIMIT})",
|
|
15
|
+
),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
Truncated = Annotated[
|
|
19
|
+
bool, Field(description="Whether errors were left out of the report")
|
|
20
|
+
]
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from fairspec_library import Resource
|
|
2
|
+
|
|
3
|
+
from ..validate import validate_data_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestValidateDataTool:
|
|
7
|
+
def test_should_validate_data_against_schema(self):
|
|
8
|
+
resource = Resource(
|
|
9
|
+
data={"name": "alice"},
|
|
10
|
+
dataSchema={"type": "object", "properties": {"name": {"type": "string"}}},
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
result = validate_data_tool(resource)
|
|
14
|
+
|
|
15
|
+
assert result.report.valid is True
|
|
16
|
+
|
|
17
|
+
def test_should_detect_invalid_data(self):
|
|
18
|
+
resource = Resource(
|
|
19
|
+
data={"name": 1},
|
|
20
|
+
dataSchema={"type": "object", "properties": {"name": {"type": "string"}}},
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
result = validate_data_tool(resource)
|
|
24
|
+
|
|
25
|
+
assert result.report.valid is False
|
|
26
|
+
assert len(result.report.errors) > 0
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from fairspec_library import Report, Resource, validate_data
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from ...helpers.limit import limit_report
|
|
7
|
+
from ...models.tool import ToolResult
|
|
8
|
+
from .._shared.schemas import MaxErrors, Truncated
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ValidateDataResult(ToolResult):
|
|
12
|
+
report: Report
|
|
13
|
+
truncated: Truncated
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_data_tool(
|
|
17
|
+
resource: Annotated[Resource, Field(description="The data resource to validate")],
|
|
18
|
+
maxErrors: MaxErrors = None,
|
|
19
|
+
) -> ValidateDataResult:
|
|
20
|
+
"""Validate data against a data schema (JSON Schema). Returns a validation report with errors if any."""
|
|
21
|
+
report, truncated = limit_report(validate_data(resource), max_errors=maxErrors)
|
|
22
|
+
return ValidateDataResult(report=report, truncated=truncated)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from fairspec_library import Resource
|
|
2
|
+
|
|
3
|
+
from ..infer import infer_data_schema_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestInferDataSchemaTool:
|
|
7
|
+
def test_should_infer_schema_from_inline_data(self):
|
|
8
|
+
resource = Resource(data={"name": "alice", "age": 25})
|
|
9
|
+
|
|
10
|
+
result = infer_data_schema_tool(resource)
|
|
11
|
+
|
|
12
|
+
assert result.dataSchema is not None
|
|
13
|
+
assert result.dataSchema["type"] == "object"
|
|
14
|
+
assert set(result.dataSchema["properties"]) == {"name", "age"}
|
|
15
|
+
|
|
16
|
+
def test_should_return_none_for_resource_without_data(self):
|
|
17
|
+
result = infer_data_schema_tool(Resource())
|
|
18
|
+
|
|
19
|
+
assert result.dataSchema is None
|
|
20
|
+
assert result.model_dump(mode="json", by_alias=True) == {}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from fairspec_library import Report, write_temp_file
|
|
4
|
+
|
|
5
|
+
from ..validate import validate_data_schema_tool
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestValidateDataSchemaTool:
|
|
9
|
+
def test_should_validate_valid_descriptor(self):
|
|
10
|
+
result = validate_data_schema_tool({"type": "object"})
|
|
11
|
+
|
|
12
|
+
assert result.report.valid is True
|
|
13
|
+
assert result.report.errors == []
|
|
14
|
+
|
|
15
|
+
def test_should_detect_invalid_descriptor(self):
|
|
16
|
+
result = validate_data_schema_tool({"type": 123})
|
|
17
|
+
|
|
18
|
+
assert result.report.valid is False
|
|
19
|
+
assert len(result.report.errors) > 0
|
|
20
|
+
|
|
21
|
+
def test_should_validate_descriptor_from_path(self):
|
|
22
|
+
path = write_temp_file(json.dumps({"type": "object"}), format="json")
|
|
23
|
+
|
|
24
|
+
result = validate_data_schema_tool(path)
|
|
25
|
+
|
|
26
|
+
assert result.report.valid is True
|
|
27
|
+
|
|
28
|
+
def test_should_return_plain_report(self):
|
|
29
|
+
result = validate_data_schema_tool({"type": "object"})
|
|
30
|
+
|
|
31
|
+
assert type(result.report) is Report
|