web-algebra 1.0.0__py3-none-any.whl

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 (62) hide show
  1. web_algebra/__init__.py +0 -0
  2. web_algebra/__main__.py +17 -0
  3. web_algebra/client.py +245 -0
  4. web_algebra/json_result.py +168 -0
  5. web_algebra/main.py +149 -0
  6. web_algebra/mcp_tool.py +25 -0
  7. web_algebra/operation.py +250 -0
  8. web_algebra/operations/__init__.py +0 -0
  9. web_algebra/operations/bindings.py +59 -0
  10. web_algebra/operations/current.py +40 -0
  11. web_algebra/operations/execute.py +56 -0
  12. web_algebra/operations/filter.py +119 -0
  13. web_algebra/operations/for_each.py +114 -0
  14. web_algebra/operations/linked_data/__init__.py +0 -0
  15. web_algebra/operations/linked_data/get.py +77 -0
  16. web_algebra/operations/linked_data/patch.py +125 -0
  17. web_algebra/operations/linked_data/post.py +132 -0
  18. web_algebra/operations/linked_data/put.py +132 -0
  19. web_algebra/operations/linkeddatahub/__init__.py +0 -0
  20. web_algebra/operations/linkeddatahub/add_generic_service.py +303 -0
  21. web_algebra/operations/linkeddatahub/add_result_set_chart.py +300 -0
  22. web_algebra/operations/linkeddatahub/add_select.py +233 -0
  23. web_algebra/operations/linkeddatahub/add_view.py +255 -0
  24. web_algebra/operations/linkeddatahub/content/__init__.py +0 -0
  25. web_algebra/operations/linkeddatahub/content/add_object_block.py +311 -0
  26. web_algebra/operations/linkeddatahub/content/add_xhtml_block.py +277 -0
  27. web_algebra/operations/linkeddatahub/content/generate_class_containers.py +234 -0
  28. web_algebra/operations/linkeddatahub/content/generate_ontology_views.py +200 -0
  29. web_algebra/operations/linkeddatahub/content/generate_portal.py +131 -0
  30. web_algebra/operations/linkeddatahub/content/remove_block.py +138 -0
  31. web_algebra/operations/linkeddatahub/create_container.py +190 -0
  32. web_algebra/operations/linkeddatahub/create_item.py +156 -0
  33. web_algebra/operations/linkeddatahub/list.py +159 -0
  34. web_algebra/operations/merge.py +109 -0
  35. web_algebra/operations/resolve_uri.py +88 -0
  36. web_algebra/operations/schema/__init__.py +0 -0
  37. web_algebra/operations/schema/extract_classes.py +55 -0
  38. web_algebra/operations/schema/extract_datatype_properties.py +119 -0
  39. web_algebra/operations/schema/extract_object_properties.py +140 -0
  40. web_algebra/operations/schema/extract_ontology.py +57 -0
  41. web_algebra/operations/sparql/__init__.py +0 -0
  42. web_algebra/operations/sparql/construct.py +100 -0
  43. web_algebra/operations/sparql/describe.py +100 -0
  44. web_algebra/operations/sparql/select.py +96 -0
  45. web_algebra/operations/sparql/substitute.py +235 -0
  46. web_algebra/operations/sparql_string.py +85 -0
  47. web_algebra/operations/str.py +70 -0
  48. web_algebra/operations/string/__init__.py +0 -0
  49. web_algebra/operations/string/concat.py +67 -0
  50. web_algebra/operations/string/encode_for_uri.py +64 -0
  51. web_algebra/operations/string/replace.py +131 -0
  52. web_algebra/operations/struuid.py +39 -0
  53. web_algebra/operations/uri.py +61 -0
  54. web_algebra/operations/value.py +66 -0
  55. web_algebra/operations/variable.py +53 -0
  56. web_algebra/server.py +131 -0
  57. web_algebra-1.0.0.dist-info/METADATA +223 -0
  58. web_algebra-1.0.0.dist-info/RECORD +62 -0
  59. web_algebra-1.0.0.dist-info/WHEEL +5 -0
  60. web_algebra-1.0.0.dist-info/entry_points.txt +2 -0
  61. web_algebra-1.0.0.dist-info/licenses/LICENSE +202 -0
  62. web_algebra-1.0.0.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,17 @@
1
+ import logging
2
+ from mcp.server.stdio import stdio_server
3
+ import asyncio
4
+ from .server import server
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ async def serve():
10
+ options = server.create_initialization_options()
11
+ async with stdio_server() as (read_stream, write_stream):
12
+ logger.info("Starting server with stdio")
13
+ await server.run(read_stream, write_stream, options)
14
+
15
+
16
+ if __name__ == "__main__":
17
+ asyncio.run(serve())
web_algebra/client.py ADDED
@@ -0,0 +1,245 @@
1
+ from typing import Optional
2
+ import ssl
3
+ import json
4
+ import urllib.request
5
+ from http.client import HTTPResponse
6
+ from rdflib import Graph
7
+ from rdflib.plugins.sparql.parser import parseQuery
8
+
9
+
10
+ MEDIA_TYPES = {
11
+ "application/n-triples": "nt",
12
+ "text/turtle": "turtle",
13
+ "application/ld+json": "json-ld",
14
+ "application/rdf+xml": "xml",
15
+ }
16
+
17
+
18
+ class HTTPRedirectHandler308(urllib.request.HTTPRedirectHandler):
19
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
20
+ """Handle 308 Permanent Redirect by preserving method and body"""
21
+ if code == 308:
22
+ return urllib.request.Request(
23
+ newurl, data=req.data, headers=req.headers, method=req.get_method()
24
+ )
25
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
26
+
27
+
28
+ class LinkedDataClient:
29
+ def __init__(
30
+ self,
31
+ cert_pem_path: Optional[str] = None,
32
+ cert_password: Optional[str] = None,
33
+ verify_ssl: bool = True,
34
+ ):
35
+ """
36
+ Initializes the LinkedDataClient with SSL configuration.
37
+
38
+ :param cert_pem_path: Path to the certificate .pem file (containing both private key and certificate).
39
+ :param cert_password: Password for the encrypted private key in the .pem file.
40
+ :param verify_ssl: Whether to verify the server's SSL certificate. Default is True.
41
+ """
42
+ # Always create SSL context
43
+ self.ssl_context = ssl.create_default_context()
44
+
45
+ # Load client certificate if provided
46
+ if cert_pem_path and cert_password:
47
+ self.ssl_context.load_cert_chain(
48
+ certfile=cert_pem_path, password=cert_password
49
+ )
50
+
51
+ # Configure SSL verification
52
+ if not verify_ssl:
53
+ self.ssl_context.check_hostname = False
54
+ self.ssl_context.verify_mode = ssl.CERT_NONE
55
+
56
+ # Create an HTTPS handler with the configured SSL context
57
+ self.opener = urllib.request.build_opener(
58
+ urllib.request.HTTPSHandler(context=self.ssl_context),
59
+ HTTPRedirectHandler308(),
60
+ )
61
+
62
+ # Add proper User-Agent header for external services like Wikidata
63
+ self.opener.addheaders = [
64
+ (
65
+ "User-Agent",
66
+ "Web-Algebra/1.0 (LinkedData Processing System; https://github.com/atomgraph/Web-Algebra)",
67
+ )
68
+ ]
69
+
70
+ def get(self, url: str) -> Graph:
71
+ """
72
+ Fetches RDF data from the given URL and returns it as an RDFLib Graph.
73
+
74
+ :param url: The URL to fetch RDF data from.
75
+ :return: An RDFLib Graph object containing the parsed RDF data.
76
+ """
77
+ # Set the Accept header
78
+ accept_header = ", ".join(MEDIA_TYPES.keys())
79
+ headers = {"Accept": accept_header}
80
+ request = urllib.request.Request(url, headers=headers)
81
+
82
+ # Perform the HTTP request
83
+ response = self.opener.open(request)
84
+
85
+ # Read and decode the response data
86
+ data = response.read().decode("utf-8")
87
+ content_type = response.headers.get("Content-Type").split(";")[0]
88
+ rdf_format = MEDIA_TYPES.get(content_type)
89
+ if not rdf_format:
90
+ raise ValueError(
91
+ f"Unsupported Content-Type: {content_type}. Supported types are: {', '.join(MEDIA_TYPES.keys())}"
92
+ )
93
+
94
+ # Parse the RDF data into an RDFLib Graph
95
+ g = Graph()
96
+ g.parse(data=data, format=rdf_format, publicID=url)
97
+ return g
98
+
99
+ def post(self, url: str, graph: Graph) -> HTTPResponse:
100
+ """
101
+ Sends RDF data to the given URL using HTTP POST.
102
+
103
+ :param url: The URL to send RDF data to.
104
+ :param data: An RDFLib Graph containing the data to send.
105
+ :return: The HTTPResponse object.
106
+ """
107
+ # Serialize the RDF data to N-Triples
108
+ data = graph.serialize(format="nt")
109
+ headers = {
110
+ "Content-Type": "application/n-triples",
111
+ "Accept": "application/n-triples",
112
+ }
113
+ request = urllib.request.Request(
114
+ url, data=data.encode("utf-8"), headers=headers, method="POST"
115
+ )
116
+
117
+ return self.opener.open(request)
118
+
119
+ def put(self, url: str, graph: Graph) -> HTTPResponse:
120
+ """
121
+ Sends RDF data to the given URL using HTTP PUT.
122
+
123
+ :param url: The URL to send RDF data to.
124
+ :param data: An RDFLib Graph containing the data to send.
125
+ :return: The HTTPResponse object.
126
+ """
127
+ # Serialize the RDF data to N-Triples
128
+ data = graph.serialize(format="nt")
129
+ headers = {
130
+ "Content-Type": "application/n-triples",
131
+ "Accept": "application/n-triples",
132
+ }
133
+ request = urllib.request.Request(
134
+ url, data=data.encode("utf-8"), headers=headers, method="PUT"
135
+ )
136
+
137
+ return self.opener.open(request)
138
+
139
+ def delete(self, url: str) -> HTTPResponse:
140
+ """
141
+ Sends an HTTP DELETE request to the given URL.
142
+
143
+ :param url: The URL to send the DELETE request to.
144
+ :return: The HTTPResponse object.
145
+ """
146
+ request = urllib.request.Request(url, method="DELETE")
147
+
148
+ return self.opener.open(request)
149
+
150
+ def patch(self, url: str, sparql_update: str) -> HTTPResponse:
151
+ """
152
+ Sends a SPARQL UPDATE query to the given URL using HTTP PATCH.
153
+
154
+ :param url: The URL to send the SPARQL UPDATE to.
155
+ :param sparql_update: The SPARQL UPDATE query string.
156
+ :return: The HTTPResponse object.
157
+ """
158
+ headers = {
159
+ "Content-Type": "application/sparql-update",
160
+ "Accept": "application/n-triples",
161
+ }
162
+ request = urllib.request.Request(
163
+ url, data=sparql_update.encode("utf-8"), headers=headers, method="PATCH"
164
+ )
165
+
166
+ return self.opener.open(request)
167
+
168
+
169
+ class SPARQLClient:
170
+ def __init__(
171
+ self,
172
+ cert_pem_path: Optional[str] = None,
173
+ cert_password: Optional[str] = None,
174
+ verify_ssl: bool = True,
175
+ ):
176
+ """
177
+ Initializes the SPARQLClient with optional SSL certificate.
178
+
179
+ :param cert_pem_path: Path to .pem file containing cert+key
180
+ :param cert_password: Password for the PEM file
181
+ :param verify_ssl: Whether to verify server SSL certificate
182
+ """
183
+ # Always create SSL context
184
+ self.ssl_context = ssl.create_default_context()
185
+
186
+ # Load client certificate if provided
187
+ if cert_pem_path and cert_password:
188
+ self.ssl_context.load_cert_chain(
189
+ certfile=cert_pem_path, password=cert_password
190
+ )
191
+
192
+ # Configure SSL verification
193
+ if not verify_ssl:
194
+ self.ssl_context.check_hostname = False
195
+ self.ssl_context.verify_mode = ssl.CERT_NONE
196
+
197
+ self.opener = urllib.request.build_opener(
198
+ urllib.request.HTTPSHandler(context=self.ssl_context)
199
+ )
200
+
201
+ # Add proper User-Agent header for external services like Wikidata
202
+ self.opener.addheaders = [
203
+ (
204
+ "User-Agent",
205
+ "Web-Algebra/1.0 (LinkedData Processing System; https://github.com/atomgraph/Web-Algebra)",
206
+ )
207
+ ]
208
+
209
+ def query(self, endpoint_url: str, query_string: str) -> dict:
210
+ """
211
+ Executes a SPARQL query. Returns Graph for CONSTRUCT/DESCRIBE, Result for SELECT/ASK.
212
+
213
+ :param endpoint_url: The SPARQL endpoint URL
214
+ :param query_string: SPARQL query string
215
+ :return: rdflib.Graph or rdflib.query.Result
216
+ """
217
+ parsed = parseQuery(query_string)
218
+ query_type = parsed[1].name # e.g., 'SelectQuery', 'ConstructQuery'
219
+
220
+ if query_type in {"SelectQuery", "AskQuery"}:
221
+ accept = "application/sparql-results+json"
222
+ elif query_type in {"ConstructQuery", "DescribeQuery"}:
223
+ accept = "application/n-triples"
224
+ else:
225
+ raise ValueError(f"Unsupported query type: {query_type}")
226
+
227
+ # Encode URL parameters
228
+ params = urllib.parse.urlencode({"query": query_string})
229
+ url = f"{endpoint_url}?{params}"
230
+ headers = {"Accept": accept}
231
+
232
+ request = urllib.request.Request(url, headers=headers)
233
+ response = self.opener.open(request)
234
+ data = response.read()
235
+
236
+ if accept == "application/n-triples":
237
+ g = Graph()
238
+ # convert N-Triples to JSON-LD
239
+ g.parse(data=data.decode("utf-8"), format="nt")
240
+ jsonld_str = g.serialize(format="json-ld")
241
+ jsonld_data = json.loads(jsonld_str)
242
+ return jsonld_data
243
+ else:
244
+ # return SPARQL JSON results as a dict
245
+ return json.loads(data.decode("utf-8"))
@@ -0,0 +1,168 @@
1
+ from typing import List, Dict, Iterator
2
+ from rdflib.term import Node
3
+ from rdflib import URIRef, Literal, BNode
4
+ from rdflib.query import Result, ResultRow
5
+
6
+
7
+ class JSONResult(Result):
8
+ """
9
+ A SPARQL Results container that subclasses rdflib.query.Result,
10
+ can be constructed from SPARQL JSON format and works with RDFLib objects internally.
11
+ """
12
+
13
+ def __init__(self, vars: List[str], bindings: List[Dict[str, Node]]):
14
+ super().__init__("SELECT") # Always SELECT type for our use case
15
+ self.head = {"vars": vars}
16
+ self.bindings = bindings
17
+ # Set the parent class vars attribute
18
+ self.vars = vars
19
+
20
+ @classmethod
21
+ def from_json(cls, json_dict: dict) -> "JSONResult":
22
+ """Construct from SPARQL JSON format"""
23
+ vars = json_dict["head"]["vars"]
24
+ bindings = []
25
+
26
+ for json_binding in json_dict["results"]["bindings"]:
27
+ rdf_binding = {}
28
+ for var, binding_dict in json_binding.items():
29
+ rdf_binding[var] = cls._parse_binding(binding_dict)
30
+ bindings.append(rdf_binding)
31
+
32
+ return cls(vars, bindings)
33
+
34
+ @staticmethod
35
+ def _parse_binding(binding_dict: dict) -> Node:
36
+ """Convert SPARQL JSON binding to RDFLib object"""
37
+ if binding_dict["type"] == "uri":
38
+ return URIRef(binding_dict["value"])
39
+ elif binding_dict["type"] == "literal":
40
+ value = binding_dict["value"]
41
+ datatype = binding_dict.get("datatype")
42
+ lang = binding_dict.get("xml:lang")
43
+
44
+ if datatype:
45
+ datatype = URIRef(datatype)
46
+
47
+ return Literal(value, datatype=datatype, lang=lang)
48
+ elif binding_dict["type"] == "bnode":
49
+ return BNode(binding_dict["value"])
50
+ else:
51
+ raise ValueError(f"Unknown binding type: {binding_dict['type']}")
52
+
53
+ @staticmethod
54
+ def _serialize_binding(term: Node) -> dict:
55
+ """Convert RDFLib object to SPARQL JSON binding"""
56
+ if isinstance(term, URIRef):
57
+ return {"type": "uri", "value": str(term)}
58
+ elif isinstance(term, Literal):
59
+ result = {"type": "literal", "value": str(term)}
60
+ if term.datatype:
61
+ result["datatype"] = str(term.datatype)
62
+ if term.language:
63
+ result["xml:lang"] = term.language
64
+ return result
65
+ elif isinstance(term, BNode):
66
+ return {"type": "bnode", "value": str(term)}
67
+ else:
68
+ raise ValueError(f"Unknown RDFLib term type: {type(term)}")
69
+
70
+ def to_json(self) -> dict:
71
+ """Convert back to SPARQL JSON format"""
72
+ json_bindings = []
73
+
74
+ for binding in self.bindings:
75
+ json_binding = {}
76
+ for var, term in binding.items():
77
+ json_binding[var] = self._serialize_binding(term)
78
+ json_bindings.append(json_binding)
79
+
80
+ return {"head": {"vars": self.vars}, "results": {"bindings": json_bindings}}
81
+
82
+ def __iter__(self) -> Iterator[ResultRow]:
83
+ """Allow iteration over bindings as ResultRow objects"""
84
+ for binding in self.bindings:
85
+ yield ResultRow(binding, self.vars)
86
+
87
+ def __len__(self) -> int:
88
+ """Number of bindings"""
89
+ return len(self.bindings)
90
+
91
+ def __getitem__(self, index: int) -> Dict[str, Node]:
92
+ """Access binding by index"""
93
+ return self.bindings[index]
94
+
95
+ def filter_by_position(self, position: int) -> "JSONResult":
96
+ """Filter to a single binding by 1-based position (XSLT-style)"""
97
+ if position < 1:
98
+ raise ValueError("Position must be >= 1 (XSLT-style 1-based indexing)")
99
+
100
+ if position > len(self.bindings):
101
+ raise ValueError(
102
+ f"Position {position} exceeds number of bindings ({len(self.bindings)})"
103
+ )
104
+
105
+ # Convert to 0-based index and create new result with single binding
106
+ filtered_binding = self.bindings[position - 1]
107
+ return JSONResult(self.head["vars"], [filtered_binding])
108
+
109
+ # Additional Result interface methods
110
+
111
+ def __bool__(self) -> bool:
112
+ """Return True if there are any bindings"""
113
+ return len(self.bindings) > 0
114
+
115
+ def __eq__(self, other) -> bool:
116
+ """Equality comparison"""
117
+ if not isinstance(other, JSONResult):
118
+ return False
119
+ return self.head == other.head and self.bindings == other.bindings
120
+
121
+ def __repr__(self) -> str:
122
+ """Developer-friendly representation"""
123
+ return f"JSONResult(vars={self.vars}, bindings={len(self.bindings)} rows)"
124
+
125
+ def __str__(self) -> str:
126
+ """Pretty table representation"""
127
+ if not self.bindings:
128
+ return f"JSONResult: {self.vars} (0 rows)\n(empty)"
129
+
130
+ # Calculate column widths
131
+ col_widths = {}
132
+ for var in self.vars:
133
+ col_widths[var] = max(
134
+ len(var),
135
+ max(len(str(binding.get(var, "NULL"))) for binding in self.bindings),
136
+ )
137
+
138
+ # Build table
139
+ lines = []
140
+
141
+ # Header
142
+ header = (
143
+ "| " + " | ".join(var.ljust(col_widths[var]) for var in self.vars) + " |"
144
+ )
145
+ separator = (
146
+ "+" + "+".join("-" * (col_widths[var] + 2) for var in self.vars) + "+"
147
+ )
148
+
149
+ lines.append(f"JSONResult: {self.vars} ({len(self.bindings)} rows)")
150
+ lines.append(separator)
151
+ lines.append(header)
152
+ lines.append(separator)
153
+
154
+ # Rows
155
+ for binding in self.bindings:
156
+ row = (
157
+ "| "
158
+ + " | ".join(
159
+ str(binding.get(var, "NULL")).ljust(col_widths[var])
160
+ for var in self.vars
161
+ )
162
+ + " |"
163
+ )
164
+ lines.append(row)
165
+
166
+ lines.append(separator)
167
+
168
+ return "\n".join(lines)
web_algebra/main.py ADDED
@@ -0,0 +1,149 @@
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import sys
5
+ import json
6
+ from types import ModuleType
7
+ from typing import List, Type, Optional
8
+ import pkgutil
9
+ import importlib
10
+ import inspect
11
+ import rdflib
12
+ from openai import OpenAI
13
+ from pydantic_settings import BaseSettings
14
+ import web_algebra.operations
15
+ from web_algebra.operation import Operation
16
+
17
+ # Configure logging to show INFO level and above
18
+ logging.basicConfig(
19
+ level=logging.INFO, # ✅ Show INFO, WARNING, ERROR, and CRITICAL messages
20
+ format="%(asctime)s - %(levelname)s - %(message)s", # ✅ Add timestamps for clarity
21
+ handlers=[logging.StreamHandler()], # ✅ Ensure output goes to console
22
+ )
23
+
24
+
25
+ class LinkedDataHubSettings(BaseSettings):
26
+ cert_pem_path: Optional[str] = None
27
+ cert_password: Optional[str] = None
28
+ openai_client: Optional[OpenAI] = None
29
+ openai_model: str = "gpt-4o-mini"
30
+
31
+
32
+ def list_operation_subclasses(
33
+ pkg: ModuleType, base_class: Type[Operation]
34
+ ) -> List[Type[Operation]]:
35
+ subclasses = []
36
+
37
+ for loader, module_name, is_pkg in pkgutil.walk_packages(
38
+ pkg.__path__, pkg.__name__ + "."
39
+ ):
40
+ module = importlib.import_module(module_name)
41
+
42
+ for name, obj in inspect.getmembers(module, inspect.isclass):
43
+ # Filter: class defined in the module AND is a subclass of Operation (but not Operation itself)
44
+ if (
45
+ obj.__module__ == module.__name__
46
+ and issubclass(obj, base_class)
47
+ and obj is not base_class
48
+ ):
49
+ subclasses.append(obj)
50
+
51
+ return subclasses
52
+
53
+
54
+ def register(classes: List[Type[Operation]]):
55
+ for cls in classes:
56
+ Operation.register(cls)
57
+
58
+
59
+ def main(settings: BaseSettings, json_data: Optional[str]):
60
+ register(list_operation_subclasses(web_algebra.operations, Operation))
61
+
62
+ if json_data:
63
+ logging.info("Executing from JSON input %s", json_data)
64
+ # Load JSON input from file
65
+ with open(json_data) as json_file:
66
+ json_input = json.load(json_file)
67
+
68
+ # Execute the JSON input
69
+ result = Operation.process_json(settings, json_input)
70
+
71
+ # Serialize final result for output
72
+ if isinstance(result, rdflib.Graph):
73
+ # Serialize Graph to JSON-LD for final output
74
+ jsonld_str = result.serialize(format="json-ld")
75
+ print(jsonld_str)
76
+ else:
77
+ print(result)
78
+ else:
79
+ # Load API key
80
+ api_key = os.getenv("OPENAI_API_KEY")
81
+ if api_key is None:
82
+ sys.exit("Error: Environment variable OPENAI_API_KEY is not set.")
83
+
84
+ client = OpenAI(api_key=api_key)
85
+ model = "gpt-4o-mini"
86
+
87
+ # Load prompts
88
+ with (
89
+ open("prompts/system.md") as system_prompt_file,
90
+ open("prompts/user.template.txt") as user_prompt_template_file,
91
+ ):
92
+ system_prompt = system_prompt_file.read()
93
+ messages = [{"role": "system", "content": system_prompt}]
94
+
95
+ user_prompt_template = user_prompt_template_file.read()
96
+
97
+ while True:
98
+ instruction = input("Instruction: ")
99
+
100
+ user_prompt = user_prompt_template.format(instruction=instruction)
101
+ messages.append(
102
+ {"role": "user", "content": user_prompt},
103
+ )
104
+
105
+ chat_completion = client.chat.completions.create(
106
+ model=model,
107
+ messages=messages,
108
+ response_format={"type": "json_object"},
109
+ )
110
+
111
+ reply_json = json.loads(chat_completion.choices[0].message.content)
112
+ assert isinstance(reply_json, dict)
113
+ logging.info("Generated response: %s", reply_json)
114
+
115
+ # Execute the generated operation
116
+ print(Operation.process_json(settings, reply_json))
117
+
118
+
119
+ def cli() -> None:
120
+ parser = argparse.ArgumentParser(description="Run the AI-powered DSL interpreter.")
121
+ parser.add_argument(
122
+ "--from-json",
123
+ type=str,
124
+ help="JSON input file to execute",
125
+ )
126
+ parser.add_argument(
127
+ "--cert_pem_path",
128
+ type=str,
129
+ help="Path to the client certificate PEM file",
130
+ )
131
+ parser.add_argument(
132
+ "--cert_password",
133
+ type=str,
134
+ help="Password for the client certificate",
135
+ )
136
+ args = parser.parse_args()
137
+
138
+ if args.cert_pem_path and args.cert_password:
139
+ settings = LinkedDataHubSettings(
140
+ cert_pem_path=args.cert_pem_path, cert_password=args.cert_password
141
+ )
142
+ else:
143
+ settings = BaseSettings()
144
+
145
+ main(settings, args.from_json)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ cli()
@@ -0,0 +1,25 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+
5
+ class MCPTool(ABC):
6
+ """
7
+ Interface for operations that can be exposed as MCP (Model Context Protocol) tools.
8
+
9
+ Operations implementing this interface can be called directly from MCP clients
10
+ like Claude Desktop, providing standalone utility functionality.
11
+ """
12
+
13
+ @abstractmethod
14
+ def mcp_run(self, arguments: dict, context: Any = None) -> Any:
15
+ """
16
+ Execute the operation as an MCP tool with plain arguments.
17
+
18
+ Args:
19
+ arguments: Plain dictionary arguments from MCP client
20
+ context: Optional context (usually None for MCP calls)
21
+
22
+ Returns:
23
+ List of MCP content objects (TextContent, ImageContent, etc.)
24
+ """
25
+ pass