Skip to content

Commit 5c4ef8d

Browse files
peopleworksclaude
andcommitted
Add SignsOfAI.Mcp: MCP server exposing the engine as tools
A Model Context Protocol server (official ModelContextProtocol 1.4.1 SDK, stdio transport) that references SignsOfAI.Core and exposes it to Claude Desktop and any MCP client. Because the engine is pure .NET, the server just surfaces it as tools: - analyze_ai_writing (offline) score/verdict/findings/stats - check_originality (offline) pairwise overlap + shared passages - search_catalog (offline) filter the sign catalog - extract_distinctive_phrases (offline) phrases + web-search links - measure_predictability (server) perplexity via the hosted API - check_paraphrase (server) EmbeddingGemma paraphrase matches The two server tools call the API (endpoint via SIGNSOFAI_API_ENDPOINT, defaults to the hosted instance) and disclose that they send text off-device — same opt-in privacy stance as the web app. All logging goes to stderr so it never corrupts the stdout JSON-RPC stream. Packs as a global tool (signsofai-mcp). Ships its own README with the Claude Desktop config; main README gains a section and architecture entry. Verified end-to-end over a raw JSON-RPC handshake (initialize / tools/list / tools/call) — all 6 tools, including the two live server calls. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a33f97f commit 5c4ef8d

8 files changed

Lines changed: 538 additions & 0 deletions

File tree

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,35 @@ labelled corpus the two overlap badly (memorized human text scores *predictable*
7474
predictability honestly as one signal among many, calibrated per language. Opt-in; runs on the PeopleWorks
7575
server. The model lazily loads and idle-unloads to keep the server light.
7676

77+
## 4. Use it from other apps — MCP server
78+
79+
Everything above is also available to **Claude Desktop and any [MCP](https://modelcontextprotocol.io)
80+
client** through `SignsOfAI.Mcp`, a Model Context Protocol server (built on the official
81+
[`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) SDK, stdio transport). Because
82+
the engine lives in `SignsOfAI.Core` — pure .NET, no browser — the server just exposes it as tools:
83+
84+
| Tool | What it does | Where it runs |
85+
|------|--------------|---------------|
86+
| `analyze_ai_writing` | score + verdict + findings (with fixes) + statistics | 🔒 on-device |
87+
| `check_originality` | overlap % and shared passages across 2+ documents | 🔒 on-device |
88+
| `search_catalog` | search the catalog of AI-writing signs (EN/ES) | 🔒 on-device |
89+
| `extract_distinctive_phrases` | distinctive phrases + ready-made web-search links | 🔒 on-device |
90+
| `measure_predictability` | perplexity via the optional server | 🌐 server (**opt-in**) |
91+
| `check_paraphrase` | reworded/translated matches via EmbeddingGemma | 🌐 server (**opt-in**) |
92+
93+
The first four run entirely on the machine; the last two disclose that they send text to the server
94+
(endpoint via the `SIGNSOFAI_API_ENDPOINT` environment variable). Point Claude Desktop at it:
95+
96+
```jsonc
97+
// %APPDATA%\Claude\claude_desktop_config.json
98+
{ "mcpServers": { "signs-of-ai": {
99+
"command": "dotnet",
100+
"args": ["…/src/SignsOfAI.Mcp/bin/Release/net10.0/SignsOfAI.Mcp.dll"]
101+
}}}
102+
```
103+
104+
It also packs as a global tool (`signsofai-mcp`). See `src/SignsOfAI.Mcp/README.md` for details.
105+
77106
---
78107

79108
## Architecture
@@ -90,6 +119,7 @@ SignsOfAI.slnx
90119
│ │ └─ AiWritingAnalyzer # Public facade: Analyze(text, language)
91120
│ ├─ SignsOfAI.Web # Blazor WebAssembly front end (Analyze, Originality, Catalog)
92121
│ ├─ SignsOfAI.Cli # `dotnet tool` for CI pipelines
122+
│ ├─ SignsOfAI.Mcp # MCP server (stdio): the engine as tools for Claude Desktop / any client
93123
│ └─ SignsOfAI.Perplexity.Api # Optional ASP.NET Core server: predictability + embeddings
94124
│ ├─ Engine/ # OnnxPerplexityEngine, OnnxEmbeddingEngine (lazy-load + idle-unload)
95125
│ └─ Config/ # model profiles, calibration, embedding + web-search options

src/SignsOfAI.Mcp/Program.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Microsoft.Extensions.Hosting;
3+
using Microsoft.Extensions.Logging;
4+
5+
var builder = Host.CreateApplicationBuilder(args);
6+
7+
// The MCP protocol owns stdout (JSON-RPC frames). Every log line MUST go to stderr,
8+
// or it corrupts the stream the client is parsing.
9+
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);
10+
11+
builder.Services
12+
.AddMcpServer()
13+
.WithStdioServerTransport()
14+
.WithToolsFromAssembly();
15+
16+
await builder.Build().RunAsync();

src/SignsOfAI.Mcp/README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# SignsOfAI.Mcp — Model Context Protocol server
2+
3+
Exposes the **Signs of AI Writing** engine as [MCP](https://modelcontextprotocol.io) tools, so Claude
4+
Desktop (or any MCP client) can analyze text, compare documents for copying, browse the sign catalog, and —
5+
optionally — measure perplexity and find cross-language paraphrases.
6+
7+
Built on the official [`ModelContextProtocol`](https://www.nuget.org/packages/ModelContextProtocol) SDK
8+
(stdio transport) and a project reference to `SignsOfAI.Core`.
9+
10+
## Tools
11+
12+
| Tool | What it does | Runs |
13+
| --- | --- | --- |
14+
| `analyze_ai_writing` | Score 0–100 + verdict + findings (overused vocabulary, rhetorical crutches, syntactic tells, burstiness), each with a fix | 🖥️ offline |
15+
| `check_originality` | Compares 2+ documents against **each other** → overlap % + the actual shared passages | 🖥️ offline |
16+
| `search_catalog` | Searches the catalog of AI-writing signs (EN/ES), filter by keyword / language / category | 🖥️ offline |
17+
| `extract_distinctive_phrases` | Distinctive phrases + ready-made exact-phrase web-search links | 🖥️ offline |
18+
| `measure_predictability` | Perplexity — how predictable/generic a model finds the phrasing | ☁️ server |
19+
| `check_paraphrase` | Reworded/translated copies via sentence embeddings (EmbeddingGemma) | ☁️ server |
20+
21+
The first four run **entirely on the machine** — the text never leaves it. The last two **send the text**
22+
to the SignsOfAI server (their descriptions disclose this); see [Server tools](#server-tools-optional).
23+
24+
## Run it
25+
26+
```bash
27+
# From the repo root — for development:
28+
dotnet run --project src/SignsOfAI.Mcp
29+
30+
# …or build once and point Claude Desktop at the DLL (see below):
31+
dotnet build src/SignsOfAI.Mcp -c Release
32+
```
33+
34+
To install as a global command (`signsofai-mcp`):
35+
36+
```bash
37+
dotnet pack src/SignsOfAI.Mcp -c Release
38+
dotnet tool install --global --add-source src/SignsOfAI.Mcp/bin/Release SignsOfAI.Mcp
39+
```
40+
41+
## Claude Desktop
42+
43+
Add one of these to `claude_desktop_config.json`
44+
(Windows: `%APPDATA%\Claude\claude_desktop_config.json`), then restart Claude Desktop.
45+
46+
Using the built DLL:
47+
48+
```json
49+
{
50+
"mcpServers": {
51+
"signs-of-ai": {
52+
"command": "dotnet",
53+
"args": ["C:\\Proyecto\\AI\\SignsofAI\\src\\SignsOfAI.Mcp\\bin\\Release\\net10.0\\SignsOfAI.Mcp.dll"]
54+
}
55+
}
56+
}
57+
```
58+
59+
Or, if installed as a global tool:
60+
61+
```json
62+
{
63+
"mcpServers": {
64+
"signs-of-ai": { "command": "signsofai-mcp" }
65+
}
66+
}
67+
```
68+
69+
## Server tools (optional)
70+
71+
`measure_predictability` and `check_paraphrase` call the SignsOfAI API. By default they use the
72+
PeopleWorks-hosted endpoint; override it with the `SIGNSOFAI_API_ENDPOINT` environment variable:
73+
74+
```json
75+
{
76+
"mcpServers": {
77+
"signs-of-ai": {
78+
"command": "signsofai-mcp",
79+
"env": { "SIGNSOFAI_API_ENDPOINT": "https://your-server" }
80+
}
81+
}
82+
}
83+
```
84+
85+
Unlike the offline tools, these two **send the text off the device** to run the model — the same disclosed,
86+
opt-in behavior as the web app's Predictability and Paraphrase features. `check_paraphrase` also needs the
87+
embedding feature enabled on the server.
88+
89+
## Protocol note
90+
91+
MCP speaks JSON-RPC over **stdout**, so this server logs everything to **stderr** — never write to stdout
92+
from a tool.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<LangVersion>latest</LangVersion>
9+
10+
<!-- Distributable as a global .NET tool: `dotnet tool install -g SignsOfAI.Mcp` gives the
11+
`signsofai-mcp` command, which Claude Desktop (or any MCP client) launches over stdio. -->
12+
<PackAsTool>true</PackAsTool>
13+
<ToolCommandName>signsofai-mcp</ToolCommandName>
14+
<PackageId>SignsOfAI.Mcp</PackageId>
15+
<Version>0.1.0</Version>
16+
<Authors>Pedro Hernández (PeopleWorks)</Authors>
17+
<Company>PeopleWorks</Company>
18+
<Description>Model Context Protocol (MCP) server for Signs of AI Writing. Exposes the engine as tools — analyze text for AI tells, check originality between documents, search the sign catalog, extract distinctive phrases, and (optionally, via the hosted API) measure perplexity and find paraphrases — to Claude Desktop and any MCP client.</Description>
19+
<PackageTags>mcp;ai;writing;detector;claude;modelcontextprotocol;stylometry</PackageTags>
20+
<PackageProjectUrl>https://github.com/peopleworks/SignsofAI</PackageProjectUrl>
21+
<RepositoryUrl>https://github.com/peopleworks/SignsofAI</RepositoryUrl>
22+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
23+
<PackageReadmeFile>README.md</PackageReadmeFile>
24+
</PropertyGroup>
25+
26+
<ItemGroup>
27+
<None Include="README.md" Pack="true" PackagePath="\" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
32+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.7" />
33+
</ItemGroup>
34+
35+
<ItemGroup>
36+
<ProjectReference Include="..\SignsOfAI.Core\SignsOfAI.Core.csproj" />
37+
</ItemGroup>
38+
39+
</Project>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System.ComponentModel;
2+
using ModelContextProtocol.Server;
3+
using SignsOfAI.Core.Rules;
4+
5+
namespace SignsOfAI.Mcp.Tools;
6+
7+
/// <summary>Browse/search the catalog of AI-writing signs the analyzer knows about. Offline reference.</summary>
8+
[McpServerToolType]
9+
public static class CatalogTools
10+
{
11+
// The catalog is built once from the embedded rule-packs.
12+
private static readonly IReadOnlyList<SignInfo> All = RuleCatalog.All();
13+
14+
[McpServerTool(Name = "search_catalog", ReadOnly = true),
15+
Description("""
16+
Searches the catalog of AI-writing "signs" the analyzer looks for (English & Spanish) — each with why it
17+
reads as AI and how to fix it. Useful as a reference / study aid, or to explain a finding in depth. Filter
18+
by keyword, language ("en"/"es"), and/or category (Lexical, Rhetorical, Syntactic, Statistical). Offline.
19+
""")]
20+
public static CatalogResult SearchCatalog(
21+
[Description("Keyword filter (matches title, examples, message, suggestion). Empty = all.")] string query = "",
22+
[Description("Language filter: \"en\", \"es\", or empty for both.")] string language = "",
23+
[Description("Category filter: Lexical, Rhetorical, Syntactic, Statistical, or empty.")] string category = "")
24+
{
25+
IEnumerable<SignInfo> items = All;
26+
27+
if (!string.IsNullOrWhiteSpace(language))
28+
items = items.Where(s => s.Language.Equals(language.Trim(), StringComparison.OrdinalIgnoreCase));
29+
30+
if (!string.IsNullOrWhiteSpace(category))
31+
items = items.Where(s => s.Category.ToString().Equals(category.Trim(), StringComparison.OrdinalIgnoreCase));
32+
33+
if (!string.IsNullOrWhiteSpace(query))
34+
{
35+
var q = query.Trim();
36+
items = items.Where(s => s.SearchText.Contains(q, StringComparison.OrdinalIgnoreCase));
37+
}
38+
39+
var entries = items
40+
.Take(60)
41+
.Select(s => new CatalogEntry(
42+
s.Id, s.Language, s.Category.ToString(), s.Severity.ToString(), s.Title, s.Examples, s.Message, s.Suggestion, s.Evidence))
43+
.ToList();
44+
45+
return new CatalogResult(entries.Count, entries);
46+
}
47+
}
48+
49+
public sealed record CatalogResult(int Count, IReadOnlyList<CatalogEntry> Entries);
50+
51+
public sealed record CatalogEntry(
52+
string Id,
53+
string Language,
54+
string Category,
55+
string Severity,
56+
string Title,
57+
string[] Examples,
58+
string Message,
59+
string Suggestion,
60+
string? Evidence);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using System.ComponentModel;
2+
using ModelContextProtocol.Server;
3+
using SignsOfAI.Core.Originality;
4+
5+
namespace SignsOfAI.Mcp.Tools;
6+
7+
/// <summary>Copy/originality tools: compare documents to each other, and extract phrases worth a web check. Offline.</summary>
8+
[McpServerToolType]
9+
public static class OriginalityTools
10+
{
11+
private static readonly OriginalityChecker Checker = new();
12+
13+
[McpServerTool(Name = "check_originality", ReadOnly = true),
14+
Description("""
15+
Compares two or more documents AGAINST EACH OTHER to find copied passages — a cohort of student
16+
submissions, a draft against its sources. For each document pair it returns the overlap percentage
17+
(case- and accent-insensitive) and the actual shared passages as evidence. This is NOT a whole-internet
18+
index like Turnitin; it only compares the documents you provide, fully offline. It surfaces evidence and
19+
lets a human judge — it never accuses.
20+
""")]
21+
public static OriginalityResult CheckOriginality(
22+
[Description("Two or more documents to compare. Each has an optional title and its text.")] DocumentInput[] documents)
23+
{
24+
if (documents is null || documents.Length < 2)
25+
throw new ArgumentException("Provide at least two documents to compare.");
26+
27+
var inputs = documents
28+
.Select((d, i) => new OriginalityInput(
29+
(i + 1).ToString(),
30+
string.IsNullOrWhiteSpace(d.Title) ? $"Document {i + 1}" : d.Title!,
31+
d.Text ?? string.Empty))
32+
.ToList();
33+
34+
var report = Checker.Check(inputs);
35+
36+
var pairs = report.Pairs
37+
.Where(p => p.Overlap > 0)
38+
.Select(p => new PairOverlap(
39+
p.TitleA, p.TitleB,
40+
Math.Round(p.Overlap * 100, 1),
41+
Math.Round(p.Jaccard * 100, 1),
42+
p.SharedWords,
43+
p.LongestRunWords,
44+
p.Passages
45+
.OrderByDescending(x => x.WordLength)
46+
.Take(15)
47+
.Select(x => Slice(inputs[p.IndexA].Text, x.SpanA.Start, x.SpanA.Length))
48+
.Where(s => s.Length > 0)
49+
.ToList()))
50+
.ToList();
51+
52+
return new OriginalityResult(inputs.Count, pairs.Count, pairs);
53+
}
54+
55+
[McpServerTool(Name = "extract_distinctive_phrases", ReadOnly = true),
56+
Description("""
57+
Extracts the most DISTINCTIVE phrases from a document — long, specific, proper-noun- or number-bearing
58+
wording most worth checking on the web — and returns each with ready-made exact-phrase search links
59+
(Google, Bing, DuckDuckGo). It does NOT search the web itself; it hands you the searches to run. Offline.
60+
""")]
61+
public static PhrasesResult ExtractDistinctivePhrases(
62+
[Description("The document text.")] string text,
63+
[Description("Maximum phrases to return. Default 8.")] int maxPhrases = 8)
64+
{
65+
var phrases = DistinctivePhraseExtractor.Extract(text ?? string.Empty, Math.Clamp(maxPhrases, 1, 25), 10);
66+
return new PhrasesResult(phrases.Select(p => new PhraseHit(p.Phrase, MakeSearchLinks(p.Phrase))).ToList());
67+
}
68+
69+
private static string Slice(string s, int start, int len) =>
70+
start >= 0 && len >= 0 && start + len <= s.Length ? s.Substring(start, len) : string.Empty;
71+
72+
private static SearchLinks MakeSearchLinks(string phrase)
73+
{
74+
var q = Uri.EscapeDataString("\"" + phrase + "\"");
75+
return new SearchLinks(
76+
$"https://www.google.com/search?q={q}",
77+
$"https://www.bing.com/search?q={q}",
78+
$"https://duckduckgo.com/?q={q}");
79+
}
80+
}
81+
82+
public sealed record DocumentInput(string? Title, string Text);
83+
84+
public sealed record OriginalityResult(int DocumentCount, int OverlappingPairs, IReadOnlyList<PairOverlap> Pairs);
85+
86+
public sealed record PairOverlap(
87+
string DocumentA,
88+
string DocumentB,
89+
double OverlapPercent,
90+
double JaccardPercent,
91+
int SharedWords,
92+
int LongestSharedRunWords,
93+
IReadOnlyList<string> SharedPassages);
94+
95+
public sealed record PhrasesResult(IReadOnlyList<PhraseHit> Phrases);
96+
97+
public sealed record PhraseHit(string Phrase, SearchLinks Search);
98+
99+
public sealed record SearchLinks(string Google, string Bing, string DuckDuckGo);

0 commit comments

Comments
 (0)