sdcreader 4.1.0__tar.gz → 4.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.
- {sdcreader-4.1.0 → sdcreader-4.3.0}/NOTICE +5 -0
- {sdcreader-4.1.0/src/sdcreader.egg-info → sdcreader-4.3.0}/PKG-INFO +22 -2
- {sdcreader-4.1.0 → sdcreader-4.3.0}/README.md +21 -1
- {sdcreader-4.1.0 → sdcreader-4.3.0}/pyproject.toml +1 -1
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader/__init__.py +7 -2
- sdcreader-4.3.0/src/sdcreader/mcp.py +245 -0
- sdcreader-4.3.0/src/sdcreader/mcp_server.py +393 -0
- sdcreader-4.3.0/src/sdcreader/record.py +180 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0/src/sdcreader.egg-info}/PKG-INFO +22 -2
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader.egg-info/SOURCES.txt +5 -1
- sdcreader-4.3.0/tests/test_mcp_scaffold.py +88 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/tests/test_mcp_server.py +32 -3
- sdcreader-4.3.0/tests/test_record.py +130 -0
- sdcreader-4.1.0/src/sdcreader/mcp_server.py +0 -496
- {sdcreader-4.1.0 → sdcreader-4.3.0}/LICENSE +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/setup.cfg +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader/model.py +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader/package.py +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader.egg-info/dependency_links.txt +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader.egg-info/entry_points.txt +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader.egg-info/requires.txt +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/src/sdcreader.egg-info/top_level.txt +0 -0
- {sdcreader-4.1.0 → sdcreader-4.3.0}/tests/test_reader.py +0 -0
|
@@ -7,3 +7,8 @@ The test fixture under samples/nhanes-participant/ is the published package of t
|
|
|
7
7
|
FAIR Data Demo (https://github.com/SemanticDataCharter/FAIR_Data_Demo), fetched from SDCStudio's public catalog, a
|
|
8
8
|
Semantic Data Charter model of National Health and Nutrition Examination Survey (CDC) variables; it is used as
|
|
9
9
|
evidence, unchanged.
|
|
10
|
+
|
|
11
|
+
The test fixture under samples/halvorsen-credit-note/ is the published package of the Credit Note model of the
|
|
12
|
+
business documents library and one Credit Note record generated by HalvorsenDemo
|
|
13
|
+
(https://github.com/SemanticDataCharter/HalvorsenDemo, Apache 2.0), whose companies and documents are fictitious; it
|
|
14
|
+
is used as evidence, unchanged.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcreader
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.0
|
|
4
4
|
Summary: Read a published Semantic Data Charter model's package for projection into other standards (CDIF, DCAT-US, DCAT-AP, HealthDCAT-AP); MCP server included.
|
|
5
5
|
Author: Axius SDC, Inc.
|
|
6
6
|
License: Apache-2.0
|
|
@@ -89,6 +89,25 @@ which is complete.
|
|
|
89
89
|
- Constants: the leaf, numeric and quantified types; the predicate IRIs `IDENTIFIER`, `SOURCE`, `PUBLISHER`, `EXACT`,
|
|
90
90
|
`CLOSE`, `SEE_ALSO`, `HAS_UNIT`; `DC_DEFAULTS`.
|
|
91
91
|
|
|
92
|
+
## Reading a record (4.3.0)
|
|
93
|
+
|
|
94
|
+
`read_record(source, model)` reads a Governed Data Record, the XML instance, and joins each value to the component
|
|
95
|
+
that defines it. `source` is the record's bytes, its text or a path; a record of another model is refused
|
|
96
|
+
(`RecordError`), as is anything that is not an `sdc4:dm-<ct_id>` record.
|
|
97
|
+
|
|
98
|
+
- **`Record`**: `attributes` (`instance_id`, `instance_version`, `creation_timestamp`, `current-state`, ...),
|
|
99
|
+
`values` in document order, and `exceptional`, the values that carry an Exceptional Value.
|
|
100
|
+
- **`RecordValue`**: the `component`, its `path` of clusters (root first), an `occurrence` index for a component
|
|
101
|
+
repeated under the same path, its `position`, its `elements` as the record holds them (a quantity's units nested:
|
|
102
|
+
`{"xdquantity-value": "1", "xdquantity-units": {"label": "Trade Units", "xdstring-value": "EA"}}`), and its
|
|
103
|
+
`context` (`act`, `vtb`, `vte`, `tr`, `modified`, `latitude`, `longitude`).
|
|
104
|
+
- **`ExceptionalValue`**: the ISO 21090 null flavor (`code`, `name`) and its `origin`: `source` when it stands in
|
|
105
|
+
place of the value, `recovery` when sdcvalidator's recovery inserted it beside a value that failed its constraint.
|
|
106
|
+
For `recovery`, `RecordValue.rejected` is that value, kept so a person can see it.
|
|
107
|
+
|
|
108
|
+
Cluster members sit inside adapter elements that are not components of the model; the reader passes through them.
|
|
109
|
+
It does not type, validate or interpret values: that is the caller's work (a mapper, an exporter, a checker).
|
|
110
|
+
|
|
92
111
|
## MCP server
|
|
93
112
|
|
|
94
113
|
`sdcreader` is also an MCP server, registered as `io.github.SemanticDataCharter/sdcreader`, so an assistant can read a
|
|
@@ -107,8 +126,9 @@ uvx sdcreader --host https://your-sdcstudio.example # a sovereign SDCStudio's
|
|
|
107
126
|
| `list_leaves` | the leaves in document order with path, type, constraints, code count and identity links, paged |
|
|
108
127
|
| `list_codes` | one component's enumerated values with IRI, definition and code system |
|
|
109
128
|
| `check_package` | the reader's integrity check on a package directory |
|
|
129
|
+
| `read_record` | a Governed Data Record on disk read against its model: attributes, then each value with its component, path, elements, context and Exceptional Value, paged; the model is the one the record names unless a source is given |
|
|
110
130
|
|
|
111
|
-
Each tool takes a `ct_id` (fetched from the host the server was started with) or a package `directory` on disk.
|
|
131
|
+
Each model tool takes a `ct_id` (fetched from the host the server was started with) or a package `directory` on disk.
|
|
112
132
|
No tool takes a URL: the host is fixed at start, so a caller cannot point the server elsewhere. Every tool calls the
|
|
113
133
|
same functions as the library; the two interfaces describe one model the same way.
|
|
114
134
|
|
|
@@ -64,6 +64,25 @@ which is complete.
|
|
|
64
64
|
- Constants: the leaf, numeric and quantified types; the predicate IRIs `IDENTIFIER`, `SOURCE`, `PUBLISHER`, `EXACT`,
|
|
65
65
|
`CLOSE`, `SEE_ALSO`, `HAS_UNIT`; `DC_DEFAULTS`.
|
|
66
66
|
|
|
67
|
+
## Reading a record (4.3.0)
|
|
68
|
+
|
|
69
|
+
`read_record(source, model)` reads a Governed Data Record, the XML instance, and joins each value to the component
|
|
70
|
+
that defines it. `source` is the record's bytes, its text or a path; a record of another model is refused
|
|
71
|
+
(`RecordError`), as is anything that is not an `sdc4:dm-<ct_id>` record.
|
|
72
|
+
|
|
73
|
+
- **`Record`**: `attributes` (`instance_id`, `instance_version`, `creation_timestamp`, `current-state`, ...),
|
|
74
|
+
`values` in document order, and `exceptional`, the values that carry an Exceptional Value.
|
|
75
|
+
- **`RecordValue`**: the `component`, its `path` of clusters (root first), an `occurrence` index for a component
|
|
76
|
+
repeated under the same path, its `position`, its `elements` as the record holds them (a quantity's units nested:
|
|
77
|
+
`{"xdquantity-value": "1", "xdquantity-units": {"label": "Trade Units", "xdstring-value": "EA"}}`), and its
|
|
78
|
+
`context` (`act`, `vtb`, `vte`, `tr`, `modified`, `latitude`, `longitude`).
|
|
79
|
+
- **`ExceptionalValue`**: the ISO 21090 null flavor (`code`, `name`) and its `origin`: `source` when it stands in
|
|
80
|
+
place of the value, `recovery` when sdcvalidator's recovery inserted it beside a value that failed its constraint.
|
|
81
|
+
For `recovery`, `RecordValue.rejected` is that value, kept so a person can see it.
|
|
82
|
+
|
|
83
|
+
Cluster members sit inside adapter elements that are not components of the model; the reader passes through them.
|
|
84
|
+
It does not type, validate or interpret values: that is the caller's work (a mapper, an exporter, a checker).
|
|
85
|
+
|
|
67
86
|
## MCP server
|
|
68
87
|
|
|
69
88
|
`sdcreader` is also an MCP server, registered as `io.github.SemanticDataCharter/sdcreader`, so an assistant can read a
|
|
@@ -82,8 +101,9 @@ uvx sdcreader --host https://your-sdcstudio.example # a sovereign SDCStudio's
|
|
|
82
101
|
| `list_leaves` | the leaves in document order with path, type, constraints, code count and identity links, paged |
|
|
83
102
|
| `list_codes` | one component's enumerated values with IRI, definition and code system |
|
|
84
103
|
| `check_package` | the reader's integrity check on a package directory |
|
|
104
|
+
| `read_record` | a Governed Data Record on disk read against its model: attributes, then each value with its component, path, elements, context and Exceptional Value, paged; the model is the one the record names unless a source is given |
|
|
85
105
|
|
|
86
|
-
Each tool takes a `ct_id` (fetched from the host the server was started with) or a package `directory` on disk.
|
|
106
|
+
Each model tool takes a `ct_id` (fetched from the host the server was started with) or a package `directory` on disk.
|
|
87
107
|
No tool takes a URL: the host is fixed at start, so a caller cannot point the server elsewhere. Every tool calls the
|
|
88
108
|
same functions as the library; the two interfaces describe one model the same way.
|
|
89
109
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcreader"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.3.0"
|
|
8
8
|
description = "Read a published Semantic Data Charter model's package for projection into other standards (CDIF, DCAT-US, DCAT-AP, HealthDCAT-AP); MCP server included."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -7,12 +7,17 @@ versions. This package loads or fetches that evidence and reads it into one shap
|
|
|
7
7
|
record tree, its leaves in document order, the enumerated values with the codes the schema annotates them with,
|
|
8
8
|
and the model's own Dublin Core with SDCStudio's field defaults read as unset. It knows nothing about any
|
|
9
9
|
projection.
|
|
10
|
+
|
|
11
|
+
Since 4.3.0 it also reads a Governed Data Record itself (`read_record`): the record's attributes and each value joined
|
|
12
|
+
to the component that defines it, Exceptional Values included, for record-level mappers.
|
|
10
13
|
"""
|
|
11
14
|
from .model import (CLOSE, DC_DEFAULTS, EXACT, HAS_UNIT, IDENTIFIER, LEAF_TYPES, NUMERIC_TYPES, PUBLISHER, QUANTIFIED_TYPES, SEE_ALSO,
|
|
12
15
|
SOURCE, Code, Component, Leaf, Model, enumeration_iri, read_header, read_model)
|
|
13
16
|
from .package import DEFAULT_HOST, ModelPackage, PackageError, fetch_package, load_package
|
|
17
|
+
from .record import CONTEXT_ELEMENTS, EXCEPTIONAL_VALUES, RECORD_ATTRIBUTES, ExceptionalValue, Record, RecordError, RecordValue, read_record
|
|
14
18
|
|
|
15
|
-
__version__ = "4.
|
|
19
|
+
__version__ = "4.3.0"
|
|
16
20
|
__all__ = ["ModelPackage", "PackageError", "DEFAULT_HOST", "load_package", "fetch_package", "read_model", "read_header", "enumeration_iri",
|
|
17
21
|
"Model", "Component", "Leaf", "Code", "LEAF_TYPES", "NUMERIC_TYPES", "QUANTIFIED_TYPES", "DC_DEFAULTS", "IDENTIFIER", "SOURCE",
|
|
18
|
-
"PUBLISHER", "EXACT", "CLOSE", "SEE_ALSO", "HAS_UNIT", "
|
|
22
|
+
"PUBLISHER", "EXACT", "CLOSE", "SEE_ALSO", "HAS_UNIT", "read_record", "Record", "RecordValue", "RecordError", "ExceptionalValue",
|
|
23
|
+
"EXCEPTIONAL_VALUES", "RECORD_ATTRIBUTES", "CONTEXT_ELEMENTS", "__version__"]
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright (c) 2026, Axius SDC, Inc.
|
|
3
|
+
# Licensed under the Apache License, Version 2.0.
|
|
4
|
+
#
|
|
5
|
+
"""
|
|
6
|
+
The MCP scaffold the SDC servers share: JSON-RPC 2.0 over stdio, one message
|
|
7
|
+
per line, no SDK.
|
|
8
|
+
|
|
9
|
+
`sdcvalidator`, `sdcgovernance`, `sdcreceipt` and `sdcreader` each carried this
|
|
10
|
+
by hand. The four projection servers would have made eight copies, so it lives
|
|
11
|
+
here once, as the reader itself does: `sdcreader` is the package every
|
|
12
|
+
projection already depends on.
|
|
13
|
+
|
|
14
|
+
What a server built on this gets, and must not re-implement:
|
|
15
|
+
|
|
16
|
+
- `initialize` that negotiates rather than asserts: a client asking for a
|
|
17
|
+
revision in `SUPPORTED_PROTOCOL_VERSIONS` gets it echoed; anything else gets
|
|
18
|
+
the newest and decides for itself. 2026-07-28 removes the initialize
|
|
19
|
+
handshake and is a different architecture, so it is not claimed.
|
|
20
|
+
- Tool failures as `isError` results (SEP-1303), never JSON-RPC errors, so the
|
|
21
|
+
calling model sees what went wrong and can correct itself. A `ToolError`
|
|
22
|
+
is the recoverable kind and travels with its message; anything else is a
|
|
23
|
+
defect and travels without its traceback.
|
|
24
|
+
- `-32600` for a request or `params` that is not an object, `-32602` for
|
|
25
|
+
`arguments` that is not an object, `-32601` for an unknown method or tool,
|
|
26
|
+
`-32700` for a line that is not JSON.
|
|
27
|
+
- `PackageCache`: the `ct_id`-or-`directory` source argument every reader-based
|
|
28
|
+
tool takes, resolved once per process. A published package is immutable, so
|
|
29
|
+
caching it for the life of the server is safe.
|
|
30
|
+
|
|
31
|
+
No tool built on this may take a URL as an argument. The catalog host is fixed
|
|
32
|
+
when the server starts, so a caller cannot point a server at an arbitrary host.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
import sys
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any, Callable
|
|
41
|
+
|
|
42
|
+
from sdcreader.model import Model, read_model
|
|
43
|
+
from sdcreader.package import DEFAULT_HOST, ModelPackage, PackageError, fetch_package, load_package
|
|
44
|
+
|
|
45
|
+
JSONRPC_VERSION = "2.0"
|
|
46
|
+
|
|
47
|
+
#: Newest revision this scaffold implements, and the set it negotiates.
|
|
48
|
+
MCP_PROTOCOL_VERSION = "2025-11-25"
|
|
49
|
+
SUPPORTED_PROTOCOL_VERSIONS = ("2025-06-18", "2025-11-25")
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"JSONRPC_VERSION", "MCP_PROTOCOL_VERSION", "SUPPORTED_PROTOCOL_VERSIONS",
|
|
53
|
+
"ToolError", "Server", "PackageCache", "SOURCE_ARGS", "source_properties", "int_arg", "str_arg",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ToolError(Exception):
|
|
58
|
+
"""A tool failure the calling model can act on: bad input, or a package that does not hold up."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
#: The source argument every reader-based tool takes: one model by identifier (fetched from the fixed host) or one
|
|
62
|
+
#: package directory on disk, never both.
|
|
63
|
+
SOURCE_ARGS = {
|
|
64
|
+
"ct_id": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "The model's identifier (CUID2) in the public catalog; the package is fetched from the host this server was started with.",
|
|
67
|
+
},
|
|
68
|
+
"directory": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "A package directory already on disk (dm-<ct>.jsonld and dm-<ct>.xsd, optionally catalog.json and versions.json). Give this or ct_id, not both.",
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def source_properties(plural: bool = False) -> dict:
|
|
76
|
+
"""The JSON Schema properties for the source argument; `plural` gives the list forms a multi-model catalog takes."""
|
|
77
|
+
if not plural:
|
|
78
|
+
return dict(SOURCE_ARGS)
|
|
79
|
+
return {
|
|
80
|
+
"ct_ids": {"type": "array", "items": {"type": "string"}, "minItems": 1,
|
|
81
|
+
"description": "Model identifiers (CUID2) in the public catalog, fetched from the host this server was started with."},
|
|
82
|
+
"directories": {"type": "array", "items": {"type": "string"}, "minItems": 1,
|
|
83
|
+
"description": "Package directories on disk. Give ct_ids or directories or both; at least one model."},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def int_arg(args: dict[str, Any], name: str, default: int, lo: int, hi: int) -> int:
|
|
88
|
+
v = args.get(name, default)
|
|
89
|
+
if isinstance(v, bool) or not isinstance(v, int):
|
|
90
|
+
raise ToolError(f"`{name}` must be an integer.")
|
|
91
|
+
if v < lo or v > hi:
|
|
92
|
+
raise ToolError(f"`{name}` must be between {lo} and {hi}.")
|
|
93
|
+
return v
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def str_arg(args: dict[str, Any], name: str, default: str | None = None, required: bool = False) -> str | None:
|
|
97
|
+
v = args.get(name, default)
|
|
98
|
+
if v is None:
|
|
99
|
+
if required:
|
|
100
|
+
raise ToolError(f"`{name}` is required.")
|
|
101
|
+
return None
|
|
102
|
+
if not isinstance(v, str):
|
|
103
|
+
raise ToolError(f"`{name}` must be a string.")
|
|
104
|
+
return v
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PackageCache:
|
|
108
|
+
"""Packages fetched or loaded in this process, by source, read once."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, host: str = DEFAULT_HOST, storage: dict | None = None, fetcher: Callable | None = None, loader: Callable | None = None):
|
|
111
|
+
self.host = host.rstrip("/")
|
|
112
|
+
self.storage: dict[str, tuple[ModelPackage, Model]] = storage if storage is not None else {}
|
|
113
|
+
# Injectable so a server module can expose `fetch_package` as its own patchable name for tests.
|
|
114
|
+
self.fetcher = fetcher or (lambda ct_id, host: fetch_package(ct_id, host=host))
|
|
115
|
+
self.loader = loader or (lambda directory, host: load_package(directory, host=host))
|
|
116
|
+
|
|
117
|
+
def get(self, ct_id: str | None = None, directory: str | None = None) -> tuple[ModelPackage, Model]:
|
|
118
|
+
if bool(ct_id) == bool(directory):
|
|
119
|
+
raise ToolError("Give exactly one of `ct_id` (fetched from the catalog) or `directory` (a package on disk).")
|
|
120
|
+
if ct_id is not None and not isinstance(ct_id, str):
|
|
121
|
+
raise ToolError("`ct_id` must be a string.")
|
|
122
|
+
if directory is not None and not isinstance(directory, str):
|
|
123
|
+
raise ToolError("`directory` must be a string path.")
|
|
124
|
+
key = ct_id or f"dir:{Path(directory).resolve()}"
|
|
125
|
+
if key in self.storage:
|
|
126
|
+
return self.storage[key]
|
|
127
|
+
try:
|
|
128
|
+
pkg = self.fetcher(ct_id, self.host) if ct_id else self.loader(directory, self.host)
|
|
129
|
+
model = read_model(pkg)
|
|
130
|
+
except PackageError as exc:
|
|
131
|
+
raise ToolError(str(exc)) from exc
|
|
132
|
+
except OSError as exc:
|
|
133
|
+
raise ToolError(f"Could not read the package: {exc}") from exc
|
|
134
|
+
self.storage[key] = (pkg, model)
|
|
135
|
+
return pkg, model
|
|
136
|
+
|
|
137
|
+
def one(self, args: dict[str, Any]) -> tuple[ModelPackage, Model]:
|
|
138
|
+
"""The single source a tool's arguments name."""
|
|
139
|
+
return self.get(args.get("ct_id"), args.get("directory"))
|
|
140
|
+
|
|
141
|
+
def many(self, args: dict[str, Any]) -> list[tuple[ModelPackage, Model]]:
|
|
142
|
+
"""Every source a multi-model tool's arguments name, in the order given, identifiers first."""
|
|
143
|
+
ct_ids = args.get("ct_ids") or []
|
|
144
|
+
dirs = args.get("directories") or []
|
|
145
|
+
if not isinstance(ct_ids, list) or not isinstance(dirs, list) or not all(isinstance(x, str) for x in ct_ids + dirs):
|
|
146
|
+
raise ToolError("`ct_ids` and `directories` must be arrays of strings.")
|
|
147
|
+
if not ct_ids and not dirs:
|
|
148
|
+
raise ToolError("Give at least one model in `ct_ids` or `directories`.")
|
|
149
|
+
return [self.get(ct_id=c) for c in ct_ids] + [self.get(directory=d) for d in dirs]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Server:
|
|
153
|
+
"""One MCP server: a name, a version, its instructions, its tools and their handlers."""
|
|
154
|
+
|
|
155
|
+
def __init__(self, name: str, version: str, instructions: str, tools: list[dict],
|
|
156
|
+
handlers: dict[str, Callable[[dict[str, Any]], Any]], issues_url: str):
|
|
157
|
+
if set(handlers) != {t["name"] for t in tools}:
|
|
158
|
+
raise ValueError("every tool needs a handler and every handler a tool")
|
|
159
|
+
self.name, self.version, self.instructions = name, version, instructions
|
|
160
|
+
self.tools, self.handlers, self.issues_url = tools, handlers, issues_url
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _response(id: Any, result: Any) -> dict:
|
|
164
|
+
return {"jsonrpc": JSONRPC_VERSION, "id": id, "result": result}
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _error(id: Any, code: int, message: str, data: Any = None) -> dict:
|
|
168
|
+
error = {"code": code, "message": message}
|
|
169
|
+
if data is not None:
|
|
170
|
+
error["data"] = data
|
|
171
|
+
return {"jsonrpc": JSONRPC_VERSION, "id": id, "error": error}
|
|
172
|
+
|
|
173
|
+
def _tool_error(self, id: Any, text: str) -> dict:
|
|
174
|
+
return self._response(id, {"content": [{"type": "text", "text": text}], "isError": True})
|
|
175
|
+
|
|
176
|
+
def handle(self, request: Any) -> dict | None:
|
|
177
|
+
"""Handle one JSON-RPC 2.0 request. Returns a response, or None for a notification."""
|
|
178
|
+
if not isinstance(request, dict):
|
|
179
|
+
return self._error(None, -32600, "Invalid request: expected a JSON object.")
|
|
180
|
+
method = request.get("method", "")
|
|
181
|
+
params = request.get("params", {})
|
|
182
|
+
req_id = request.get("id")
|
|
183
|
+
if params is None:
|
|
184
|
+
params = {}
|
|
185
|
+
if not isinstance(params, dict):
|
|
186
|
+
return self._error(req_id, -32600, "Invalid request: `params` must be an object.")
|
|
187
|
+
|
|
188
|
+
if method == "initialize":
|
|
189
|
+
requested = params.get("protocolVersion")
|
|
190
|
+
negotiated = requested if requested in SUPPORTED_PROTOCOL_VERSIONS else MCP_PROTOCOL_VERSION
|
|
191
|
+
return self._response(req_id, {
|
|
192
|
+
"protocolVersion": negotiated,
|
|
193
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
194
|
+
"serverInfo": {"name": self.name, "version": self.version},
|
|
195
|
+
"instructions": self.instructions,
|
|
196
|
+
})
|
|
197
|
+
if method == "notifications/initialized":
|
|
198
|
+
return None
|
|
199
|
+
if method == "tools/list":
|
|
200
|
+
return self._response(req_id, {"tools": self.tools})
|
|
201
|
+
if method == "tools/call":
|
|
202
|
+
tool_name = params.get("name", "")
|
|
203
|
+
tool_args = params.get("arguments", {})
|
|
204
|
+
if tool_args is None:
|
|
205
|
+
tool_args = {}
|
|
206
|
+
if not isinstance(tool_args, dict):
|
|
207
|
+
return self._error(req_id, -32602, "Invalid params: `arguments` must be an object.")
|
|
208
|
+
handler = self.handlers.get(tool_name)
|
|
209
|
+
if handler is None:
|
|
210
|
+
return self._error(req_id, -32601, f"Unknown tool: {tool_name}")
|
|
211
|
+
try:
|
|
212
|
+
result = handler(tool_args)
|
|
213
|
+
except ToolError as exc:
|
|
214
|
+
return self._tool_error(req_id, f"Tool execution error: {exc}")
|
|
215
|
+
except Exception as exc: # a defect in the server, reported without its traceback
|
|
216
|
+
return self._tool_error(req_id, (
|
|
217
|
+
f"Tool execution error: the server could not process this input ({type(exc).__name__}). "
|
|
218
|
+
f"This is a defect worth reporting at {self.issues_url}"))
|
|
219
|
+
if isinstance(result, str):
|
|
220
|
+
text = result
|
|
221
|
+
else:
|
|
222
|
+
text = json.dumps(result, default=str)
|
|
223
|
+
return self._response(req_id, {"content": [{"type": "text", "text": text}]})
|
|
224
|
+
if method == "ping":
|
|
225
|
+
return self._response(req_id, {})
|
|
226
|
+
if req_id is not None:
|
|
227
|
+
return self._error(req_id, -32601, f"Method not found: {method}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
def run_stdio(self) -> None:
|
|
231
|
+
"""One JSON-RPC message per line on stdin, one response per line on stdout."""
|
|
232
|
+
for line in sys.stdin:
|
|
233
|
+
line = line.strip()
|
|
234
|
+
if not line:
|
|
235
|
+
continue
|
|
236
|
+
try:
|
|
237
|
+
request = json.loads(line)
|
|
238
|
+
except json.JSONDecodeError as exc:
|
|
239
|
+
sys.stdout.write(json.dumps(self._error(None, -32700, f"Parse error: {exc}")) + "\n")
|
|
240
|
+
sys.stdout.flush()
|
|
241
|
+
continue
|
|
242
|
+
response = self.handle(request)
|
|
243
|
+
if response is not None:
|
|
244
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
245
|
+
sys.stdout.flush()
|