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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ asrs.log.txt
**/global.json
PublishProfiles/
**.log.txt
**.log
**.log
.env
34 changes: 33 additions & 1 deletion samples/Whiteboard/Diagram.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace Microsoft.Azure.SignalR.Samples.Whiteboard;
Expand All @@ -18,6 +20,8 @@ public abstract class Shape
public string Color { get; set; }

public int Width { get; set; }

public string Fill { get; set; }
}

public class Polyline : Shape
Expand Down Expand Up @@ -57,13 +61,41 @@ public class Diagram
{
private int totalUsers = 0;

private int currentZIndex = -1;

private readonly ConcurrentDictionary<string, Tuple<int, Shape>> shapes = new();

public byte[] Background { get; set; }

public string BackgroundContentType { get; set; }

public string BackgroundId { get; set; }

public ConcurrentDictionary<string, Shape> Shapes { get; } = new ConcurrentDictionary<string, Shape>();
public int AddOrUpdateShape(string id, Shape shape)
{
var s = shapes.AddOrUpdate(id, _ => Tuple.Create(Interlocked.Increment(ref currentZIndex), shape), (_, v) => Tuple.Create(v.Item1, shape));
return s.Item1;
}

public void RemoveShape(string id)
{
shapes.TryRemove(id, out _);
}

public Shape GetShape(string id)
{
return shapes[id].Item2;
}

public void ClearShapes()
{
shapes.Clear();
}

public IEnumerable<Tuple<string, int, Shape>> GetShapes()
{
return shapes.AsEnumerable().Select(l => Tuple.Create(l.Key, l.Value.Item1, l.Value.Item2));
}

public int UserEnter()
{
Expand Down
44 changes: 22 additions & 22 deletions samples/Whiteboard/Hub/DrawHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.SignalR;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using System;

namespace Microsoft.Azure.SignalR.Samples.Whiteboard;
Expand All @@ -13,17 +11,19 @@ public class DrawHub(Diagram diagram) : Hub
{
private readonly Diagram diagram = diagram;

private async Task UpdateShape(string id, Shape shape)
private async Task<int> UpdateShape(string id, Shape shape)
{
diagram.Shapes[id] = shape;
await Clients.Others.SendAsync("ShapeUpdated", id, shape.GetType().Name, shape);
var z = diagram.AddOrUpdateShape(id, shape);
await Clients.Others.SendAsync("ShapeUpdated", id, shape.GetType().Name, shape, z);
return z;
}

public override Task OnConnectedAsync()
public override async Task OnConnectedAsync()
{
var t = Task.WhenAll(diagram.Shapes.AsEnumerable().Select(l => Clients.Client(Context.ConnectionId).SendAsync("ShapeUpdated", l.Key, l.Value.GetType().Name, l.Value)));
if (diagram.Background != null) t = t.ContinueWith(_ => Clients.Client(Context.ConnectionId).SendAsync("BackgroundUpdated", diagram.BackgroundId));
return t.ContinueWith(_ => Clients.All.SendAsync("UserUpdated", diagram.UserEnter()));
await Clients.All.SendAsync("UserUpdated", diagram.UserEnter());
if (diagram.Background != null) await Clients.Client(Context.ConnectionId).SendAsync("BackgroundUpdated", diagram.BackgroundId);
foreach (var s in diagram.GetShapes())
await Clients.Caller.SendAsync("ShapeUpdated", s.Item1, s.Item3.GetType().Name, s.Item3, s.Item2);
}

public override Task OnDisconnectedAsync(Exception exception)
Expand All @@ -33,47 +33,47 @@ public override Task OnDisconnectedAsync(Exception exception)

public async Task RemoveShape(string id)
{
diagram.Shapes.Remove(id, out _);
diagram.RemoveShape(id);
await Clients.Others.SendAsync("ShapeRemoved", id);
}

public async Task AddOrUpdatePolyline(string id, Polyline polyline)
public async Task<int> AddOrUpdatePolyline(string id, Polyline polyline)
{
await this.UpdateShape(id, polyline);
return await this.UpdateShape(id, polyline);
}

public async Task PatchPolyline(string id, Polyline polyline)
{
if (diagram.Shapes[id] is not Polyline p) throw new InvalidOperationException($"Shape {id} does not exist or is not a polyline.");
if (diagram.GetShape(id) is not Polyline p) throw new InvalidOperationException($"Shape {id} does not exist or is not a polyline.");
if (polyline.Color != null) p.Color = polyline.Color;
if (polyline.Width != 0) p.Width = polyline.Width;
p.Points.AddRange(polyline.Points);
await Clients.Others.SendAsync("ShapePatched", id, polyline);
}

public async Task AddOrUpdateLine(string id, Line line)
public async Task<int> AddOrUpdateLine(string id, Line line)
{
await this.UpdateShape(id, line);
return await this.UpdateShape(id, line);
}

public async Task AddOrUpdateCircle(string id, Circle circle)
public async Task<int> AddOrUpdateCircle(string id, Circle circle)
{
await this.UpdateShape(id, circle);
return await this.UpdateShape(id, circle);
}

public async Task AddOrUpdateRect(string id, Rect rect)
public async Task<int> AddOrUpdateRect(string id, Rect rect)
{
await this.UpdateShape(id, rect);
return await this.UpdateShape(id, rect);
}

public async Task AddOrUpdateEllipse(string id, Ellipse ellipse)
public async Task<int> AddOrUpdateEllipse(string id, Ellipse ellipse)
{
await this.UpdateShape(id, ellipse);
return await this.UpdateShape(id, ellipse);
}

public async Task Clear()
{
diagram.Shapes.Clear();
diagram.ClearShapes();
diagram.Background = null;
await Clients.Others.SendAsync("Clear");
}
Expand Down
25 changes: 14 additions & 11 deletions samples/Whiteboard/MCPServer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ const logger = new class {
log = (level, message) => level > 1 && console.error(`[${level}] ${message}`);
};

const connection = new HubConnectionBuilder().withUrl(`${process.env['WHITEBOARD_ENDPOINT'] || 'http://localhost:5000'}/draw`).withAutomaticReconnect().configureLogging(logger).build();
const endpoint = process.env['WHITEBOARD_ENDPOINT'] || 'http://localhost:5000';

const connection = new HubConnectionBuilder().withUrl(`${endpoint}/draw`).withAutomaticReconnect().configureLogging(logger).build();

const server = new McpServer({
name: 'Whiteboard',
version: '1.0.0'
});

let color = z.string().describe('color of the shape, valid values are: black, grey, darkred, red, orange, yellow, green, deepskyblue, indigo, purple');
let width = z.number().describe('width of the shape, valid values are: 1, 2, 4, 8');
let color = z.string().describe('line color of the shape, value should be a valid CSS color');
let fill = z.string().describe('fill color of the shape, value should be a valid CSS color');
let width = z.number().describe('width of the shape in pixels');
let point = z.object({
x: z.number().describe('x coordinate of the point, 0 denotes the left edge of the whiteboard'),
y: z.number().describe('y coordinate of the point, 0 denotes the top edge of the whiteboard')
Expand All @@ -34,7 +37,7 @@ server.tool(
'add_or_update_polyline', 'add or update a polyline on whiteboard',
{
id, polyline: z.object({
color, width,
color, width, fill,
points: z.array(point).describe('array of points that define the polyline')
})
},
Expand All @@ -47,7 +50,7 @@ server.tool(
'add_or_update_line', 'add or update a line on whiteboard',
{
id, line: z.object({
color, width,
color, width, fill,
start: point.describe('start point of the line'),
end: point.describe('end point of the line')
})
Expand All @@ -61,7 +64,7 @@ server.tool(
'add_or_update_circle', 'add or update a circle on whiteboard',
{
id, circle: z.object({
color, width,
color, width, fill,
center: point.describe('center point of the circle'),
radius: z.number().describe('radius of the circle')
})
Expand All @@ -75,9 +78,9 @@ server.tool(
'add_or_update_rect', 'add or update a rectangle on whiteboard',
{
id, rect: z.object({
color, width,
color, width, fill,
topLeft: point.describe('top left corner of the rectangle'),
bottomRight: point.describe('bottom right of the rectangle')
bottomRight: point.describe('bottom right corner of the rectangle')
})
},
async ({ id, rect }) => {
Expand All @@ -89,9 +92,9 @@ server.tool(
'add_or_update_ellipse', 'add or update an ellipse on whiteboard',
{
id, ellipse: z.object({
color, width,
color, width, fill,
topLeft: point.describe('top left corner of the bounding rectangle of the ellipse'),
bottomRight: point.describe('bottom right of the bounding rectangle of the ellipse')
bottomRight: point.describe('bottom right corner of the bounding rectangle of the ellipse')
})
},
async ({ id, ellipse }) => {
Expand Down Expand Up @@ -120,7 +123,7 @@ const transport = new StdioServerTransport();
await server.connect(transport);

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
for (;;) {
for (; ;) {
try {
await connection.start();
break;
Expand Down
35 changes: 26 additions & 9 deletions samples/Whiteboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Open multiple windows on http://localhost:5000/, when you paint in one window, o
2. Then use the following command to deploy it to Azure Web App:

```
az webapp deployment source config-zip --src <path_to_zip_file> -n <app_name> -g <resource_group_name>
az webapp deploy --src-path <path_to_zip_file> -n <app_name> -g <resource_group_name>
```

3. Set Azure SignalR Service connection string in the application settings. You can do it through portal or using Azure CLI:
Expand Down Expand Up @@ -85,21 +85,38 @@ To install the MCP server:
npm install
```

2. The MCP server will by default connect to local server (http://localhost:5000). If your whiteboard is not running locally, set the endpoint in `WHITEBOARD_ENDPOINT` environment variable or `.env` file

3. Configure the MCP server in your LLM app (like Claude Desktop or GitHub Copilot in VS Code):
2. Configure the MCP server in your LLM app (like Claude Desktop or GitHub Copilot in VS Code):

```json
"mcpServers": {
"Whiteboard": {
"command": "node",
"args": [
"<path-to-MCPServer-project>/index.js"
]
}
}
"args": [
"<path_to_MCPServer_project>/index.js"
]
}
}
```

> This will by default connect to local server (http://localhost:5000). If your whiteboard is not running locally, save the endpoint to a `.env` file
> ```
> WHITEBOARD_ENDPOINT=<endpoint-of-whiteboard>
> ```
>
> Change the config to the following:
> ```json
> "mcpServers": {
> "Whiteboard": {
> "command": "node",
> "args": [
> "--env-file",
> "<path_to_MCPServer_project>/.env",
> "<path_to_MCPServer_project>/index.js"
> ]
> }
> }
> ```

> Change `mcpServers` to `mcp` if you're using VS Code

4. Start the server if it's not automatically started (like in VS Code)
Expand Down
14 changes: 14 additions & 0 deletions samples/Whiteboard/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
</a>
</div>
</li>
<span class="navbar-text">Color</span>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Expand All @@ -76,6 +77,19 @@
</a>
</div>
</li>
<span class="navbar-text">Fill</span>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<span class="toolbox selected" v-bind:style="{ 'background-color': fill }"></span>
</a>
<div class="dropdown-menu">
<a v-for="c in colors" v-bind:class="{ active: c === fill, 'dropdown-item': true }"
v-on:click.prevent="fill = c" href="#">
<span class="toolbox" v-bind:style="{ 'background-color': c }"></span>
</a>
</div>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Expand Down
Loading