TX Text Control .NET Server

MCP Document Server

Topic

Asked for a contract as PDF, a language model can write the wording, but it isn't a document engine that knows native styles, tables and merge fields. With the Model Context Protocol (MCP), the model calls tools instead: the MCP document server of TX Text Control creates, inspects, edits, converts and exports real documents, and the model decides which of its tools to call.

The server comes as TXTextControl.AI.McpServer, a package of the TX Text Control AI preview that was checked for this page in version 0.1.0-beta.3. Its APIs and tools may change before a stable release. How a model uses the tools in a loop, and how its edits are verified, is described in Agent Workflows.

What MCP Is

MCP is an open standard for how AI applications connect to servers that offer tools. A client asks a server for its tools, each with a JSON schema of its parameters, and calls them with structured arguments. The messages are JSON-RPC 2.0 requests over HTTP: tools/list returns the tools, and tools/call runs one. Any client that implements MCP can use any MCP server, without integration code for each pair.

The MCP document server isn't another language model. It's the component that knows document formats, paragraph styles, tables, fields and exports. Compared with a model that writes document files directly, tools give:

  • Consistent results, because TX Text Control performs every operation the same way.
  • Styles defined by the application, which the server applies to the documents it creates.
  • Fewer tokens, because a tool call is compact and the document internals never pass through the model.
  • Real formats, such as DOCX, PDF and TX.
  • Follow-up edits, because the document stays in a session on the server.

Two MCP Servers From Text Control

Text Control offers two different MCP servers:

Text Control MCP ServerMCP document server
PurposeGives AI coding assistants read-only access to blog articles, product information and API documentationCreates, edits, converts and exports documents with TX Text Control
Where it runsHosted by Text Control at https://www.textcontrol.com/mcp/v1In your own ASP.NET Core application, with the TXTextControl.AI.McpServer package
Changes dataNoYes

AI Coding Assistants describes the public endpoint and how assistants connect to it. The rest of this page is about the MCP document server.

The Package

TXTextControl.AI.McpServer is a hosting library, not a ready-to-run application: it adds the MCP endpoint and the document tools to an ASP.NET Core application that you build.

  • Included. Document workflows, native styles, tables, fields, templates, exports and a bounded pool of document workers.
  • Not included. Administration pages, a login, a language model or an inference engine. The server works independently of any AI provider, chat model or website.
  • Requirements. .NET 10 with ASP.NET Core 10 and appropriately licensed TX Text Control products. The package depends on TXTextControl.TextControl.Core.SDK, which comes from the Text Control Private NuGet Feed, on TXTextControl.Markdown.Core and on ModelContextProtocol.AspNetCore.
  • Platforms. The document engine runs on Windows and Linux, not natively on macOS. Linux needs the libraries and fonts that the TX Text Control SDK requires. An AI service that calls the server can run on a Mac.
  • Fonts. A build target copies the default fonts of the SDK into the build and publish output. Install additional fonts for your documents yourself; a font name in a style preset doesn't install that font.

The C# namespace of the package is TxTextControl.McpServer.

Host the Server

Add the prerelease package to an ASP.NET Core project that targets .NET 10:

dotnet add package TXTextControl.AI.McpServer --prerelease

The following Program.cs hosts the server for development on the local machine only:

using TxTextControl.McpServer;

// Must run first: the worker pool starts this application again as a document worker
if (await TextControlMcpWorker.RunIfRequestedAsync(args) is int exitCode)
{
    Environment.ExitCode = exitCode;
    return;
}

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("http://127.0.0.1:5000");
builder.Services.AddTextControlMcpServer(builder.Configuration);

var app = builder.Build();
app.MapTextControlMcp();
app.Run();

The code works as follows:

  • TextControlMcpWorker.RunIfRequestedAsync returns an exit code when the process was started as a document worker, and null for a normal start. It runs before the web host is built and before any console output, because the standard output of a worker carries its communication with the pool.
  • AddTextControlMcpServer registers the tools, sessions, style presets, operations, cleanup and the document engine. It takes the configuration root, which contains the McpServer, DocumentWorkerPool and DocumentAutomation sections.
  • MapTextControlMcp maps the MCP endpoint at /mcp, the extraction endpoint /mcp/knowledge/extract that RAG uses and the downloads of exported files at /exports. Map it on the application itself, not inside a route group with a prefix.

The endpoint uses the Streamable HTTP transport of MCP. Opening /mcp in a browser isn't a tool call; test the server with an MCP client. When you publish the application, deploy the runtimeconfig and deps files and the native files with it, and set DocumentWorkerPool:WorkerExecutablePath when the application runs behind IIS or a service wrapper.

The txtextcontrol-ai-samples repository contains two hosts. MinimalMcpHost runs only in the Development environment, rejects requests from other computers and listens at http://127.0.0.1:5082/mcp. McpServer adds administration pages for settings, capability packs and styles at http://127.0.0.1:5000/admin; Run the Samples describes how to start it.

Configure the Server

The settings come from appsettings.json or from environment variables with double underscores, such as DocumentWorkerPool__WorkerCount:

{
  "McpServer": { "BasePath": "documents", "SessionMaxAgeHours": 12 },
  "DocumentWorkerPool": { "Enabled": true, "WorkerCount": 2, "QueueCapacity": 128 },
  "DocumentAutomation": {
    "DefaultParagraphStyleName": "Body",
    "StyleRoles": {
      "Title": "Title", "Heading1": "Heading", "Heading2": "Heading2", "Body": "Body"
    },
    "StylePresets": [
      { "Name": "Title", "FontName": "Arial", "FontSize": 24, "Bold": true },
      { "Name": "Heading", "FontName": "Arial", "FontSize": 16, "Bold": true },
      { "Name": "Heading2", "FontName": "Arial", "FontSize": 13, "Bold": true },
      { "Name": "Body", "FontName": "Arial", "FontSize": 11 }
    ],
    "DefaultPageLayout": {
      "PageSize": "A4", "Orientation": "portrait", "Unit": "mm",
      "MarginTop": 20, "MarginBottom": 20, "MarginLeft": 20, "MarginRight": 20
    }
  }
}
SettingDefaultSets
McpServer:BasePathsamplesThe private, writable folder for documents and sessions; never a folder of static web content
McpServer:SessionMaxAgeHours12How long sessions and their files are kept before cleanup
DocumentWorkerPool:EnabledtrueWhether documents are processed in separate worker processes; false uses the engine in the process of the application
DocumentWorkerPool:WorkerCount2The number of document workers, from 1 to 16
DocumentWorkerPool:QueueCapacity128The number of waiting commands; a larger queue doesn't add processing capacity
DocumentWorkerPool:CommandTimeoutSeconds180How long a document command may take
DocumentAutomation:EnabledCapabilityPacksnullThe enabled groups of operations: BasicText, Tables, Fields, Media, Sections and HeaderFooter; null enables all, an empty list none
DocumentAutomation:EnabledOperationsnullThe enabled single operations, with the same rules
DocumentAutomation:StylePresetsemptyThe paragraph styles that new documents use
DocumentAutomation:StyleRolesTitle, Heading, Heading2, BodyWhich preset is used for the title, the first two heading levels and body text
DocumentAutomation:DefaultPageLayoutnullThe page size, orientation and margins of new documents

An operation is available only when both its pack and the operation itself are enabled. A disabled operation doesn't necessarily disappear from the tool list, so check the effective set with the get_document_automation_capabilities tool. Sessions are kept by one host and aren't a distributed store, which matters before you run several instances of the server.

The Document Tools

The README of the package documents the tools below. The tools/list response of the deployed server is authoritative, because tools change between versions.

GroupTools
Sessions and filesload_document, create_empty_document, convert_document, create_document_export, get_as_base64, get_session, delete_session
Creationcreate_document_from_markdown, create_document, list_document_recipes, create_document_from_recipe, get_authoring_guide, get_document_automation_capabilities
Readinginspect_document, inspect_document_section, classify_document, get_text, get_paragraphs, search_text, search_text_ranges, get_document_model, get_document_structure, get_document_styles, get_document_tables, get_document_fields, get_document_headers_footers, get_template_merge_fields, get_template_merge_blocks, get_template_form_fields, extract_knowledge_blocks
Editingedit_document, replace_document_section, format_text, format_text_occurrences, format_paragraph, apply_operations, apply_document_preset_styles
Styleslist_document_styles, set_document_style, apply_document_style, rename_document_style, delete_document_style, create_styles_from_paragraphs
Tablesinsert_table, format_table, add_table_rows
Fields and templatesinsert_merge_field, update_merge_field, create_merge_block, insert_form_field, update_form_field, clear_application_fields, clear_form_fields, merge_template
  • Sections, headers and footers are operations. apply_operations runs the operations of the enabled capability packs on a session, among them insert_text, insert_section_break, set_section_layout and set_header_footer. get_document_automation_capabilities returns the operation names a server has enabled.
  • Documents live in sessions. load_document loads a Base64-encoded document and create_document_from_markdown creates one; both return a sessionId that later calls reuse. The formats are TX, RTF, DOCX, PDF, HTML, Markdown and plain text, as far as the operation supports them. Conversions between formats aren't guaranteed to be lossless, and PDF import needs readable text because there is no OCR.
  • Fields are real fields. Merge fields are MERGEFIELD fields, not {{name}} text, and merge blocks are subtext parts named txmb_ followed by the block name, as Merge Fields and Merge Blocks describe. merge_template merges JSON data with MailMerge.MergeJsonData. By default, it doesn't append and removes empty fields, blocks, images, lines and trailing whitespace, which differs from the defaults of the MailMerge class.
  • Some tools aren't for models. get_authoring_guide returns large schemas and examples and should be requested deliberately. extract_knowledge_blocks is meant for indexing by the host, not for conversations.
  • classify_document assigns a category such as Legal, Finance or Human Resources without calling a model, as Document Analysis describes.

Call a Tool

A client sends a tools/call request with the name of the tool and its arguments. Most tools take one request object. The following request creates a document from Markdown:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "create_document_from_markdown",
    "arguments": {
      "request": { "markdown": "# Review checklist\n\n- Verify names\n- Approve changes" }
    }
  }
}

The result contains the sessionId of the new document. An edit passes it on and names the text to replace:

{
  "name": "edit_document",
  "arguments": {
    "request": {
      "sessionId": "RETURNED_SESSION_ID",
      "matchText": "Approve changes",
      "replacementText": "Obtain manager approval",
      "replaceAll": false
    }
  }
}

After a successful edit, create_document_export saves the document in a format and returns its metadata, including a downloadUri:

{
  "name": "create_document_export",
  "arguments": {
    "request": {
      "sessionId": "RETURNED_SESSION_ID", "format": "pdf", "fileName": "checklist.pdf"
    }
  }
}
  • Check the result, not the HTTP status. A call can succeed on the transport level and still return an error with isError and a code such as invalid_argument, not_found or license_error. Verify Before Reporting Success describes what a successful edit or export returns.
  • Download the returned URI. The export is stored in the session and expires with it. Download it from the exact downloadUri with an authorized client instead of building a URL or transferring the document as Base64.
  • End the session. delete_session removes the session and its files; otherwise, they're kept until SessionMaxAgeHours has passed.

Connect a .NET Application

TXTextControl.AI.Mcp is the matching client library for .NET 8. It connects to the server, discovers its tools and saves exported files without passing their content through a model:

using Microsoft.Extensions.AI;
using ModelContextProtocol.Client;
using TXTextControl.AI.Mcp;

var endpoint = new Uri("http://127.0.0.1:5000/mcp");

var transport = new HttpClientTransport(new HttpClientTransportOptions
{
    Name = "TX Text Control MCP",
    Endpoint = endpoint,
    TransportMode = HttpTransportMode.StreamableHttp
});

await using var mcp = await McpClient.CreateAsync(transport);

// Downloads must not follow redirects
using var downloads = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });

var documents = await McpDocumentToolSet.CreateAsync(mcp, downloads, new McpDocumentToolSetOptions
{
    OutputDirectory = Path.GetFullPath("private-exports"),
    ExportBaseUri = new Uri(endpoint.GetLeftPart(UriPartial.Authority))
});

Console.WriteLine($"Advertised tools: {documents.DiscoveredToolCount}");

The code works as follows:

  • The project references the prerelease TXTextControl.AI.Mcp package, which brings the ModelContextProtocol client with it.
  • HttpClientTransport and McpClient of the MCP SDK connect to the endpoint with the Streamable HTTP transport. Authentication of the connection is configured on this transport.
  • McpDocumentToolSet.CreateAsync discovers the tools. Tools contains them in the form a chat client passes to a model, and ServerInstructions holds the instructions the server sends for their use.
  • OutputDirectory is the private folder for exported files, and ExportBaseUri the only origin that downloads may come from. The download client has redirects disabled, as the package requires.
  • The model doesn't see get_as_base64 by default. The tool set adds a local save_document tool instead, so that the model receives only the metadata of a saved file.

The application can also export a document of an existing session directly, without a model:

DocumentExportResult exported = await documents.SaveDocumentAsync(
    sessionId, format: "pdf", fileName: "approved-document.pdf");

Console.WriteLine(exported.Path);

Path is a local path on the server, not a public download URL. Disposing the client doesn't delete the sessions on the server. Passing the tools to a model is described in Agent Workflows.

Connect AI Assistants

Desktop agents and AI coding assistants that support MCP, such as Claude Desktop or Codex, address a hosted document server by its URL. Where each assistant expects the entry is listed in Connect Your Assistant; for the document server, use its own URL instead of the public endpoint.

A server outside development requires credentials, as Secure the Server describes. The assistant sends them as an Authorization header from its configuration. Keep the key itself out of configuration files that you commit. Visual Studio Code, for example, asks for an input variable once and stores its value:

{
  "inputs": [
    {
      "type": "promptString",
      "id": "tx-document-server-key",
      "description": "API key of the MCP document server",
      "password": true
    }
  ],
  "servers": {
    "tx-document-server": {
      "type": "http",
      "url": "https://your-server.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${input:tx-document-server-key}"
      }
    }
  }
}

Claude Code expands environment variables in the headers of .mcp.json, so the key can come from a variable such as TX_MCP_API_KEY:

{
  "mcpServers": {
    "tx-document-server": {
      "type": "http",
      "url": "https://your-server.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${TX_MCP_API_KEY}"
      }
    }
  }
}

The assistant should first query the capabilities of the server and then use its tools instead of creating document files itself. Follow-up requests, such as a style change after a document was created, reuse the same sessionId. The blog article AI Natural Language Document Generation with MCP and TX Text Control .NET walks through prompts that create documents, templates with merge blocks and forms.

Secure the Server

The package doesn't authenticate users and doesn't check who owns a session; knowing a session ID isn't proof of access. Before the server is reachable by others, add TLS, authentication, checks of session ownership, quotas, request size limits and a retention policy. The endpoint can be protected in two ways.

Require an API Key

The McpServer sample protects the MCP endpoint and the downloads with a random key that every caller sends as a bearer token. Its middleware is in McpHostAccess.cs:

using System.Security.Cryptography;
using System.Text;

namespace TxTextControl.McpServer;

/// <summary>Optional bearer protection for the standalone host's document APIs.</summary>
public static class McpHostAccess
{
    public static void UseMcpHostAccess(this WebApplication app)
    {
        string key = app.Configuration["McpServiceAuthentication:ApiKey"] ?? "";
        bool required = app.Configuration.GetValue<bool>("McpServiceAuthentication:Required");
        if (!required && key.Length == 0) return;
        if (key.Length < 32) throw new InvalidOperationException("MCP service authentication requires a random API key of at least 32 characters.");
        byte[] expected = SHA256.HashData(Encoding.UTF8.GetBytes("Bearer " + key));
        app.Use(async (context, next) =>
        {
            if (context.Request.Path.StartsWithSegments("/mcp") || context.Request.Path.StartsWithSegments("/exports"))
            {
                string supplied = context.Request.Headers.Authorization.ToString();
                if (!context.Request.IsHttps || supplied.Length > 2048 ||
                    !CryptographicOperations.FixedTimeEquals(expected, SHA256.HashData(Encoding.UTF8.GetBytes(supplied))))
                {
                    context.Response.StatusCode = StatusCodes.Status401Unauthorized;
                    context.Response.Headers.WWWAuthenticate = "Bearer";
                    context.Response.Headers.CacheControl = "no-store";
                    return;
                }
            }
            await next();
        });
    }
}

Program.cs adds the middleware before it maps the server:

var app = builder.Build();
app.UseMcpHostAccess();
app.MapTextControlMcp();
app.Run();

The code works as follows:

  • The key comes from the setting McpServiceAuthentication:ApiKey. When a key is configured or McpServiceAuthentication:Required is true, the key must have at least 32 characters, or the application doesn't start. Without both, the middleware does nothing, which is why the sample refuses to start outside the Development environment unless Required is true.
  • Every request to /mcp or /exports must use HTTPS and send Authorization: Bearer followed by the key. The middleware compares hashes of the expected and the supplied header in constant time and answers any other request with status 401 and a WWW-Authenticate: Bearer header.
  • The key is a secret. Generate it, for example with openssl rand -hex 32, and pass it from a secret store or an environment variable such as McpServiceAuthentication__ApiKey, never from a committed appsettings.json.

An AI host that calls the protected server sends the key from its LocalAI:McpCredentials settings, with Endpoint and Token for each entry. The administrator login of the sample protects only its administration pages, not the MCP endpoint.

Use the Authentication of Your Application

When the users or services that call the server sign in through the authentication of your application, protect the route group that MapTextControlMcp returns with an authorization policy:

// Register the authentication of your application, for example with your identity provider
builder.Services.AddAuthorizationBuilder()
    .AddPolicy("DocumentAccess", policy => policy.RequireAuthenticatedUser());

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapTextControlMcp().RequireAuthorization("DocumentAccess");
app.Run();

AddPolicy registers the policy DocumentAccess, which here only admits authenticated users; a policy can also require roles or claims. RequireAuthorization applies the policy to the endpoints of the group. Policy-based authorization in ASP.NET Core describes policies in detail.

Beyond Authentication

  • Session ownership. Authentication alone doesn't keep one user from the sessions of another. Your application has to enforce which user may access which session and which exported file.
  • Error messages. Tool errors can contain exception details. Review what your logs and responses reveal before users see them.
  • Storage. Keep the document folder out of static files, and delete sessions deliberately or rely on the configured cleanup.
  • Shared use. The sample hosts are meant for private single-user setups. For shared use, add an identity provider, ownership per user or tenant, rate limits, network restrictions and redacted logging.

Next Steps

  • Agent Workflows: let a model plan document actions with the tools.
  • Text Control AI: run the web sample that uses the document server.
  • RAG: extract reference documents through the server for retrieval.
  • AI Coding Assistants: connect assistants to the public Text Control MCP Server.
  • Examples: the MCP hosts and the MCP document server repository.