diff --git a/AGENTS.md b/AGENTS.md
index e3f99929b..2ecc3d6f2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -878,7 +878,9 @@ landed — and **the MCP server and two other repositories brought to the releas
MCP still says what the library does. An operation the library offers by name is an entry of
`Core/Operations/Catalogue.cs` ([#1684](https://github.com/asc-community/AngouriMath/issues/1684)),
and a new factory of `Transformation` fails `CatalogueTest` until it is an entry or a building
- block with its reason.
+ block with its reason. The MCP server's tools answer from those entries, and a new entry fails
+ the server's own `CatalogueTest` until a tool offers it or `Tools.NotOffered` says why none does,
+ and the terminal's `OneShotTest` until a command offers it or `OneShot.notOffered` lists it.
- [CSharpMath](https://github.com/verybadcat/CSharpMath), for the round trip of `Latexize` — see
*The standard command wins* above. If a PR there from an earlier release is still unmerged, add
to it rather than opening a second.
diff --git a/Sources/AngouriMath/Functions/InternalAMExtensions.cs b/Sources/AngouriMath/Functions/InternalAMExtensions.cs
index b41a2d8f6..35574154b 100644
--- a/Sources/AngouriMath/Functions/InternalAMExtensions.cs
+++ b/Sources/AngouriMath/Functions/InternalAMExtensions.cs
@@ -37,6 +37,16 @@
+ "552aeb08b7a67f49a5557ef4faab3d9f033182be1a646b24c932399732107462848dcb97acecc844"
+ "9997b1b77e6bd337e1116878226dd4954e004f11193ccd8e29fc156615c798f733712923ffe8d6c5")]
+// And to the MCP server behind `amcli mcp`, whose tools answer from the catalogue of operations in
+// Core/Operations, which stays internal until v3. The server is no package of its own: it ships
+// inside the terminal's tool with the library it was built against, so it never meets another
+// version's internals. https://github.com/asc-community/AngouriMath/issues/1684
+[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AngouriMath.Mcp, PublicKey="
+ + "0024000004800000940000000602000000240000525341310004000001000100f54ef0f5905ba32e"
+ + "cae04751103c5565283a4c4fa2b07627ff7a7e556b287f198203a09935d998ce9d812a696aa2e71c"
+ + "552aeb08b7a67f49a5557ef4faab3d9f033182be1a646b24c932399732107462848dcb97acecc844"
+ + "9997b1b77e6bd337e1116878226dd4954e004f11193ccd8e29fc156615c798f733712923ffe8d6c5")]
+
namespace AngouriMath
{
// Visibility for the class is internal so we can use public for methods as we like
diff --git a/Sources/MCP/AngouriMath.Mcp/AngouriMath.Mcp.csproj b/Sources/MCP/AngouriMath.Mcp/AngouriMath.Mcp.csproj
index b1ae0d429..03e9df424 100644
--- a/Sources/MCP/AngouriMath.Mcp/AngouriMath.Mcp.csproj
+++ b/Sources/MCP/AngouriMath.Mcp/AngouriMath.Mcp.csproj
@@ -18,10 +18,18 @@
truefalse
+
+ true
+ ../../AngouriMath/key.snk
-
+
+
diff --git a/Sources/MCP/AngouriMath.Mcp/Tools.cs b/Sources/MCP/AngouriMath.Mcp/Tools.cs
index 5f6e6c855..a6e0fd9f3 100644
--- a/Sources/MCP/AngouriMath.Mcp/Tools.cs
+++ b/Sources/MCP/AngouriMath.Mcp/Tools.cs
@@ -8,6 +8,7 @@
using System.Text.Json.Nodes;
using AngouriMath;
using AngouriMath.Core;
+using AngouriMath.Core.Operations;
using AngouriMath.Extensions;
using static AngouriMath.Entity;
@@ -70,6 +71,69 @@ private static JsonObject Schema(JsonObject props, params string[] required)
private const string PreliminaryMark = "Preliminary: its arguments and answers may change in a minor release. ";
+ ///
+ /// The operations of the library's catalogue that each tool offers, by the tool's name. A
+ /// tool's answers come from the catalogue's entries; its guards, its checks of an answer and
+ /// its formatting are its own.
+ ///
+ ///
+ /// A test checks this against the tools and the catalogue: every tool offers an entry or is in
+ /// , and every entry is offered by a tool or is in
+ /// . So an operation added to the catalogue fails that test until a
+ /// tool offers it or this file says why none does.
+ /// https://github.com/asc-community/AngouriMath/issues/1684
+ ///
+ public static readonly IReadOnlyDictionary Offers = new Dictionary
+ {
+ ["am_parse"] = [Catalogue.Variables, Catalogue.Latex],
+ ["am_simplify"] = [Catalogue.Simplify, Catalogue.Alternatives],
+ ["am_solve"] = [Catalogue.Solve],
+ ["am_differentiate"] = [Catalogue.Differentiate],
+ ["am_integrate"] = [Catalogue.Integrate],
+ ["am_limit"] = [Catalogue.Limit],
+ ["am_evaluate"] = [Catalogue.EvaluateNumerically],
+ ["am_truth_table"] = [Catalogue.TruthTable, Catalogue.SatisfyingAssignments],
+ ["am_solve_system"] = [Catalogue.SolveSystem],
+ ["am_represent"] = [Catalogue.ToBase, Catalogue.FromBase],
+ ["am_matrix"] = [Catalogue.Determinant, Catalogue.Inverse, Catalogue.Transpose, Catalogue.Rank,
+ Catalogue.ReducedRowEchelonForm, Catalogue.Trace, Catalogue.TensorProduct],
+ ["am_substitute"] = [Catalogue.Substitute],
+ ["am_expand"] = [Catalogue.Expand],
+ ["am_factor"] = [Catalogue.Factorize],
+ ["am_series"] = [Catalogue.Series],
+ ["am_number_theory"] = [Catalogue.PrimeFactorization, Catalogue.Totient, Catalogue.GreatestCommonDivisor,
+ Catalogue.DivisorCount, Catalogue.IsPrime],
+ ["am_to_sympy"] = [Catalogue.Sympy],
+ };
+
+ ///
+ /// The tools whose answer is the server's own computation rather than an operation of the
+ /// catalogue, each with what it computes.
+ ///
+ public static readonly IReadOnlyDictionary OwnComputations = new Dictionary
+ {
+ ["am_verify_equal"] = "whether two expressions are equal, by simplifying their difference and by sampling it",
+ ["am_check_steps"] = "whether each step of a derivation follows from the one before",
+ ["am_domain_check"] = "where an expression is undefined or leaves the real line, by sampling",
+ ["am_eigenvalues"] = "a matrix's eigenvalues, as the roots of its characteristic polynomial",
+ ["am_compare_numeric"] = "where an approximation parts from what it approximates",
+ ["am_classify"] = "a guess at a formula's field, from the names of its symbols",
+ };
+
+ /// The catalogue's entries that no tool offers, by name, each with the reason.
+ public static readonly IReadOnlyDictionary NotOffered = new Dictionary
+ {
+ [Catalogue.Normalize.Name] = "am_simplify offers the whole simplification, which starts with this pass",
+ [Catalogue.Evaluate.Name] = "am_evaluate keeps an exact value exact, simplifying rather than evaluating, and adds a numeric value beside it",
+ };
+
+ /// The names of the catalogue's entries, which a test checks the lists above against.
+ internal static IEnumerable CatalogueNames => Catalogue.All.Select(operation => operation.Name);
+
+ /// by the entries' names.
+ internal static IReadOnlyDictionary OfferedNames =>
+ Offers.ToDictionary(tool => tool.Key, tool => tool.Value.Select(operation => operation.Name).ToArray());
+
/// The tools offered to a client: every one, or all but the preliminary ones.
public static JsonArray List(bool preliminary)
{
@@ -625,7 +689,7 @@ private static JsonObject Respond(Entity input, Entity result, List warn
["status"] = status,
["parsed"] = input.Stringize(),
["result"] = raw,
- ["latex"] = result.Latexize(),
+ ["latex"] = Catalogue.Latex.Ask(result),
};
if (status == "declined")
@@ -682,14 +746,14 @@ private static JsonObject Parse(JsonObject args)
return error!;
var vars = new JsonArray();
- foreach (var v in e.Vars) vars.Add(v.Stringize());
+ foreach (var v in Catalogue.Variables.Ask(e)) vars.Add(v.Stringize());
var response = new JsonObject
{
["status"] = "solved",
["parsed"] = e.Stringize(),
["result"] = e.Stringize(),
- ["latex"] = e.Latexize(),
+ ["latex"] = Catalogue.Latex.Ask(e),
["free_variables"] = vars,
["node_count"] = e.Nodes.Count(),
};
@@ -707,10 +771,10 @@ private static JsonObject Simplify(JsonObject args)
return FromOutcome(Guard.Run(() =>
{
- var simplified = e.Simplify();
+ var simplified = Catalogue.Simplify.Apply(e);
var alternates = new List();
if (wantAlternatives)
- foreach (var alt in e.Alternate(4).Take(5))
+ foreach (var alt in Catalogue.Alternatives.Ask(e, 4).Take(5))
{
var s = alt.Stringize();
if (s != simplified.Stringize() && !alternates.Contains(s))
@@ -748,7 +812,7 @@ private static JsonObject Solve(JsonObject args)
if (!TryParse(combined, "constraints", false, out var e, out var warnings, out var error))
return error!;
- return FromOutcome(Guard.Run(() => e.Solve(Var(variable!))), set =>
+ return FromOutcome(Guard.Run(() => Catalogue.Solve.Ask(e, Var(variable!))), set =>
{
var response = Respond(e, set, warnings);
if (set is Set.FiniteSet finite)
@@ -761,7 +825,7 @@ private static JsonObject Solve(JsonObject args)
// output-tidiness weakness the library is known for, so tidy each root.
// Guarded because Simplify can be slow and must not lose the answer.
Entity tidy;
- try { tidy = root.Simplify(); }
+ try { tidy = Catalogue.Simplify.Apply(root); }
catch { tidy = root; }
roots.Add(tidy.Stringize());
}
@@ -790,7 +854,7 @@ private static JsonObject Differentiate(JsonObject args)
if (order < 1) return Fail("'order' must be at least 1");
return FromOutcome(
- Guard.Run(() => e.Differentiate(Var(variable!), order).Simplify()),
+ Guard.Run(() => Catalogue.Simplify.Apply(Catalogue.Differentiate.Apply(e, Var(variable!), order))),
d => Respond(e, d, warnings));
}
@@ -834,13 +898,13 @@ private static JsonObject Integrate(JsonObject args)
return Numeric.TryDefiniteIntegral(e, v, a, b);
}
- var antiderivative = e.Integrate(v);
+ var antiderivative = Catalogue.Integrate.Apply(e, v);
// Decline check on the RAW result, before Simplify — see Guard.IsDeclined.
if (Guard.IsDeclined(antiderivative.Stringize()))
return (antiderivative, (bool?)null, (Entity?)null, Quadrature());
- var tidy = antiderivative.Simplify();
+ var tidy = Catalogue.Simplify.Apply(antiderivative);
// Verify by differentiating back, which is the whole reason to trust the answer.
bool? verified;
@@ -859,7 +923,7 @@ private static JsonObject Integrate(JsonObject args)
if (lower is not null && upper is not null)
{
var bare = Numeric.Strip(tidy);
- definite = (bare.Substitute(v, upper) - bare.Substitute(v, lower)).Simplify();
+ definite = Catalogue.Simplify.Apply(Catalogue.Substitute.Apply(bare, v, upper) - Catalogue.Substitute.Apply(bare, v, lower));
}
// Only where the exact route came back with nothing to show. A NaN definite
@@ -894,7 +958,7 @@ private static JsonObject Integrate(JsonObject args)
if (r.Item3 is { } definite)
{
response["definite_value"] = definite.Stringize();
- response["definite_latex"] = definite.Latexize();
+ response["definite_latex"] = Catalogue.Latex.Ask(definite);
response["definite_caveat"] =
"Computed as F(to) - F(from) from the verified antiderivative. That is " +
"only valid when the integrand is continuous across the whole interval — " +
@@ -954,10 +1018,10 @@ private static JsonObject Limit(JsonObject args)
return FromOutcome(Guard.Run(() =>
{
- var raw = e.Limit(Var(variable!), destination, side);
+ var raw = Catalogue.Limit.Apply(e, Var(variable!), destination, side);
// Check for a decline BEFORE simplifying: an unevaluated limit(...) simplifies
// to NaN, which would look like a wrong answer instead of a missing solver.
- return Guard.IsDeclined(raw.Stringize()) ? raw : raw.Simplify();
+ return Guard.IsDeclined(raw.Stringize()) ? raw : Catalogue.Simplify.Apply(raw);
}), r =>
{
var response = Respond(e, r, warnings);
@@ -1011,19 +1075,19 @@ private static JsonObject Evaluate(JsonObject args)
// Resolve calculus nodes BEFORE substituting. Substituting into
// derivative(f(x), x) cannot work — x is bound there — so `d/dx sqrt(1+x) at
// x=0` silently came back with x still free. Differentiate first, then plug in.
- var resolved = input.InnerSimplified;
+ var resolved = Catalogue.Normalize.Apply(input);
foreach (var (name, value) in substitutions)
- resolved = resolved.Substitute(Var(name), value);
+ resolved = Catalogue.Substitute.Apply(resolved, Var(name), value);
// Simplify, not Evaled: Evaled numericises aggressively, turning
// sin(pi/3) + cos(pi/6) into a hundred decimal digits and throwing away the
// exact sqrt(3) that is the whole reason to use a CAS.
- var exact = resolved.Simplify();
+ var exact = Catalogue.Simplify.Apply(resolved);
string? approx = null;
if (exact.EvaluableNumerical)
{
- var complex = exact.EvalNumerical();
+ var complex = Catalogue.EvaluateNumerically.Ask(exact);
approx = !complex.ImaginaryPart.EDecimal.IsZero
? complex.Stringize()
: digits > 0
@@ -1054,7 +1118,7 @@ private static JsonObject VerifyEqual(JsonObject args)
// Exact first: if the difference reduces to zero, that settles it. The zero test
// has to look through any `provided` guard — the difference of two equal
// expressions is routinely "0 provided x > 0", which is still zero.
- var difference = (left - right).Simplify();
+ var difference = Catalogue.Simplify.Apply(left - right);
var exactlyZero = Numeric.IsZero(difference);
bool? numerically = null;
@@ -1167,15 +1231,15 @@ private static JsonObject TruthTable(JsonObject args)
var names = A(args, "variables");
var vars = names.Count > 0
? names.Select(Var).ToArray()
- : e.Vars.ToArray();
+ : Catalogue.Variables.Ask(e).ToArray();
if (vars.Length == 0) return Fail("no variables found in the expression");
if (vars.Length > 12) return Fail($"{vars.Length} variables would need {1L << vars.Length} rows; cap is 12");
return FromOutcome(Guard.Run(() =>
{
- var table = MathS.Boolean.BuildTruthTable(e, vars);
- var satisfying = MathS.SolveBooleanTable(e, vars);
+ var table = Catalogue.TruthTable.Ask(e, vars);
+ var satisfying = Catalogue.SatisfyingAssignments.Ask(e, vars);
return (table, satisfying);
}), r =>
{
@@ -1218,7 +1282,7 @@ private static JsonObject SolveSystem(JsonObject args)
}
return FromOutcome(Guard.Run(() =>
- MathS.Equations([.. parsed]).Solve([.. variables.Select(Var)])), matrix =>
+ Catalogue.SolveSystem.Ask(parsed, [.. variables.Select(Var)])), matrix =>
{
var order = new JsonArray();
foreach (var v in variables) order.Add(v);
@@ -1301,7 +1365,7 @@ private static JsonObject DomainCheck(JsonObject args)
return FromOutcome(Guard.Run(() =>
{
Entity simplified;
- try { simplified = e.Simplify(); }
+ try { simplified = Catalogue.Simplify.Apply(e); }
catch { simplified = e; }
return (simplified,
@@ -1385,10 +1449,10 @@ private static JsonObject RepresentBase(JsonObject args)
{
var asDecimal = fromBase == 10
? value!.ToEntity()
- : MathS.FromBaseN(value!, fromBase);
+ : Catalogue.FromBase.Ask(value!, fromBase);
- var real = (Number.Real)asDecimal.Evaled;
- var converted = toBase == 10 ? real.Stringize() : MathS.ToBaseN(real, toBase);
+ var real = (Number.Real)Catalogue.Evaluate.Ask(asDecimal);
+ var converted = toBase == 10 ? real.Stringize() : Catalogue.ToBase.Ask(real, toBase);
return (real, converted);
}), r => new JsonObject
{
@@ -1496,7 +1560,7 @@ private static JsonObject RepresentFromFixed(JsonObject args)
["fraction_bits"] = fractionBits,
["result"] = v.Stringize(),
["approximate"] = v.EvaluableNumerical
- ? v.EvalNumerical().RealPart.EDecimal.ToDouble().ToString("R")
+ ? Catalogue.EvaluateNumerically.Ask(v).RealPart.EDecimal.ToDouble().ToString("R")
: null,
});
}
@@ -1536,7 +1600,7 @@ private static JsonObject RepresentPolar(JsonObject args)
return FromOutcome(Guard.Run(() =>
{
- var evaluated = e.Evaled;
+ var evaluated = Catalogue.Evaluate.Ask(e);
if (evaluated is not Number.Complex complex)
throw new ArgumentException($"'{e.Stringize()}' is not a number");
@@ -1577,7 +1641,7 @@ private static JsonObject RepresentRectangular(JsonObject args)
["status"] = "solved",
["operation"] = "rectangular",
["result"] = rectangular.Stringize(),
- ["latex"] = rectangular.Latexize(),
+ ["latex"] = Catalogue.Latex.Ask(rectangular),
};
if (warnings.Count > 0) response["warnings"] = Warn(warnings);
return response;
@@ -1616,16 +1680,16 @@ private static JsonObject MatrixOp(JsonObject args)
return FromOutcome(Guard.Run