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
41 changes: 41 additions & 0 deletions samples/Whiteboard/Controllers/BackgroundController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using System;
using System.IO;
using System.Threading.Tasks;

namespace Microsoft.Azure.SignalR.Samples.Whiteboard;

[Route("/background")]
public class BackgroundController(IHubContext<DrawHub> context, Diagram diagram) : Controller
{
private readonly IHubContext<DrawHub> hubContext = context;
private readonly Diagram diagram = diagram;

[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file)
{
diagram.BackgroundId = Guid.NewGuid().ToString().Substring(0, 8);
diagram.Background = new byte[file.Length];
diagram.BackgroundContentType = file.ContentType;
using (var stream = new MemoryStream(diagram.Background))
{
await file.CopyToAsync(stream);
}

await hubContext.Clients.All.SendAsync("BackgroundUpdated", diagram.BackgroundId);

return Ok();
}

[HttpGet("{id}")]
public IActionResult Download(string id)
{
if (diagram.BackgroundId != id) return NotFound();
return File(diagram.Background, diagram.BackgroundContentType);
}
}
77 changes: 77 additions & 0 deletions samples/Whiteboard/Diagram.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

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

namespace Microsoft.Azure.SignalR.Samples.Whiteboard;

public class Point
{
public int X { get; set; }
public int Y { get; set; }
}

public abstract class Shape
{
public string Color { get; set; }

public int Width { get; set; }
}

public class Polyline : Shape
{
public List<Point> Points { get; set; }
}

public class Line : Shape
{
public Point Start { get; set; }

public Point End { get; set; }
}

public class Circle : Shape
{
public Point Center { get; set; }

public int Radius { get; set; }
}

public class Rect : Shape
{
public Point TopLeft { get; set; }

public Point BottomRight { get; set; }
}

public class Ellipse : Shape
{
public Point TopLeft { get; set; }

public Point BottomRight { get; set; }
}

public class Diagram
{
private int totalUsers = 0;

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 UserEnter()
{
return Interlocked.Increment(ref totalUsers);
}

public int UserLeave()
{
return Interlocked.Decrement(ref totalUsers);
}
}
85 changes: 85 additions & 0 deletions samples/Whiteboard/Hub/DrawHub.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Microsoft. All rights reserved.
// 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;

public class DrawHub(Diagram diagram) : Hub
{
private readonly Diagram diagram = diagram;

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

public override 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()));
}

public override Task OnDisconnectedAsync(Exception exception)
{
return Clients.All.SendAsync("UserUpdated", diagram.UserLeave());
}

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

public async Task AddOrUpdatePolyline(string id, Polyline polyline)
{
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 (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)
{
await this.UpdateShape(id, line);
}

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

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

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

public async Task Clear()
{
diagram.Shapes.Clear();
diagram.Background = null;
await Clients.Others.SendAsync("Clear");
}

public async Task SendMessage(string name, string message)
{
await Clients.Others.SendAsync("NewMessage", name, message);
}
}
131 changes: 131 additions & 0 deletions samples/Whiteboard/MCPServer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { HubConnectionBuilder } from '@microsoft/signalr';
import dotenv from 'dotenv';

dotenv.config();

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 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 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')
});
let id = z.string().describe('unique identifier of the shape, if it does not exist, it will be created, if it exists, it will be updated');

server.tool('send_message', 'post a message on whiteboard', { name: z.string(), message: z.string() }, async ({ name, message }) => {
await connection.send('sendMessage', name, message);
return { content: [{ type: 'text', text: 'Message sent' }] }
});

server.tool(
'add_or_update_polyline', 'add or update a polyline on whiteboard',
{
id, polyline: z.object({
color, width,
points: z.array(point).describe('array of points that define the polyline')
})
},
async ({ id, polyline }) => {
await connection.send('addOrUpdatePolyline', id, polyline);
return { content: [{ type: 'text', text: 'Polyline added or updated' }] };
});

server.tool(
'add_or_update_line', 'add or update a line on whiteboard',
{
id, line: z.object({
color, width,
start: point.describe('start point of the line'),
end: point.describe('end point of the line')
})
},
async ({ id, line }) => {
await connection.send('addOrUpdateLine', id, line);
return { content: [{ type: 'text', text: 'Line added or updated' }] };
});

server.tool(
'add_or_update_circle', 'add or update a circle on whiteboard',
{
id, circle: z.object({
color, width,
center: point.describe('center point of the circle'),
radius: z.number().describe('radius of the circle')
})
},
async ({ id, circle }) => {
await connection.send('addOrUpdateCircle', id, circle);
return { content: [{ type: 'text', text: 'Circle added or updated' }] };
});

server.tool(
'add_or_update_rect', 'add or update a rectangle on whiteboard',
{
id, rect: z.object({
color, width,
topLeft: point.describe('top left corner of the rectangle'),
bottomRight: point.describe('bottom right of the rectangle')
})
},
async ({ id, rect }) => {
await connection.send('addOrUpdateRect', id, rect);
return { content: [{ type: 'text', text: 'Rectangle added or updated' }] };
});

server.tool(
'add_or_update_ellipse', 'add or update an ellipse on whiteboard',
{
id, ellipse: z.object({
color, width,
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')
})
},
async ({ id, ellipse }) => {
await connection.send('addOrUpdateEllipse', id, ellipse);
return { content: [{ type: 'text', text: 'Ellipse added or updated' }] };
});

server.tool(
'remove_shape', 'remove a shape from whiteboard',
{ id },
async ({ id }) => {
await connection.send('removeShape', id);
return { content: [{ type: 'text', text: 'Shape removed' }] };
});

server.tool(
'clear', 'clear the whiteboard',
{},
async () => {
await connection.send('clear');
return { content: [{ type: 'text', text: 'Whiteboard cleared' }] };
});

const transport = new StdioServerTransport();

await server.connect(transport);

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
for (;;) {
try {
await connection.start();
break;
} catch (e) {
console.error('Failed to start SignalR connection: ' + e.message);
await sleep(5000);
}
}
Loading