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.
Files changed (55) hide show
  1. fairspec_mcp_server-0.3.0/.gitignore +239 -0
  2. fairspec_mcp_server-0.3.0/PKG-INFO +20 -0
  3. fairspec_mcp_server-0.3.0/README.md +3 -0
  4. fairspec_mcp_server-0.3.0/fairspec_mcp_server/__init__.py +2 -0
  5. fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/__init__.py +0 -0
  6. fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/server_unit.py +93 -0
  7. fairspec_mcp_server-0.3.0/fairspec_mcp_server/_test/start_unit.py +29 -0
  8. fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/__init__.py +0 -0
  9. fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/limit.py +34 -0
  10. fairspec_mcp_server-0.3.0/fairspec_mcp_server/helpers/tool.py +38 -0
  11. fairspec_mcp_server-0.3.0/fairspec_mcp_server/models/__init__.py +0 -0
  12. fairspec_mcp_server-0.3.0/fairspec_mcp_server/models/tool.py +15 -0
  13. fairspec_mcp_server-0.3.0/fairspec_mcp_server/server.py +41 -0
  14. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/__init__.py +0 -0
  15. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/_shared/__init__.py +0 -0
  16. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/_shared/schemas.py +20 -0
  17. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/__init__.py +0 -0
  18. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/_test/__init__.py +0 -0
  19. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/_test/validate_unit.py +26 -0
  20. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data/validate.py +22 -0
  21. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/__init__.py +0 -0
  22. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/__init__.py +0 -0
  23. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/infer_unit.py +20 -0
  24. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/_test/validate_unit.py +31 -0
  25. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/infer.py +19 -0
  26. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/data_schema/validate.py +25 -0
  27. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/__init__.py +0 -0
  28. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/__init__.py +0 -0
  29. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/infer_unit.py +23 -0
  30. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/query_unit.py +39 -0
  31. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/_test/validate_unit.py +53 -0
  32. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/infer.py +17 -0
  33. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/query.py +38 -0
  34. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/dataset/validate.py +25 -0
  35. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/__init__.py +0 -0
  36. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/_test/__init__.py +0 -0
  37. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/_test/preview_unit.py +33 -0
  38. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file/preview.py +27 -0
  39. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/__init__.py +0 -0
  40. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/_test/__init__.py +0 -0
  41. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/_test/infer_unit.py +29 -0
  42. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/file_dialect/infer.py +28 -0
  43. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/__init__.py +0 -0
  44. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/__init__.py +0 -0
  45. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/query_unit.py +85 -0
  46. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/_test/validate_unit.py +98 -0
  47. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/query.py +39 -0
  48. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table/validate.py +59 -0
  49. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/__init__.py +0 -0
  50. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/__init__.py +0 -0
  51. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/infer_unit.py +42 -0
  52. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/_test/validate_unit.py +33 -0
  53. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/infer.py +28 -0
  54. fairspec_mcp_server-0.3.0/fairspec_mcp_server/tools/table_schema/validate.py +27 -0
  55. 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/).
@@ -0,0 +1,2 @@
1
+ from .server import server as server
2
+ from .server import start_server as start_server
@@ -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"
@@ -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
@@ -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")
@@ -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
+ ]
@@ -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)
@@ -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