Skip to content
Open
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
15 changes: 15 additions & 0 deletions DocxMcp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxMcp.Cli", "src\DocxMcp.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxMcp.Ui", "src\DocxMcp.Ui\DocxMcp.Ui.csproj", "{D4E5F6A7-B8C9-0123-4567-890ABCDEF012}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DocxMcp.WordAddin", "src\DocxMcp.WordAddin\DocxMcp.WordAddin.csproj", "{E5F6A7B8-C901-2345-6789-0ABCDEF01234}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -71,12 +73,25 @@ Global
{D4E5F6A7-B8C9-0123-4567-890ABCDEF012}.Release|x64.Build.0 = Release|Any CPU
{D4E5F6A7-B8C9-0123-4567-890ABCDEF012}.Release|x86.ActiveCfg = Release|Any CPU
{D4E5F6A7-B8C9-0123-4567-890ABCDEF012}.Release|x86.Build.0 = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|x64.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|x64.Build.0 = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|x86.ActiveCfg = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Debug|x86.Build.0 = Debug|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|Any CPU.Build.0 = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|x64.ActiveCfg = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|x64.Build.0 = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|x86.ActiveCfg = Release|Any CPU
{E5F6A7B8-C901-2345-6789-0ABCDEF01234}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3B0B53E5-AF70-4F88-B383-04849B4CBCE0} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{D4E5F6A7-B8C9-0123-4567-890ABCDEF012} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{E5F6A7B8-C901-2345-6789-0ABCDEF01234} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
46 changes: 46 additions & 0 deletions scripts/run-docker-windows.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
@echo off
REM run-docker-windows.bat
REM Simple launcher for docx-mcp on Windows with OneDrive

setlocal

REM Configuration - modify these paths as needed
set DOCUMENTS_PATH=%USERPROFILE%\OneDrive\Documents
set IMAGE=valdo404/docx-mcp:latest

echo.
echo 📎 Doccy - Docker Launcher
echo.

REM Check if Docker is available
docker info >nul 2>&1
if errorlevel 1 (
echo ERROR: Docker is not running. Please start Docker Desktop.
pause
exit /b 1
)

REM Check if path exists
if not exist "%DOCUMENTS_PATH%" (
echo ERROR: Documents path not found: %DOCUMENTS_PATH%
echo Edit this script to set the correct DOCUMENTS_PATH
pause
exit /b 1
)

echo Documents: %DOCUMENTS_PATH%
echo.

REM Pull if first argument is "pull"
if "%1"=="pull" (
echo Pulling latest image...
docker pull %IMAGE%
echo.
)

REM Run container
echo Starting container...
docker run -i --rm ^
-v "%DOCUMENTS_PATH%:/data" ^
-v "docx-sessions:/home/app/.docx-mcp/sessions" ^
%IMAGE%
73 changes: 73 additions & 0 deletions scripts/run-docker-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# run-docker-windows.ps1
# Script to run docx-mcp Docker container on Windows with OneDrive support

param(
[string]$DocumentsPath = "$env:USERPROFILE\OneDrive\Documents",
[string]$Image = "valdo404/docx-mcp:latest",
[switch]$Pull,
[switch]$Interactive
)

Write-Host "📎 Doccy - Docker Launcher for Windows" -ForegroundColor Cyan
Write-Host ""

# Check if Docker is running
try {
docker info | Out-Null
} catch {
Write-Host "Error: Docker is not running. Please start Docker Desktop." -ForegroundColor Red
exit 1
}

# Pull latest image if requested
if ($Pull) {
Write-Host "Pulling latest image..." -ForegroundColor Yellow
docker pull $Image
Write-Host ""
}

# Validate documents path
if (-not (Test-Path $DocumentsPath)) {
Write-Host "Error: Documents path not found: $DocumentsPath" -ForegroundColor Red
Write-Host "Use -DocumentsPath to specify a different path" -ForegroundColor Yellow
exit 1
}

# Check for OneDrive "Files On-Demand" warning
if ($DocumentsPath -like "*OneDrive*") {
Write-Host "OneDrive detected. Make sure 'Files On-Demand' is disabled for this folder:" -ForegroundColor Yellow
Write-Host " Right-click folder -> 'Always keep on this device'" -ForegroundColor Gray
Write-Host ""
}

# Convert Windows path to Docker-compatible format
$DockerPath = $DocumentsPath -replace '\\', '/' -replace '^([A-Za-z]):', '/$1'
$DockerPath = $DockerPath.ToLower()

Write-Host "Documents path: $DocumentsPath" -ForegroundColor Gray
Write-Host "Docker mount: $DockerPath -> /data" -ForegroundColor Gray
Write-Host ""

# Build docker run command
$dockerArgs = @(
"run"
"--rm"
"-v", "${DocumentsPath}:/data"
"-v", "docx-sessions:/home/app/.docx-mcp/sessions"
)

if ($Interactive) {
$dockerArgs += "-it"
Write-Host "Running in interactive mode..." -ForegroundColor Green
} else {
$dockerArgs += "-i"
Write-Host "Running in MCP mode (stdin/stdout)..." -ForegroundColor Green
}

$dockerArgs += $Image

Write-Host "Command: docker $($dockerArgs -join ' ')" -ForegroundColor Gray
Write-Host ""

# Run container
& docker @dockerArgs
19 changes: 19 additions & 0 deletions src/DocxMcp.WordAddin/DocxMcp.WordAddin.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>doccy</AssemblyName>
<RootNamespace>DocxMcp.WordAddin</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\DocxMcp\DocxMcp.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Anthropic.SDK" Version="5.9.0" />
</ItemGroup>

</Project>
182 changes: 182 additions & 0 deletions src/DocxMcp.WordAddin/Models/LlmModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System.Text.Json.Serialization;

namespace DocxMcp.WordAddin.Models;

/// <summary>
/// Request to start an LLM editing session.
/// </summary>
public sealed record LlmEditRequest
{
/// <summary>
/// The document session ID (from docx-mcp).
/// </summary>
public required string SessionId { get; init; }

/// <summary>
/// User's instruction for the LLM (e.g., "rewrite this paragraph more concisely").
/// </summary>
public required string Instruction { get; init; }

/// <summary>
/// Current document content (from Word via Office.js).
/// </summary>
public required DocumentContent Document { get; init; }

/// <summary>
/// Optional: specific path to focus on (e.g., "/body/paragraph[2]").
/// </summary>
public string? FocusPath { get; init; }

/// <summary>
/// Optional: user's recent logical changes (for context).
/// </summary>
public List<LogicalChange>? RecentChanges { get; init; }
}

/// <summary>
/// Document content sent from Word.
/// </summary>
public sealed class DocumentContent
{
/// <summary>
/// Full text content of the document.
/// </summary>
public required string Text { get; init; }

/// <summary>
/// Structured elements (paragraphs, headings, etc.) with IDs.
/// </summary>
public List<DocumentElement>? Elements { get; init; }

/// <summary>
/// Current selection in Word (if any).
/// </summary>
public SelectionInfo? Selection { get; init; }
}

/// <summary>
/// A document element with stable ID.
/// </summary>
public sealed class DocumentElement
{
public required string Id { get; init; }
public required string Type { get; init; }
public required string Text { get; init; }
public int Index { get; init; }
public string? Style { get; init; }
}

/// <summary>
/// Information about the current selection in Word.
/// </summary>
public sealed class SelectionInfo
{
public required string Text { get; init; }
public int StartIndex { get; init; }
public int EndIndex { get; init; }
public string? ContainingElementId { get; init; }
}

/// <summary>
/// A logical change detected from user edits.
/// </summary>
public sealed class LogicalChange
{
public required string ChangeType { get; init; } // added, removed, modified, moved
public required string ElementType { get; init; }
public required string Description { get; init; }
public string? OldText { get; init; }
public string? NewText { get; init; }
public DateTime Timestamp { get; init; }
}

/// <summary>
/// SSE event sent during LLM streaming.
/// </summary>
public sealed class LlmStreamEvent
{
public required string Type { get; init; }
public string? Content { get; init; }
public LlmPatch? Patch { get; init; }
public string? Error { get; init; }
public LlmStreamStats? Stats { get; init; }
}

/// <summary>
/// A patch generated by the LLM to apply to the document.
/// </summary>
public sealed record LlmPatch
{
/// <summary>
/// Operation: add, replace, remove, move.
/// </summary>
public required string Op { get; init; }

/// <summary>
/// Target path in the document.
/// </summary>
public required string Path { get; init; }

/// <summary>
/// Value for add/replace operations.
/// </summary>
public object? Value { get; init; }

/// <summary>
/// Source path for move operations.
/// </summary>
public string? From { get; init; }
}

/// <summary>
/// Statistics at the end of streaming.
/// </summary>
public sealed class LlmStreamStats
{
public int InputTokens { get; init; }
public int OutputTokens { get; init; }
public int PatchesGenerated { get; init; }
public double DurationMs { get; init; }
}

/// <summary>
/// Report user's changes to the backend for context tracking.
/// </summary>
public sealed class UserChangeReport
{
public required string SessionId { get; init; }
public required DocumentContent Before { get; init; }
public required DocumentContent After { get; init; }
}

/// <summary>
/// Result of processing user changes into logical patches.
/// </summary>
public sealed class UserChangeResult
{
public required List<LogicalChange> Changes { get; init; }
public required string Summary { get; init; }
}

/// <summary>
/// JSON serialization context for AOT compatibility.
/// </summary>
[JsonSerializable(typeof(LlmEditRequest))]
[JsonSerializable(typeof(LlmStreamEvent))]
[JsonSerializable(typeof(LlmPatch))]
[JsonSerializable(typeof(UserChangeReport))]
[JsonSerializable(typeof(UserChangeResult))]
[JsonSerializable(typeof(DocumentContent))]
[JsonSerializable(typeof(DocumentElement))]
[JsonSerializable(typeof(SelectionInfo))]
[JsonSerializable(typeof(LogicalChange))]
[JsonSerializable(typeof(LlmStreamStats))]
[JsonSerializable(typeof(List<LlmPatch>))]
[JsonSerializable(typeof(List<LogicalChange>))]
[JsonSerializable(typeof(List<DocumentElement>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class WordAddinJsonContext : JsonSerializerContext
{
}
Loading