-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMauiProgram.cs
More file actions
158 lines (135 loc) · 7.55 KB
/
Copy pathMauiProgram.cs
File metadata and controls
158 lines (135 loc) · 7.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
using System.Net;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Protocol;
using Sample.Maui.Server;
using Shiny.Net.HttpServer;
using Shiny.Net.HttpServer.FileBrowser;
using Shiny.Net.HttpServer.Mcp;
using Shiny.Net.HttpServer.Security;
using Shiny.Net.HttpServer.Ssh;
using Shiny.Net.HttpServer.StaticFiles;
using Shiny.Net.HttpServer.WebDav;
namespace Sample.Maui;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
// Shiny.Maui.Shell owns the tabs: AddGeneratedMaps() is source-generated from the
// [ShellMap] attributes on the view models, so every page and view model is registered
// — and every route named — without a list here that drifts from the attributes.
.UseShinyShell(shell => shell.AddGeneratedMaps())
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
// The app's own JSON metadata, registered once. A source generator cannot see another
// generator's output, so this is the app's job rather than the library's — and it is what
// keeps serialization reflection-free, which iOS requires.
JsonTypeInfoRegistry.Register(ApiJsonContext.Default);
builder.Services.AddSingleton<NoteStore>();
builder.Services.AddSingleton<RequestLog>();
builder.Services.AddSingleton<RequireAuthenticationMiddleware>();
// The badge on the Traffic tab has to keep counting while a different tab is on screen, so
// it is a service with a startup hook rather than anything the Traffic view model owns.
builder.Services.AddSingleton<TrafficBadge>();
builder.Services.AddSingleton<IMauiInitializeService>(sp => sp.GetRequiredService<TrafficBadge>());
// The accounts live in the app's own storage and change from the Server screen, so the
// validator is a service rather than a list handed over at startup.
builder.Services.AddAuthentication().AddBasic<CredentialStore>(o => o.Realm = "Shiny device");
// An MCP server, in a phone app. The MCP SDK's own HTTP transport is an ASP.NET Core package,
// so this is the part that cannot be done any other way.
//
// ApiJsonContext is handed to WithTools rather than left to reflection: a tool's parameter
// and return types are published to the client as a JSON schema, and building that schema by
// reflection does not survive trimming — which on iOS is not optional. The context already
// describes DeviceSummary and Note for the HTTP API, so the tools reuse it. Miss a type and
// MapMcp() says so at startup rather than on the first request.
builder.Services
.AddMcpServer(o =>
{
o.ServerInfo = new Implementation { Name = "shiny-device", Version = "1.0.0" };
o.ServerInstructions =
"Reads and edits the notes held by a phone running the Shiny HTTP Server sample, " +
"and reports what that device is.";
})
.WithTools<DeviceTools>(ApiJsonContext.Default.Options)
.WithHttpTransport(o =>
{
// A phone is not a datacentre, and this endpoint is reachable from the public
// internet whenever the tunnel is up.
o.MaxSessions = 8;
o.IdleSessionTimeout = TimeSpan.FromMinutes(10);
// AllowedOrigins is left empty, which refuses every browser origin. Native MCP
// clients send no Origin header and are unaffected; what this stops is a page the
// user happens to have open driving the server behind their back.
});
builder.Services.AddHttpServer(
options =>
{
// Any, not loopback: the point is for another device to reach this one. Port 0
// lets the OS pick, so two copies of the app on one network do not collide.
options.Address = IPAddress.Any;
options.Port = 0;
},
server =>
{
var services = server.Services!;
// First in the pipeline, ahead of authentication, so the Traffic tab shows the
// requests that were turned away as well as the ones that were answered.
server.Use(services.GetRequiredService<RequestLog>());
// Identify the caller, then insist on one. Both run before the static file handler,
// because that handler answers before routing and would otherwise serve the page to
// anyone with the link.
server.UseAuthentication();
server.Use(services.GetRequiredService<RequireAuthenticationMiddleware>());
// The page a visitor lands on, served straight out of the assembly — a packaged app
// has no wwwroot on disk to point at.
server.UseEmbeddedFiles(typeof(MauiProgram).Assembly, "Sample.Maui.wwwroot");
DeviceApi.Map(server, services.GetRequiredService<NoteStore>());
// Deliberately not added to RequireAuthenticationMiddleware.AllowAnonymous: this
// endpoint hands out the device's state and lets a caller write to it, so it sits
// behind the same password as everything else. An MCP client reaches it by sending
// the Basic credentials from the settings screen.
server.MapMcp();
// The app's own storage, over HTTP. Mapped as routes rather than middleware so
// authorization can differ per verb — here everything is already behind the password
// gate, so this asks for nothing further.
server.MapFileBrowser("/files", o =>
{
o.RootPath = FileSystem.AppDataDirectory;
o.AllowWrite = true;
o.AllowDelete = true;
});
// The same directory again, over WebDAV — and the difference is who the client is.
// /files is a JSON API for a script; this is the protocol a desktop already speaks,
// so the phone's storage opens as a drive in Finder or Windows Explorer with nothing
// installed on either end. Point them at http://<device>:<port>/dav and sign in with
// the same account as everything else.
//
// Locking is left on, and that is not a detail: Finder and the Windows redirector
// both check for DAV class 2 at mount time and mount read-only without it, however
// much AllowWrite allows.
server.MapWebDav("/dav", o =>
{
o.RootPath = FileSystem.AppDataDirectory;
o.AllowWrite = true;
o.AllowDelete = true;
o.DisplayName = DeviceInfo.Name;
});
},
// Started by the UI instead, so the app does not open a port before anyone asked it to.
autoStart: false
);
// localhost.run: no account, no key, nothing installed. The URL it assigns is what the app
// shows on screen.
builder.Services.AddQuickTunnel();
return builder.Build();
}
}