Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions Sources/AngouriMath/Functions/InternalAMExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion Sources/MCP/AngouriMath.Mcp/AngouriMath.Mcp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- Shipped inside the terminal's tool package, not as a package of its own. -->
<IsPackable>false</IsPackable>
<!-- Signed with the library's key, so that the library's InternalsVisibleTo can name it and
the tools can answer from its internal catalogue of operations: a strong-named assembly
may only befriend another strong-named one. It ships with the library it was built
against, so it never meets another version's internals.
https://github.com/asc-community/AngouriMath/issues/1684 -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../AngouriMath/key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="McpUnitTests" />
<!-- Its tests are signed with the same key, which a signed assembly's friends have to be. -->
<InternalsVisibleTo Include="McpUnitTests" Key="0024000004800000940000000602000000240000525341310004000001000100f54ef0f5905ba32ecae04751103c5565283a4c4fa2b07627ff7a7e556b287f198203a09935d998ce9d812a696aa2e71c552aeb08b7a67f49a5557ef4faab3d9f033182be1a646b24c932399732107462848dcb97acecc8449997b1b77e6bd337e1116878226dd4954e004f11193ccd8e29fc156615c798f733712923ffe8d6c5" />
</ItemGroup>

<ItemGroup>
Expand Down
174 changes: 118 additions & 56 deletions Sources/MCP/AngouriMath.Mcp/Tools.cs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Sources/MCP/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ Each of these looks arbitrary and is not:
corrupts the stream, and the client reports the server as failed.
- **Every library call goes through `Guard.Run`.** It is what keeps a stack overflow or a hang
inside one request.
- **A tool's answer comes from the library's catalogue of operations**, `Core/Operations/Catalogue.cs`,
and `Tools.Offers` says which entries each tool offers. An entry added there fails
`CatalogueTest` until a tool offers it or `Tools.NotOffered` says why none does, so the server
keeps up with the library by construction. The server's own checks of an answer, such as
differentiating an antiderivative back, call the library directly. The catalogue is internal,
which is why this assembly is signed with the library's key.
- **Decline detection reads the raw result, before any `Simplify`.** An unevaluated `limit(...)`
simplifies to `NaN`, which would turn an honest decline into what looks like a wrong answer.
- **Simplify before stripping a `provided` guard, never after.** The guard is what licenses the
Expand Down
35 changes: 35 additions & 0 deletions Sources/Terminal/AngouriMath.Terminal.Lib/OneShot.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,41 @@ An argument written as _ is read from stdin, and so is one that is left out:
/// The names <c>run</c> answers. Anything else as a first argument is a usage error.
let commands = set [ "eval"; "simp"; "fsimp"; "diff"; "solve"; "sub"; "latex"; "info"; "mcp"; "help"; "-h"; "--help" ]

/// <summary>
/// The operations of the library's catalogue, <c>Core/Operations/Catalogue.cs</c>, that each command
/// offers, by the entries' names. A test checks this against <see cref="commands"/> and the
/// catalogue: every command offers an entry or is in <see cref="ownCommands"/>, and every entry is
/// offered or is in <see cref="notOffered"/>.
/// https://github.com/asc-community/AngouriMath/issues/1684
/// </summary>
let offers =
Map [ "eval", [ "evaluate" ]
"simp", [ "simplify" ]
"fsimp", [ "normalize" ]
"diff", [ "differentiate" ]
"solve", [ "solve" ]
"sub", [ "substitute" ]
"latex", [ "latex" ] ]

/// The commands that are not one of the catalogue's operations, each with what it does.
let ownCommands =
Map [ "info", "prints the variables, derivatives, roots and stationary points, which several operations answer together"
"mcp", "serves the library to an agent"
"help", "prints the usage"
"-h", "prints the usage"
"--help", "prints the usage" ]

/// <summary>
/// The catalogue's entries that no command offers. The one-shot commands are the eight of
/// asc-community/AngouriMathCLI, kept so that its scripts keep working, and which others to add is
/// the question of #1019 item 16; an entry added to the catalogue joins this list or a command.
/// </summary>
let notOffered =
set [ "expand"; "factorize"; "integrate"; "limit"; "solve_system"; "evaluate_numerically"; "variables"
"alternatives"; "sympy"; "truth_table"; "satisfying_assignments"; "series"; "to_base"; "from_base"
"determinant"; "inverse"; "transpose"; "rank"; "reduced_row_echelon_form"; "trace"; "tensor_product"
"prime_factorization"; "totient"; "greatest_common_divisor"; "divisor_count"; "is_prime" ]

/// The arguments still to read, falling back to a line of stdin where one is missing or is <c>_</c>.
type private Arguments(given: string list, input: TextReader) =
let mutable rest = given
Expand Down
42 changes: 42 additions & 0 deletions Sources/Tests/McpUnitTests/CatalogueTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// Copyright (c) 2019-2026 Angouri.
// AngouriMath is licensed under MIT.
// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md.
// Website: https://am.angouri.org.
//

using Xunit;

namespace AngouriMath.Mcp.Tests;

/// <summary>
/// The tools against the library's catalogue of operations: every tool offers an entry or says
/// what it computes itself, and every entry is offered by a tool or is listed with the reason
/// none offers it. An operation added to the catalogue fails here until the server takes it.
/// https://github.com/asc-community/AngouriMath/issues/1684
/// </summary>
public sealed class CatalogueTest
{
private static readonly HashSet<string> ToolNames =
Tools.List(preliminary: true).Select(tool => tool!["name"]!.GetValue<string>()).ToHashSet();

[Fact]
public void EveryToolOffersAnEntryOrSaysWhatItComputes()
{
var unaccounted = ToolNames.Where(tool => !Tools.OfferedNames.ContainsKey(tool) && !Tools.OwnComputations.ContainsKey(tool));
Assert.Empty(unaccounted);
Assert.Empty(Tools.OfferedNames.Keys.Intersect(Tools.OwnComputations.Keys));
Assert.Empty(Tools.OfferedNames.Keys.Concat(Tools.OwnComputations.Keys).Where(tool => !ToolNames.Contains(tool)));
Assert.All(Tools.OfferedNames, tool => Assert.NotEmpty(tool.Value));
}

[Fact]
public void EveryEntryIsOfferedOrSaysWhyNot()
{
var offered = Tools.OfferedNames.Values.SelectMany(names => names).ToHashSet();
var catalogue = Tools.CatalogueNames.ToHashSet();
Assert.Empty(catalogue.Where(entry => !offered.Contains(entry) && !Tools.NotOffered.ContainsKey(entry)));
Assert.Empty(offered.Intersect(Tools.NotOffered.Keys));
Assert.Empty(Tools.NotOffered.Keys.Where(entry => !catalogue.Contains(entry)));
}
}
4 changes: 4 additions & 0 deletions Sources/Tests/McpUnitTests/McpUnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<!-- Signed with the library's key, as the server it tests is, so that the server's
InternalsVisibleTo can name it. -->
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>../../AngouriMath/key.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
Expand Down
32 changes: 32 additions & 0 deletions Sources/Tests/TerminalUnitTests/OneShotTest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,35 @@ let ``info names the variables and gives the derivative and the roots for each``
Assert.Contains("variables: x", lines)
Assert.Contains("derivative with respect to x: 2 * x", lines)
Assert.Contains(lines, fun line -> line.StartsWith("roots for x: ") && line.Contains("-2") && line.Contains("2"))

/// The names of the library's catalogue of operations, which is internal, so read here by reflection.
/// https://github.com/asc-community/AngouriMath/issues/1684
let private catalogue =
let read (name: string) (instance: obj | null) (owner: System.Type | null) =
match owner with
| null -> failwith "the library has no catalogue"
| owner ->
match owner.GetProperty name with
| null -> failwith $"{owner.Name} has no {name}"
| property -> property.GetValue instance
match read "All" null (typeof<AngouriMath.Entity>.Assembly.GetType "AngouriMath.Core.Operations.Catalogue") with
| :? System.Collections.IEnumerable as all ->
set [ for operation in all do
match operation with
| null -> ()
| operation -> yield string (read "Name" operation (operation.GetType())) ]
| _ -> failwith "Catalogue.All is not a list"

[<Fact>]
let ``every command offers an entry of the catalogue or says what it does`` () =
for command in OneShot.commands do
Assert.True(OneShot.offers.ContainsKey command <> OneShot.ownCommands.ContainsKey command, command)
for command in Seq.append (OneShot.offers |> Map.keys) (OneShot.ownCommands |> Map.keys) do
Assert.Contains(command, OneShot.commands)

[<Fact>]
let ``every entry of the catalogue is offered by a command or listed as not offered`` () =
let offered = OneShot.offers |> Map.values |> Seq.concat |> set
Assert.Empty(catalogue - offered - OneShot.notOffered)
Assert.Empty(Set.intersect offered OneShot.notOffered)
Assert.Empty(offered + OneShot.notOffered - catalogue)
Loading