cd /news/developer-tools/i-m-building-an-ai-assistant-in-c-i-… · home topics developer-tools article
[ARTICLE · art-130233] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

I'm Building an AI Assistant in C#. I Don't Want to Rewrite Every Tool.

A developer has released WeavePort, an open-source plugin runtime for .NET that lets C# applications call capabilities written in Python or TypeScript without rewriting them. The project's Decision Room sample demonstrates equivalent C# and Python strategies producing identical results, with the host retaining control over workflow, data access, and validation. The developer says the goal is to let teams contribute code in their own language without becoming C# developers.

by read7 min views2 publishedSep 15, 2026

I'm building an AI assistant in C#. Some of the code I want it to use already exists in Python or TypeScript.

I could rewrite it. But then every improvement to the original becomes something I have to port, test and maintain. Choosing C# for the application shouldn't mean choosing C# for every capability it will ever have.

Calling a script is easy enough. Deciding what happens when it hangs, which data it can ask the application for, and how to replace it without disturbing the rest of the system takes more thought.

That's why I built WeavePort, an open-source plugin runtime for .NET. I also want to keep other applications small by moving selected functionality and strategies into replaceable plugins. A department that works in Python should be able to contribute useful code without first becoming a C# team.

Suppose I want the assistant to help compare improvement proposals. One capability could score them using business priorities and risk information. The assistant could explain the results; the application would decide when the calculation is allowed to run and what data it can use. That is an intended use, not a finished assistant integration.

The scoring strategy might use a Python library or rules maintained by another team. Its inputs, outputs and meaning need to stay stable when I change the implementation.

This is the idea behind the repository's Decision Room example. Two participants evaluate three proposals using configurable priorities. Equivalent C# and Python strategies produce the same results. Change the language and the winner stays the same; change the strategy and a different proposal can win.

With the default configuration, “Automate support” wins. Increase the strategy's risk penalty and “Improve documentation” wins instead. The host still runs the same workflow.

Decision Room deliberately leaves out the model. It exercises the capability I would put underneath an assistant: send proposals to a strategy, get evaluations back, and keep control of the workflow in the application. The same capability can serve a backend with no AI at all.

Here is the call in the C# host, after it has bound the selected strategy to a worker:

await using var client = new LocalPluginClient(strategy);
Evaluation evaluation = await client.CallAsync<DecisionRequest, Evaluation>(
    "strategy.evaluate",
    new DecisionRequest(participant.Id, state),
    token);

The request contains the participant and the room state, including the proposals. With Python selected, it reaches this function in the Python plugin:

@app.function("strategy.evaluate")
async def evaluate(request, context):
    weights = context.configuration["priorities"]
    knowledge = await context.call_host("knowledge.read", {})
    scores = {
        p["id"]: p["benefit"] * weights["benefit"] - p["cost"] * weights["cost"]
        - knowledge["risks"][p["id"]] * weights["risk"] * RISK_MULTIPLIER
        for p in request["snapshot"]["definition"]["proposals"]
    }
    return dict(participant=request["participant"], source=knowledge["source"],
                risks=knowledge["risks"], scores=scores, pluginVersion=VERSION)

These are excerpts from the runnable sample. Its setup supplies the binding, domain types, application registration and release constants. The SDK carries the request and result across the process boundary; C# receives an Evaluation. The host then validates that evaluation before committing it to the room's state.

The calculation is intentionally small enough to inspect. Version 1 uses a risk multiplier of one; version 2 uses ten. Selecting an equivalent C# strategy keeps the result. Selecting the stronger risk penalty changes it.

The callback is the more interesting part. Its empty payload is deliberate: the host has already bound the plugin to a tenant and knowledge profile. The application uses that context to select the risk data. The strategy doesn't choose another customer's ID in its request.

WeavePort checks whether knowledge.read is granted. The application decides which records that grant permits. This lets the Python code use application-owned knowledge without carrying its own database integration.

There is still integration work. An existing library needs a wrapper, its dependencies need packaging, and its result needs a meaning the caller can rely on. A typed result won't tell you whether two ranking implementations use the same scale. Those business-contract tests remain the application's responsibility.

Python plugins run in Python processes. TypeScript plugins run through Node. The .NET host manages those workers; it doesn't execute Python inside a C# worker.

For local stdio execution, requests and responses travel as framed JSON over stdin and stdout. There's no listening port to allocate for each plugin. The host handles startup, deadlines, admission limits and cleanup, and workers can be retained where the configured lifecycle allows it.

Suppose a scoring call stops responding. The application needs a failed call it can handle, and the runtime needs to clean up the worker. It must also avoid silently repeating an operation that may already have changed something. A timeout cannot tell us whether an external side effect happened.

I want to make those lifecycle decisions once and apply them to every strategy I add.

There is a security boundary worth being precise about: these are trusted local plugins, not a sandbox for arbitrary uploaded code. Callback permissions restrict access through the host API. A Python process still runs with its OS user's access to files and the network. Contributions from another department therefore still need review and an approved deployment.

Sometimes the wrapper already exists: a tool comes with an MCP server. In that case, I want to use its existing interface.

WeavePort can launch a trusted local MCP server, discover its tools and invoke them under the same host lifecycle and capacity budgets. One server can expose several functions. No language model has to be involved in calling them.

The separate MCP sample uses text normalization as a minimal API demonstration. After binding its server, the call looks like this:

using System.Text.Json;
using WeavePort.Hosting;

var result = await session.InvokeAsync(
    McpMethods.CallTool,
    JsonSerializer.SerializeToElement(new
    {
        name = "normalize",
        arguments = new { text = " hello   world " }
    }));

The runnable example uses Hosting 0.3.1 and includes binding, discovery with McpMethods.ListTools, and checks for both host failures and tool errors.

I still want the native plugin contract for a strategy like Decision Room, where callbacks into application-owned knowledge are part of the design. MCP is useful for consuming existing tools. It doesn't automatically replace those native callbacks or result streams, and installing a native plugin doesn't automatically expose it to an AI assistant. The application chooses what the assistant may invoke.

The current MCP integration covers local stdio tool discovery and calls. Remote HTTP servers and interactive requests back into the host need a different integration. The MCP guide describes the supported revisions and limits.

For a small extension written entirely in C#, I would start with an ordinary interface. For functionality that needs independent deployment, several consuming applications or its own scaling policy, I would consider a service.

A managed plugin fits the space I'm interested in: the application owns the workflow and wants to reuse or replace a substantial piece of behavior, possibly written in another language.

Crossing a process boundary still costs something. In a local sequential echo comparison on macOS arm64 with Node 24.21.0 and the released 0.3.0 Hosting assembly, small calls over the native WeavePort protocol averaged about 0.057 ms; MCP calls averaged 0.086–0.101 ms. First calls including startup were about 59 ms and 111 ms respectively. These figures describe that test, not the latency of your tool. The benchmark report includes larger payloads, tail latency and the measurement setup.

I'd pass a useful unit of work across that boundary, such as evaluating a set of proposals. I wouldn't split every arithmetic operation into a separate call.

The easiest way to judge the design is to run Decision Room and swap a participant's language:

git clone --branch v0.3.1 --depth 1 https://github.com/yesbert/WeavePort.git
cd WeavePort
./scripts/decision-room.sh --build --verify

You'll need the .NET SDK selected by global.json and Python 3.11+ with venv and pip. The script builds local packages and prepares a private Python environment. This walkthrough is validated on macOS arm64; the platform notes distinguish that from Windows and Linux source checks.

The walkthrough shows how to change the language, select a different strategy, deny a callback and recover a run. Start by changing only the language, then change the strategy. You can check for yourself which behavior belongs to the application and which belongs to the plugin.

WeavePort is MIT-licensed and still pre-1.0. Its .NET packages are on NuGet; the Python and TypeScript author SDKs are currently built from the repository.

What I want for my assistant is simple to describe: when I find a useful capability, most of the work should be understanding its inputs, outputs and permissions. I want the question to be “Can I use this?” long before it becomes “Do I have to rewrite this?”

Where have you drawn that line in your own backend: a library, a plugin or a separate service?

Disclosure: I maintain WeavePort. AI generated the article text and cover from my design goals, source code and project documentation. I reviewed the draft and directed its revisions.

── more in #developer-tools 4 stories · sorted by recency
── more on @weaveport 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-m-building-an-ai-a…] indexed:0 read:7min 2026-09-15 ·