Give Your MAUI Agent Hands — Contacts, Reminders & Location as AI Tools
I built a chat assistant into a MAUI app a while back and it was… fine. It could answer questions. It could summarise things. And then a tester asked it, out loud, “remind me to call the plumber at three” — and it cheerfully replied “Sure, I’ll remind you!” and then, of course, did absolutely nothing. It had no hands. It could only talk.
That’s the moment that stuck with me. A model on its own is a very articulate box with no arms. The interesting part isn’t the conversation — it’s giving it a small, safe set of things it can actually do on the device.
When I shipped Shiny.Health.Extensions.AI, that was the proof: hand the model a tool to read step counts and “how did I sleep last week?” stops being a vibe and becomes a number pulled from HealthKit. So I did the same thing for three more device services that people ask assistants about constantly — contacts, reminders, and location.
Same recipe every time: you opt-in to exactly which operations the model is allowed to call (an allow-list you control on behalf of the agent — it is emphatically not an OS permission prompt), it’s read-only by default, and it’s all IsAotCompatible — hand-built schemas, JsonNode results, no reflection in the tool path.
Contacts — talk to your address book
dotnet add package Shiny.Contacts.Extensions.AI
builder.Services.AddContactStore();
builder.Services.AddContactsAITools(tools => tools
.AddContacts(ContactAICapabilities.ReadWrite) // Read is the default; ReadWrite adds create/update/delete
);
That gives the model search_contacts and get_contact, and — if you opt-in to write — create_contact, update_contact, and delete_contact.
I reach for this whenever the app is basically a personal assistant and tapping through the contacts UI is the slow path. The model searches for an id, then acts on it. Real things people say:
- “What’s Jane Doe’s mobile number?”
- “Add a contact for my new dentist — Dr. Patel at Bright Smiles Dental, 555-0142.”
- “Change my brother Mike’s email to mike@newjob.com.”
- “Do I have anyone saved from Acme Corp?”
- “Delete the contact for my old landlord.” — this one’s destructive, so I tell the model in the system prompt to confirm before it calls
delete_contact.
Reminders — “remind me…” that actually fires
dotnet add package Shiny.Notifications.Extensions.AI
builder.Services.AddNotifications();
builder.Services.AddNotificationAITools(tools => tools
.AddReminders(ReminderAICapabilities.ReadWrite, channel: "reminders")
);
This is the one that fixes my plumber story. Under the hood it’s local notifications, but I framed the tools as reminders because that’s how people talk: list_reminders, create_reminder (now, at a set time, or daily), and cancel_reminder.
- “Remind me to call the plumber at 3pm today.”
- “Set a daily reminder to take my meds at 8:30am.”
- “What reminders do I have set?”
- “Cancel the one about the dentist.”
- “Nudge me to water the plants every evening at 6.”
scheduleFor handles the one-shots and repeatDailyAt handles the recurring ones — the model picks which based on how you phrased it.
Location — where am I, and how far to there?
dotnet add package Shiny.Locations.Extensions.AI
builder.Services.AddGps();
builder.Services.AddLocationAITool(); // read-only — GPS has no write capability
GPS is read-only, so there’s nothing to configure — one call wires up get_current_location, get_distance_to, and estimate_travel_time. Now the agent knows where the user is and can reason about getting somewhere.
- “Where am I right now?”
- “How far is it from here to 51.4700, -0.4543?”
- “Roughly how long to cycle to the park at 51.51, -0.12?”
- “Am I within 2 km of the office?”
- “What’s my current speed and heading?”
One thing I was deliberate about: these are straight-line, great-circle distances and the travel-time is an assumed average speed — not a routed ETA with roads and traffic. Rather than let the model bluff, every result carries a note saying exactly that, so it answers “about 4 km as the crow flies” instead of pretending to be Google Maps.
The good part: they compose
Each package hands you a little bundle from DI. Concatenate the .Tools and give the whole pile to any IChatClient:
var tools = new List<AITool>();
tools.AddRange(sp.GetRequiredService<ContactAITools>().Tools);
tools.AddRange(sp.GetRequiredService<NotificationAITools>().Tools);
tools.AddRange(sp.GetRequiredService<LocationAITools>().Tools);
var response = await chatClient.GetResponseAsync(
messages,
new ChatOptions { Tools = tools }
);
Which means one sentence can light up all three:
“I’m meeting Sarah at the office at 5 — how far am I, and remind me to leave in an hour.”
search_contacts for Sarah, get_distance_to for the office, create_reminder for the nudge — one turn, three device services, no glue code from me.
The boring-but-important bit
The capability builders decide what the agent can see. They do not grant OS permissions. You still call RequestAccess (or start a GPS listener) from your app the normal way before you hand the agent the wheel — the tools assume access is already granted and return a tidy error object if it isn’t. Keep the model on the allow-list you’re comfortable with, request the real permissions yourself, and let it do the rest.
That’s the whole idea, really. Don’t build an assistant that can only talk. Give it a few hands and watch what people ask it to do.
comments powered by Disqus