engineering-argument-language 3.2.3__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 (72) hide show
  1. eal/__init__.py +3 -0
  2. eal/abstractions.py +144 -0
  3. eal/acquisition_coordination.py +80 -0
  4. eal/api_load_methods.py +77 -0
  5. eal/aspic.py +478 -0
  6. eal/aspic_compiler.py +445 -0
  7. eal/aspic_export.py +309 -0
  8. eal/builtin_methods.py +115 -0
  9. eal/catalogue.py +384 -0
  10. eal/cli.py +204 -0
  11. eal/client_transports.py +69 -0
  12. eal/collection_identity.py +28 -0
  13. eal/collection_scheduler.py +72 -0
  14. eal/command_process.py +118 -0
  15. eal/command_supervisor.py +112 -0
  16. eal/composition.py +531 -0
  17. eal/credentials.py +33 -0
  18. eal/dialectic.py +321 -0
  19. eal/discovery.py +127 -0
  20. eal/evaluator.py +619 -0
  21. eal/expressions.py +404 -0
  22. eal/extensions.py +38 -0
  23. eal/formatter.py +156 -0
  24. eal/generated/EALLexer.py +434 -0
  25. eal/generated/EALParser.py +6559 -0
  26. eal/generated/EALVisitor.py +393 -0
  27. eal/generated/__init__.py +1 -0
  28. eal/host.py +179 -0
  29. eal/host_redaction.py +51 -0
  30. eal/knowledge.py +96 -0
  31. eal/limits.py +88 -0
  32. eal/mcp_guard.py +56 -0
  33. eal/methods.py +463 -0
  34. eal/model.py +266 -0
  35. eal/model_context.py +55 -0
  36. eal/modes.py +121 -0
  37. eal/observation_reuse.py +128 -0
  38. eal/operation_contracts.py +25 -0
  39. eal/operations.py +155 -0
  40. eal/packets.py +604 -0
  41. eal/parser.py +407 -0
  42. eal/planning.py +136 -0
  43. eal/propositions.py +231 -0
  44. eal/reachability.py +85 -0
  45. eal/reasoning/__init__.py +25 -0
  46. eal/reasoning/abductive.py +45 -0
  47. eal/reasoning/analogical.py +41 -0
  48. eal/reasoning/causal.py +46 -0
  49. eal/reasoning/counterfactual.py +64 -0
  50. eal/reasoning/deductive.py +72 -0
  51. eal/reasoning/inductive.py +29 -0
  52. eal/reasoning/strategy.py +18 -0
  53. eal/reasoning/structured.py +11 -0
  54. eal/reasoning/temporal.py +47 -0
  55. eal/reasoning/validation.py +90 -0
  56. eal/registered_assessment.py +181 -0
  57. eal/runtime.py +454 -0
  58. eal/sampled_negative.py +131 -0
  59. eal/scope_transfer.py +59 -0
  60. eal/semantics.py +656 -0
  61. eal/server.py +79 -0
  62. eal/server_auth.py +37 -0
  63. eal/server_settings.py +258 -0
  64. eal/source_printer.py +129 -0
  65. eal/store.py +272 -0
  66. eal/tool_acquisition.py +383 -0
  67. engineering_argument_language-3.2.3.dist-info/METADATA +188 -0
  68. engineering_argument_language-3.2.3.dist-info/RECORD +72 -0
  69. engineering_argument_language-3.2.3.dist-info/WHEEL +5 -0
  70. engineering_argument_language-3.2.3.dist-info/entry_points.txt +4 -0
  71. engineering_argument_language-3.2.3.dist-info/licenses/LICENSE +24 -0
  72. engineering_argument_language-3.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,393 @@
1
+ # Generated from grammar/EAL.g4 by ANTLR 4.13.2
2
+ from antlr4 import *
3
+ if "." in __name__:
4
+ from .EALParser import EALParser
5
+ else:
6
+ from EALParser import EALParser
7
+
8
+ # This class defines a complete generic visitor for a parse tree produced by EALParser.
9
+
10
+ class EALVisitor(ParseTreeVisitor):
11
+
12
+ # Visit a parse tree produced by EALParser#program.
13
+ def visitProgram(self, ctx:EALParser.ProgramContext):
14
+ return self.visitChildren(ctx)
15
+
16
+
17
+ # Visit a parse tree produced by EALParser#declaration.
18
+ def visitDeclaration(self, ctx:EALParser.DeclarationContext):
19
+ return self.visitChildren(ctx)
20
+
21
+
22
+ # Visit a parse tree produced by EALParser#moduleDecl.
23
+ def visitModuleDecl(self, ctx:EALParser.ModuleDeclContext):
24
+ return self.visitChildren(ctx)
25
+
26
+
27
+ # Visit a parse tree produced by EALParser#importDecl.
28
+ def visitImportDecl(self, ctx:EALParser.ImportDeclContext):
29
+ return self.visitChildren(ctx)
30
+
31
+
32
+ # Visit a parse tree produced by EALParser#contextDecl.
33
+ def visitContextDecl(self, ctx:EALParser.ContextDeclContext):
34
+ return self.visitChildren(ctx)
35
+
36
+
37
+ # Visit a parse tree produced by EALParser#contextAttribute.
38
+ def visitContextAttribute(self, ctx:EALParser.ContextAttributeContext):
39
+ return self.visitChildren(ctx)
40
+
41
+
42
+ # Visit a parse tree produced by EALParser#environmentDecl.
43
+ def visitEnvironmentDecl(self, ctx:EALParser.EnvironmentDeclContext):
44
+ return self.visitChildren(ctx)
45
+
46
+
47
+ # Visit a parse tree produced by EALParser#toolDecl.
48
+ def visitToolDecl(self, ctx:EALParser.ToolDeclContext):
49
+ return self.visitChildren(ctx)
50
+
51
+
52
+ # Visit a parse tree produced by EALParser#versionField.
53
+ def visitVersionField(self, ctx:EALParser.VersionFieldContext):
54
+ return self.visitChildren(ctx)
55
+
56
+
57
+ # Visit a parse tree produced by EALParser#evidenceDecl.
58
+ def visitEvidenceDecl(self, ctx:EALParser.EvidenceDeclContext):
59
+ return self.visitChildren(ctx)
60
+
61
+
62
+ # Visit a parse tree produced by EALParser#evidenceField.
63
+ def visitEvidenceField(self, ctx:EALParser.EvidenceFieldContext):
64
+ return self.visitChildren(ctx)
65
+
66
+
67
+ # Visit a parse tree produced by EALParser#toolField.
68
+ def visitToolField(self, ctx:EALParser.ToolFieldContext):
69
+ return self.visitChildren(ctx)
70
+
71
+
72
+ # Visit a parse tree produced by EALParser#kindField.
73
+ def visitKindField(self, ctx:EALParser.KindFieldContext):
74
+ return self.visitChildren(ctx)
75
+
76
+
77
+ # Visit a parse tree produced by EALParser#environmentField.
78
+ def visitEnvironmentField(self, ctx:EALParser.EnvironmentFieldContext):
79
+ return self.visitChildren(ctx)
80
+
81
+
82
+ # Visit a parse tree produced by EALParser#maxAgeField.
83
+ def visitMaxAgeField(self, ctx:EALParser.MaxAgeFieldContext):
84
+ return self.visitChildren(ctx)
85
+
86
+
87
+ # Visit a parse tree produced by EALParser#inputField.
88
+ def visitInputField(self, ctx:EALParser.InputFieldContext):
89
+ return self.visitChildren(ctx)
90
+
91
+
92
+ # Visit a parse tree produced by EALParser#assumptionDecl.
93
+ def visitAssumptionDecl(self, ctx:EALParser.AssumptionDeclContext):
94
+ return self.visitChildren(ctx)
95
+
96
+
97
+ # Visit a parse tree produced by EALParser#assumptionField.
98
+ def visitAssumptionField(self, ctx:EALParser.AssumptionFieldContext):
99
+ return self.visitChildren(ctx)
100
+
101
+
102
+ # Visit a parse tree produced by EALParser#statementField.
103
+ def visitStatementField(self, ctx:EALParser.StatementFieldContext):
104
+ return self.visitChildren(ctx)
105
+
106
+
107
+ # Visit a parse tree produced by EALParser#validateField.
108
+ def visitValidateField(self, ctx:EALParser.ValidateFieldContext):
109
+ return self.visitChildren(ctx)
110
+
111
+
112
+ # Visit a parse tree produced by EALParser#validFromField.
113
+ def visitValidFromField(self, ctx:EALParser.ValidFromFieldContext):
114
+ return self.visitChildren(ctx)
115
+
116
+
117
+ # Visit a parse tree produced by EALParser#validUntilField.
118
+ def visitValidUntilField(self, ctx:EALParser.ValidUntilFieldContext):
119
+ return self.visitChildren(ctx)
120
+
121
+
122
+ # Visit a parse tree produced by EALParser#reasoningDecl.
123
+ def visitReasoningDecl(self, ctx:EALParser.ReasoningDeclContext):
124
+ return self.visitChildren(ctx)
125
+
126
+
127
+ # Visit a parse tree produced by EALParser#reasoningField.
128
+ def visitReasoningField(self, ctx:EALParser.ReasoningFieldContext):
129
+ return self.visitChildren(ctx)
130
+
131
+
132
+ # Visit a parse tree produced by EALParser#methodField.
133
+ def visitMethodField(self, ctx:EALParser.MethodFieldContext):
134
+ return self.visitChildren(ctx)
135
+
136
+
137
+ # Visit a parse tree produced by EALParser#rationaleField.
138
+ def visitRationaleField(self, ctx:EALParser.RationaleFieldContext):
139
+ return self.visitChildren(ctx)
140
+
141
+
142
+ # Visit a parse tree produced by EALParser#backingField.
143
+ def visitBackingField(self, ctx:EALParser.BackingFieldContext):
144
+ return self.visitChildren(ctx)
145
+
146
+
147
+ # Visit a parse tree produced by EALParser#transferField.
148
+ def visitTransferField(self, ctx:EALParser.TransferFieldContext):
149
+ return self.visitChildren(ctx)
150
+
151
+
152
+ # Visit a parse tree produced by EALParser#claimDecl.
153
+ def visitClaimDecl(self, ctx:EALParser.ClaimDeclContext):
154
+ return self.visitChildren(ctx)
155
+
156
+
157
+ # Visit a parse tree produced by EALParser#claimField.
158
+ def visitClaimField(self, ctx:EALParser.ClaimFieldContext):
159
+ return self.visitChildren(ctx)
160
+
161
+
162
+ # Visit a parse tree produced by EALParser#propositionDecl.
163
+ def visitPropositionDecl(self, ctx:EALParser.PropositionDeclContext):
164
+ return self.visitChildren(ctx)
165
+
166
+
167
+ # Visit a parse tree produced by EALParser#propositionField.
168
+ def visitPropositionField(self, ctx:EALParser.PropositionFieldContext):
169
+ return self.visitChildren(ctx)
170
+
171
+
172
+ # Visit a parse tree produced by EALParser#subjectField.
173
+ def visitSubjectField(self, ctx:EALParser.SubjectFieldContext):
174
+ return self.visitChildren(ctx)
175
+
176
+
177
+ # Visit a parse tree produced by EALParser#quantityField.
178
+ def visitQuantityField(self, ctx:EALParser.QuantityFieldContext):
179
+ return self.visitChildren(ctx)
180
+
181
+
182
+ # Visit a parse tree produced by EALParser#unitField.
183
+ def visitUnitField(self, ctx:EALParser.UnitFieldContext):
184
+ return self.visitChildren(ctx)
185
+
186
+
187
+ # Visit a parse tree produced by EALParser#scopeField.
188
+ def visitScopeField(self, ctx:EALParser.ScopeFieldContext):
189
+ return self.visitChildren(ctx)
190
+
191
+
192
+ # Visit a parse tree produced by EALParser#queryField.
193
+ def visitQueryField(self, ctx:EALParser.QueryFieldContext):
194
+ return self.visitChildren(ctx)
195
+
196
+
197
+ # Visit a parse tree produced by EALParser#resultField.
198
+ def visitResultField(self, ctx:EALParser.ResultFieldContext):
199
+ return self.visitChildren(ctx)
200
+
201
+
202
+ # Visit a parse tree produced by EALParser#argumentDecl.
203
+ def visitArgumentDecl(self, ctx:EALParser.ArgumentDeclContext):
204
+ return self.visitChildren(ctx)
205
+
206
+
207
+ # Visit a parse tree produced by EALParser#argumentFlow.
208
+ def visitArgumentFlow(self, ctx:EALParser.ArgumentFlowContext):
209
+ return self.visitChildren(ctx)
210
+
211
+
212
+ # Visit a parse tree produced by EALParser#support.
213
+ def visitSupport(self, ctx:EALParser.SupportContext):
214
+ return self.visitChildren(ctx)
215
+
216
+
217
+ # Visit a parse tree produced by EALParser#supportGroup.
218
+ def visitSupportGroup(self, ctx:EALParser.SupportGroupContext):
219
+ return self.visitChildren(ctx)
220
+
221
+
222
+ # Visit a parse tree produced by EALParser#supportKind.
223
+ def visitSupportKind(self, ctx:EALParser.SupportKindContext):
224
+ return self.visitChildren(ctx)
225
+
226
+
227
+ # Visit a parse tree produced by EALParser#patternDecl.
228
+ def visitPatternDecl(self, ctx:EALParser.PatternDeclContext):
229
+ return self.visitChildren(ctx)
230
+
231
+
232
+ # Visit a parse tree produced by EALParser#patternParameter.
233
+ def visitPatternParameter(self, ctx:EALParser.PatternParameterContext):
234
+ return self.visitChildren(ctx)
235
+
236
+
237
+ # Visit a parse tree produced by EALParser#parameterKind.
238
+ def visitParameterKind(self, ctx:EALParser.ParameterKindContext):
239
+ return self.visitChildren(ctx)
240
+
241
+
242
+ # Visit a parse tree produced by EALParser#applicationDecl.
243
+ def visitApplicationDecl(self, ctx:EALParser.ApplicationDeclContext):
244
+ return self.visitChildren(ctx)
245
+
246
+
247
+ # Visit a parse tree produced by EALParser#patternBinding.
248
+ def visitPatternBinding(self, ctx:EALParser.PatternBindingContext):
249
+ return self.visitChildren(ctx)
250
+
251
+
252
+ # Visit a parse tree produced by EALParser#bindingValue.
253
+ def visitBindingValue(self, ctx:EALParser.BindingValueContext):
254
+ return self.visitChildren(ctx)
255
+
256
+
257
+ # Visit a parse tree produced by EALParser#patternGuard.
258
+ def visitPatternGuard(self, ctx:EALParser.PatternGuardContext):
259
+ return self.visitChildren(ctx)
260
+
261
+
262
+ # Visit a parse tree produced by EALParser#objectionDecl.
263
+ def visitObjectionDecl(self, ctx:EALParser.ObjectionDeclContext):
264
+ return self.visitChildren(ctx)
265
+
266
+
267
+ # Visit a parse tree produced by EALParser#objectionSupport.
268
+ def visitObjectionSupport(self, ctx:EALParser.ObjectionSupportContext):
269
+ return self.visitChildren(ctx)
270
+
271
+
272
+ # Visit a parse tree produced by EALParser#objectionGroup.
273
+ def visitObjectionGroup(self, ctx:EALParser.ObjectionGroupContext):
274
+ return self.visitChildren(ctx)
275
+
276
+
277
+ # Visit a parse tree produced by EALParser#objectionSupportKind.
278
+ def visitObjectionSupportKind(self, ctx:EALParser.ObjectionSupportKindContext):
279
+ return self.visitChildren(ctx)
280
+
281
+
282
+ # Visit a parse tree produced by EALParser#targetKind.
283
+ def visitTargetKind(self, ctx:EALParser.TargetKindContext):
284
+ return self.visitChildren(ctx)
285
+
286
+
287
+ # Visit a parse tree produced by EALParser#argumentationDirective.
288
+ def visitArgumentationDirective(self, ctx:EALParser.ArgumentationDirectiveContext):
289
+ return self.visitChildren(ctx)
290
+
291
+
292
+ # Visit a parse tree produced by EALParser#referenceList.
293
+ def visitReferenceList(self, ctx:EALParser.ReferenceListContext):
294
+ return self.visitChildren(ctx)
295
+
296
+
297
+ # Visit a parse tree produced by EALParser#reference.
298
+ def visitReference(self, ctx:EALParser.ReferenceContext):
299
+ return self.visitChildren(ctx)
300
+
301
+
302
+ # Visit a parse tree produced by EALParser#qualifiedName.
303
+ def visitQualifiedName(self, ctx:EALParser.QualifiedNameContext):
304
+ return self.visitChildren(ctx)
305
+
306
+
307
+ # Visit a parse tree produced by EALParser#predicate.
308
+ def visitPredicate(self, ctx:EALParser.PredicateContext):
309
+ return self.visitChildren(ctx)
310
+
311
+
312
+ # Visit a parse tree produced by EALParser#expression.
313
+ def visitExpression(self, ctx:EALParser.ExpressionContext):
314
+ return self.visitChildren(ctx)
315
+
316
+
317
+ # Visit a parse tree produced by EALParser#orExpression.
318
+ def visitOrExpression(self, ctx:EALParser.OrExpressionContext):
319
+ return self.visitChildren(ctx)
320
+
321
+
322
+ # Visit a parse tree produced by EALParser#andExpression.
323
+ def visitAndExpression(self, ctx:EALParser.AndExpressionContext):
324
+ return self.visitChildren(ctx)
325
+
326
+
327
+ # Visit a parse tree produced by EALParser#comparisonExpression.
328
+ def visitComparisonExpression(self, ctx:EALParser.ComparisonExpressionContext):
329
+ return self.visitChildren(ctx)
330
+
331
+
332
+ # Visit a parse tree produced by EALParser#additiveExpression.
333
+ def visitAdditiveExpression(self, ctx:EALParser.AdditiveExpressionContext):
334
+ return self.visitChildren(ctx)
335
+
336
+
337
+ # Visit a parse tree produced by EALParser#multiplicativeExpression.
338
+ def visitMultiplicativeExpression(self, ctx:EALParser.MultiplicativeExpressionContext):
339
+ return self.visitChildren(ctx)
340
+
341
+
342
+ # Visit a parse tree produced by EALParser#unaryExpression.
343
+ def visitUnaryExpression(self, ctx:EALParser.UnaryExpressionContext):
344
+ return self.visitChildren(ctx)
345
+
346
+
347
+ # Visit a parse tree produced by EALParser#primaryExpression.
348
+ def visitPrimaryExpression(self, ctx:EALParser.PrimaryExpressionContext):
349
+ return self.visitChildren(ctx)
350
+
351
+
352
+ # Visit a parse tree produced by EALParser#comparator.
353
+ def visitComparator(self, ctx:EALParser.ComparatorContext):
354
+ return self.visitChildren(ctx)
355
+
356
+
357
+ # Visit a parse tree produced by EALParser#jsonValue.
358
+ def visitJsonValue(self, ctx:EALParser.JsonValueContext):
359
+ return self.visitChildren(ctx)
360
+
361
+
362
+ # Visit a parse tree produced by EALParser#jsonObject.
363
+ def visitJsonObject(self, ctx:EALParser.JsonObjectContext):
364
+ return self.visitChildren(ctx)
365
+
366
+
367
+ # Visit a parse tree produced by EALParser#jsonArray.
368
+ def visitJsonArray(self, ctx:EALParser.JsonArrayContext):
369
+ return self.visitChildren(ctx)
370
+
371
+
372
+ # Visit a parse tree produced by EALParser#jsonScalar.
373
+ def visitJsonScalar(self, ctx:EALParser.JsonScalarContext):
374
+ return self.visitChildren(ctx)
375
+
376
+
377
+ # Visit a parse tree produced by EALParser#signedNumber.
378
+ def visitSignedNumber(self, ctx:EALParser.SignedNumberContext):
379
+ return self.visitChildren(ctx)
380
+
381
+
382
+ # Visit a parse tree produced by EALParser#identifier.
383
+ def visitIdentifier(self, ctx:EALParser.IdentifierContext):
384
+ return self.visitChildren(ctx)
385
+
386
+
387
+ # Visit a parse tree produced by EALParser#lineEnd.
388
+ def visitLineEnd(self, ctx:EALParser.LineEndContext):
389
+ return self.visitChildren(ctx)
390
+
391
+
392
+
393
+ del EALParser
@@ -0,0 +1 @@
1
+ """ANTLR 4.13.2 generated recogniser; regenerate from grammar/EAL.g4."""
eal/host.py ADDED
@@ -0,0 +1,179 @@
1
+ """Strict JSON adapter for clients which cannot call MCP tools directly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import ipaddress
8
+ import json
9
+ import math
10
+ import os
11
+ import sys
12
+ from typing import Any
13
+
14
+ from fastmcp import Client
15
+ from fastmcp.client.transports import ClientTransport
16
+ from mcp import StdioServerParameters
17
+ from jsonschema import Draft202012Validator
18
+
19
+ from .client_transports import create_client_transport, create_http_transport, read_bearer_token
20
+ from .host_redaction import sanitise_response
21
+ from .operation_contracts import OPERATION_FIELDS
22
+ from .runtime import strict_json
23
+ from .server_settings import ServerSettings, parse_server_settings
24
+
25
+
26
+ MAX_REQUEST_BYTES = 2 * 1024 * 1024
27
+
28
+
29
+ def parse_request(text: str) -> tuple[str, dict[str, Any]]:
30
+ if len(text.encode("utf-8")) > MAX_REQUEST_BYTES:
31
+ raise ValueError("Host request exceeds 2 MiB")
32
+ request = strict_json(text)
33
+ if not isinstance(request, dict):
34
+ raise ValueError("Request must be one JSON object")
35
+ operation = request.get("operation")
36
+ if not isinstance(operation, str) or operation not in OPERATION_FIELDS:
37
+ raise ValueError("operation must be " + ", ".join(OPERATION_FIELDS))
38
+ required, optional = OPERATION_FIELDS[operation]
39
+ arguments = {key: value for key, value in request.items() if key != "operation"}
40
+ if missing := required - set(arguments):
41
+ raise ValueError(f"Missing request fields: {', '.join(sorted(missing))}")
42
+ if unknown := set(arguments) - required - optional:
43
+ raise ValueError(f"Unknown request fields: {', '.join(sorted(unknown))}")
44
+ for key in ("source", "assessment_id", "entry_id"):
45
+ if key in arguments and not isinstance(arguments[key], str):
46
+ raise ValueError(f"{key} must be a string")
47
+ if operation == "assess_known" and not arguments["entry_id"]:
48
+ raise ValueError("assess_known requires a non-empty registered entry ID")
49
+ for key in ("claim", "collection_id", "now", "semantics", "query_mode", "preference"):
50
+ if key in arguments and arguments[key] is not None and not isinstance(arguments[key], str):
51
+ raise ValueError(f"{key} must be a string or null")
52
+ if operation in {"plan", "collect_claim", "assess_known"} and not arguments["claim"]:
53
+ raise ValueError(f"{operation} requires a non-empty claim identifier")
54
+ if operation == "compile_aspic":
55
+ if not isinstance(arguments["goal"], str) or not arguments["goal"].strip():
56
+ raise ValueError("compile_aspic requires a non-empty goal string")
57
+ if not isinstance(arguments["collection_id"], str) or not arguments["collection_id"]:
58
+ raise ValueError("compile_aspic requires a non-empty collection_id string")
59
+ if "query" in arguments and arguments["query"] is not None and not isinstance(arguments["query"], str):
60
+ raise ValueError("query must be a string or null")
61
+ for key in ("limit", "offset"):
62
+ if key in arguments and type(arguments[key]) is not int:
63
+ raise ValueError(f"{key} must be an integer")
64
+ if "context" in arguments and not isinstance(arguments["context"], dict):
65
+ raise ValueError("context must be an object")
66
+ for key in ("evidence_ids", "arguments"):
67
+ value = arguments.get(key)
68
+ if (value is not None or key in required) and (not isinstance(value, list) or not all(isinstance(item, str) for item in value)):
69
+ raise ValueError(f"{key} must be a list of strings")
70
+ if "attacks" in arguments:
71
+ attacks = arguments["attacks"]
72
+ if not isinstance(attacks, list) or not all(isinstance(edge, list) and len(edge) == 2 and all(isinstance(item, str) for item in edge) for edge in attacks):
73
+ raise ValueError("attacks must be a list of [attacker, target] string pairs")
74
+ return "eal_" + operation, arguments
75
+
76
+
77
+ async def _dispatch_tool(tool: str, arguments: dict[str, Any], parameters: StdioServerParameters | ClientTransport,
78
+ *, timeout_seconds: float = 120.0) -> dict[str, Any]:
79
+ """Send a schema-checked operation through either configured transport."""
80
+ if (isinstance(timeout_seconds, bool) or not isinstance(timeout_seconds, (int, float))
81
+ or not math.isfinite(timeout_seconds) or timeout_seconds <= 0):
82
+ raise ValueError("Host timeout must be positive and finite")
83
+ async with asyncio.timeout(timeout_seconds):
84
+ async with Client(create_client_transport(parameters)) as client:
85
+ available = await client.list_tools()
86
+ schema = next((entry.input_schema for entry in available if entry.name == tool), None)
87
+ if schema is None:
88
+ raise ValueError(f"MCP server does not offer {tool}")
89
+ errors = list(Draft202012Validator(schema).iter_errors(arguments))
90
+ if errors:
91
+ raise ValueError("Discovered input schema rejected request: " + errors[0].message)
92
+ result = await client.call_tool(tool, arguments=arguments, raise_on_error=False)
93
+ content = [item.model_dump(mode="json", exclude_none=True) for item in result.content]
94
+ return {
95
+ "operation": tool.removeprefix("eal_"), "protocol_version": client.protocol_version,
96
+ "is_error": bool(result.is_error), "result": result.structured_content,
97
+ "content": content,
98
+ }
99
+
100
+
101
+ async def dispatch_request(text: str, parameters: StdioServerParameters | ClientTransport,
102
+ *, timeout_seconds: float = 120.0) -> dict[str, Any]:
103
+ """Send one strict JSON request over MCP."""
104
+ tool, arguments = parse_request(text)
105
+ return await _dispatch_tool(tool, arguments, parameters, timeout_seconds=timeout_seconds)
106
+
107
+
108
+ def _server_arguments(settings: ServerSettings) -> list[str]:
109
+ """Pass resolved trusted settings to a stdio child without re-reading config."""
110
+ arguments = ["-m", "eal.server", "--workspace", str(settings.workspace),
111
+ "--transport", "stdio", "--exposure", settings.exposure,
112
+ "--max-in-flight", str(settings.max_in_flight)]
113
+ for name in ("registry", "database", "methods", "limits"):
114
+ if value := getattr(settings, name):
115
+ arguments.extend(["--" + name, str(value)])
116
+ for identifier in settings.known_entries:
117
+ arguments.extend(["--known-entry", identifier])
118
+ return arguments
119
+
120
+
121
+ def _configured_http_url(settings: ServerSettings) -> str:
122
+ """Use a concrete configured HTTP bind address as a client target."""
123
+ try:
124
+ address = ipaddress.ip_address(settings.host)
125
+ except ValueError:
126
+ address = None
127
+ if address is not None and address.is_unspecified:
128
+ raise ValueError("An HTTP configuration with a wildcard bind address requires --url")
129
+ host = f"[{settings.host}]" if ":" in settings.host else settings.host
130
+ return f"http://{host}:{settings.port}{settings.path}"
131
+
132
+
133
+ def main() -> None:
134
+ parser = argparse.ArgumentParser(description="Send one JSON request to the EAL MCP server", exit_on_error=False,
135
+ epilog="Service settings use the eal-mcp options, including --config, --workspace, "
136
+ "--registry, --database, --methods, --limits, --known-entry and --token-env. "
137
+ "An HTTP transport connects to an existing server.")
138
+ parser.add_argument("--url", help="Existing Streamable HTTP endpoint; overrides the configured transport")
139
+ parser.add_argument("--timeout", type=float, default=120.0, help="Total MCP session deadline in seconds")
140
+ token = None
141
+ try:
142
+ args, service_args = parser.parse_known_args()
143
+ try:
144
+ settings = parse_server_settings(service_args)
145
+ except SystemExit as exc:
146
+ if exc.code == 0:
147
+ raise
148
+ raise ValueError("Invalid server configuration arguments") from exc
149
+ raw = sys.stdin.buffer.read(MAX_REQUEST_BYTES + 1)
150
+ if len(raw) > MAX_REQUEST_BYTES:
151
+ raise ValueError("Host input exceeds its configured byte limit")
152
+ if args.url is not None or settings.transport == "http":
153
+ token = read_bearer_token(settings.token_env)
154
+ parameters = create_http_transport(args.url if args.url is not None else _configured_http_url(settings),
155
+ token_env=settings.token_env)
156
+ else:
157
+ # A source checkout and its child load matching application code.
158
+ parameters = StdioServerParameters(command=sys.executable, args=_server_arguments(settings),
159
+ env={key: value for key, value in os.environ.items() if not key.startswith("EAL_MCP_")})
160
+ result = asyncio.run(dispatch_request(raw.decode("utf-8"), parameters,
161
+ timeout_seconds=args.timeout))
162
+ print(json.dumps(sanitise_response(result, token), ensure_ascii=False, allow_nan=False))
163
+ if result.get("is_error"):
164
+ raise SystemExit(1)
165
+ except Exception as exc:
166
+ # Keep the one-request/one-JSON-response contract on transport failures.
167
+ def describe(error):
168
+ if isinstance(error, BaseExceptionGroup):
169
+ return "; ".join(describe(child) for child in error.exceptions)
170
+ if isinstance(error, TimeoutError):
171
+ return "MCP session exceeded its configured deadline"
172
+ return str(error)
173
+
174
+ print(json.dumps(sanitise_response({"is_error": True, "error": describe(exc)}, token)))
175
+ raise SystemExit(2) from exc
176
+
177
+
178
+ if __name__ == "__main__":
179
+ main()
eal/host_redaction.py ADDED
@@ -0,0 +1,51 @@
1
+ """Sanitise host response values without changing their JSON contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from .tool_acquisition import strict_json
9
+
10
+
11
+ def _redact(value: Any, token: str | None) -> Any:
12
+ """Sanitise remote values while preserving their JSON field names.
13
+
14
+ A credential can coincide with a public schema identifier. Field names are
15
+ structural metadata, so redaction applies to values instead. MCP text blocks
16
+ can contain a second JSON representation of the application result; decode
17
+ those before redaction to preserve the same field names in both forms.
18
+ """
19
+ if token is None:
20
+ return value
21
+ if isinstance(value, str):
22
+ try:
23
+ decoded = strict_json(value)
24
+ except ValueError:
25
+ decoded = None
26
+ if isinstance(decoded, (dict, list, str)):
27
+ sanitised = _redact(decoded, token)
28
+ if sanitised == decoded:
29
+ return value
30
+ return json.dumps(sanitised, ensure_ascii=False, allow_nan=False)
31
+ return value.replace(token, "[redacted]")
32
+ if isinstance(value, list):
33
+ return [_redact(item, token) for item in value]
34
+ if isinstance(value, dict):
35
+ return {key: _redact(item, token) for key, item in value.items()}
36
+ return value
37
+
38
+
39
+ def sanitise_response(response: dict[str, Any], token: str | None) -> dict[str, Any]:
40
+ """Keep public host metadata stable and sanitise application data and errors."""
41
+ metadata = {"operation", "protocol_version", "is_error"}
42
+ sanitised = {key: value if key in metadata else _redact(value, token)
43
+ for key, value in response.items()}
44
+ if isinstance(response.get("content"), list):
45
+ sanitised["content"] = [
46
+ {key: value if key == "type" else _redact(value, token)
47
+ for key, value in block.items()} if isinstance(block, dict)
48
+ else _redact(block, token)
49
+ for block in response["content"]
50
+ ]
51
+ return sanitised