Orpheus Systems Architecture

Phase C — Application Architecture

Phase C — Application Architecture

1. Architectural style

A modular monorepo of SPFx solutions sharing two library packages. The SPFx suite runs in the browser inside SharePoint Online pages and has no server-side component; that includes the HTML template timesheet, which is rendered client-side (ADR-010). One data access seam (IDataService) separates UI from storage (ADR-003).

The contract agent adds the one server component: an MCP server (mcp-contract-calendar, Node 22) behind a declarative agent in Microsoft 365 Copilot Chat (ADR-011, ADR-012). It imports shared-core and writes through the same IDataService implementation as the web part, so both channels enforce the same booking rules from the same code. ADR-011 to ADR-015 were accepted after the live check on the dev tenant (2026-10-23); ADR-016, the Azure production target, stays Proposed.

2. Package catalog (design §4)

Package npm name Kind Responsibility
packages/shared-core @orpheussystems/shared-core library Models, IDataService, LocalDataService, SharePointDataService, period / overlap / totals logic
packages/shared-ui @orpheussystems/shared-ui library Orpheus theme tokens, header/footer components, PDF renderer (shared-ui/pdf)
packages/ext-app-customizer @orpheussystems/ext-app-customizer SPFx application customizer Header and footer placeholders, role-aware menu
packages/wp-contract-calendar @orpheussystems/wp-contract-calendar SPFx web part Calendar, stats, overlap dialog, timesheet generation
packages/ext-field-customizer @orpheussystems/ext-field-customizer SPFx field customizer Remaining-days bars on the Contracts list
packages/ext-form-customizer @orpheussystems/ext-form-customizer SPFx form customizer New / Edit / Display forms for Contracts
packages/mcp-contract-calendar @orpheussystems/mcp-contract-calendar MCP server, Node 22, TypeScript Action layer of the contract agent: six tools over stateless Streamable HTTP, Entra token validation, on-behalf-of to SharePoint, audit event per tool call (README)
packages/agent-contract-calendar @orpheussystems/agent-contract-calendar Declarative agent (Agents Toolkit project, no dependencies, no build) "Contract Calendar" agent manifest, instructions and RemoteMCPServer plugin with Entra SSO (README)

Build order via TypeScript project references: shared-core → shared-ui → the four SPFx packages; mcp-contract-calendar builds after shared-core (npm run build:shared first). The agent package is provisioned by the Agents Toolkit, not built. Each SPFx package stays a standard Yeoman-generated project; shared packages are added via workspace symlinks and path mapping in tsconfig.base.json. No prior-employer identifiers (design §8) remain anywhere.

3. Container view

C4Container
  title Containers — Orpheus SPFx suite (browser, SharePoint Online page) and contract agent (MCP server)
  Person(user, "Contractor / Manager / HR / Admin")
  System_Boundary(suite, "Orpheus SPFx suite") {
    Container(app, "ext-app-customizer", "SPFx application customizer", "Header, footer, role-aware menu: My calendar · Contracts · Holidays")
    Container(wp, "wp-contract-calendar", "SPFx web part, React + Fluent UI", "Contract selector, month grids, stats, overlap dialog, Generate timesheet (built-in or HTML template)")
    Container(field, "ext-field-customizer", "SPFx field customizer", "On-site / off-site progress bars on RemainingDays")
    Container(form, "ext-form-customizer", "SPFx form customizer", "Contracts New/Edit/Display with validation and overlap notice")
    Container(core, "shared-core", "TypeScript library", "Models, IDataService, Local + SharePoint implementations, template data query")
    Container(ui, "shared-ui", "TypeScript library", "Theme tokens, header/footer components, pdfmake timesheet builder, html-to-pdfmake template renderer")
  }
  System_Boundary(agentsys, "Contract agent") {
    Container(agent, "agent-contract-calendar", "Declarative agent, Agents Toolkit", "Instructions; RemoteMCPServer plugin with dynamic tool discovery and Entra SSO")
    Container(mcp, "mcp-contract-calendar", "MCP server, Node 22, TypeScript, Express", "Six tools; token validation; on-behalf-of; role and visibility checks; audit event per tool call")
  }
  System_Ext(copilot, "Microsoft 365 Copilot Chat", "Copilot Chat or Teams; hosts the declarative agent")
  System_Ext(entra, "Microsoft Entra ID", "Issues the SSO token; on-behalf-of exchange")
  System_Ext(spo, "SharePoint Online", "Contracts, CalendarEntries, Holidays lists; ContractCalendar.aspx; timesheet template file")
  Rel(user, app, "Sees on every page")
  Rel(user, wp, "Marks days, generates timesheet")
  Rel(user, field, "Sees in Contracts list views")
  Rel(user, form, "Creates / edits contracts")
  Rel(wp, core, "IDataService via React context")
  Rel(wp, ui, "Theme, PDF builder")
  Rel(app, ui, "Theme, header/footer")
  Rel(app, core, "Role derivation")
  Rel(form, core, "Models, overlap query")
  Rel(field, ui, "Theme")
  Rel(core, spo, "PnPjs v4: CAML/OData on indexed columns, batched writes")
  Rel(user, copilot, "Asks: book, remove, list days and contracts")
  Rel(copilot, agent, "Runs the agent")
  Rel(agent, mcp, "MCP over HTTPS (POST /mcp), Entra SSO bearer token")
  Rel(mcp, entra, "On-behalf-of exchange for SharePoint")
  Rel(mcp, core, "Imports SharePointDataService, rules, dates")
  Rel(mcp, spo, "PnPjs v4 REST as the user (contract directory with Vendor)")

The agent path is Copilot Chat → declarative agent → MCP server → SharePoint. The server never uses app-only access; every SharePoint call carries the calling user's delegated token (ADR-012).

4. Data access layer — IDataService (design §6.3)

interface IDataService {
  getMyContracts(): Promise<Contract[]>;            // role-filtered, see security model
  getEntries(contractId, from, to): Promise<CalendarEntry[]>;
  upsertEntry(entry): Promise<CalendarEntry>;
  deleteEntry(contractId, date): Promise<void>;
  getHolidays(country, from, to): Promise<Holiday[]>;
  getCurrentUser(): Promise<UserInfo>;
}
Implementation Selected when Behaviour
LocalDataService Environment.type === Local (workbench) or web part property useLocalData is on Bundled JSON fixtures; in-memory writes; demo user (contractor or manager) chosen in the property pane
SharePointDataService Otherwise PnPjs v4 against the three lists chosen in the property pane; CAML/OData filters on indexed columns; batched writes; keeps OnSiteUsedDays / OffSiteUsedDays current

Service selection happens once in the web part's onInit and is injected into the React tree via context. No component talks to SharePoint directly.

Service-level invariants: one entry per contractor per date across all their contracts (ADR-008); upsertEntry on a date already held by another of the contractor's contracts moves the entry.

The MCP server is a third consumer of IDataService. Per request it creates a SharePointDataService from an SPFI built with the user's on-behalf-of token, and it uses LocalDataService in its unit tests and in DATA_MODE=local. Every agent write goes through upsertEntry / deleteEntry, so the invariants above hold for agent writes too (ADR-011).

5. Component descriptions

5.1 Calendar web part — wp-contract-calendar (design §6.4)

Behaviour rebuilt in React with Fluent UI from docs/mocks/onsite_calendar.html.

Property pane

Property Default Note
Contracts / CalendarEntries / Holidays list pickers — Filtered by content type
useLocalData off Forces LocalDataService
Demo user contractor Local only
Months to show Contract span, capped at 12
Treat weekends as blocked on
Approving-organisation label "Client approval" Printed on the third signature box
Service-provider label "Service provider" Printed on the second signature box
Timesheet template (optional) (timesheetTemplateUrl) empty Site-relative or server-relative path to an .html / .htm file on the site; empty means the built-in layout (ADR-010)

UI

Mock behaviours carried over: Monday-first grid offset; toggle-off on same type; half = 0.5, full = 1; "good" vs "warning" colouring of remaining; blocked days are not clickable. Mock behaviours not carried over: localStorage persistence (replaced by IDataService), JSON export, editable targets in the UI (targets come from the contract).

5.2 Timesheet PDF — shared-ui/pdf (design §6.5)

Client-side with pdfmake (ADR-004). Landscape A4, one page per 12 months.

HTML template (ADR-010, authoring guide). When the web part property Timesheet template (optional) holds a path, the page layout comes from an HTML file stored on the site instead of the built-in layout:

5.3 Application customizer — ext-app-customizer (design §6.6, §6.9)

⚠️ Open: Final wording of the footer kicker, and whether the Orpheus logo SVG is available or must be redrawn (design §11).

5.4 Field customizer — ext-field-customizer (design §6.7)

Bound at deployment to RemainingDays on Contracts. Renders two slim progress bars (on-site and off-site used vs target) with the remaining number as text; amber at ≥ 80 % used, red when over. Reads OnSiteTargetDays, OffSiteTargetDays, OnSiteUsedDays, OffSiteUsedDays from the item itself.

5.5 Form customizer — ext-form-customizer (design §6.8)

Replaces New / Edit / Display forms of Contracts.

5.6 Contract agent — agent-contract-calendar and mcp-contract-calendar

Contractors book and remove days, and contract managers and HR read contracts grouped by vendor, by talking to the "Contract Calendar" agent in Copilot Chat (ADR-011, ADR-012).

Declarative agent (README). Manifest-only Agents Toolkit project: app manifest, declarative agent manifest, instructions (instruction.txt) and one action, a RemoteMCPServer plugin (manifest v2.4) with dynamic tool discovery (functions empty, run_for_functions: ["*"]) and Entra SSO. Copilot reads the tool list and schemas from the server at runtime, so the package does not change when a tool changes. The instructions require one confirmation before book_days and remove_days.

MCP server (README). Express app on port 8787 with stateless Streamable HTTP: POST /mcp for MCP requests, GET /healthz for liveness, GET / DELETE /mcp return 405. It reuses shared-core (SharePointDataService, deriveRoles, rules.ts, dates.ts) unchanged and adds only what the web part lacks: role and visibility checks for the agent's users including HR, the vendor read side, range orchestration, and the audit event.

Tool Who What
get_my_contracts all The caller's visible contracts with vendor, period, used and remaining days, plus the caller's roles and today's date
list_contracts (vendor?, manager?, includeClosed?) managers, HR, admins Contracts grouped by vendor; manager takes a name or email; several matching people give needs_choice
get_calendar (contractId, from, to) all, within visibility Entries, holidays, totals and remaining days of one contract (max 366 days)
book_days (from, to, type?, contractId?, note?) contractors Books business days, max 31 per call; weekends, holidays and days outside the contract are skipped and reported; an overlap day without contractId returns needs_choice and writes nothing; per-day result booked, skipped, failed
remove_days (from, to, contractId) contractors Removes days from one own active contract; days without an entry come back as notFound
get_holidays (country, from, to) all Public holidays of a country (max 366 days)

With six tools the plugin stays under Microsoft's guidance of at most ten. Tool errors come back as MCP tool results with isError and a code (not_found, forbidden, invalid_input, internal). Run modes combine AUTH_MODE (entra default, or none) and DATA_MODE (sharepoint default, or local); none with sharepoint is rejected at start-up.

The contract agent does not maintain holidays; it only reads them. The holiday agent (RM-1 in Phase E) stays a separate item.

6. Security model (design §7)

Contract agent (ADR-012, ADR-013):

⚠️ Open: Guest contractors are not verified with declarative agents and Copilot Chat (R-AGT-4). This can block the channel for contractors who are guest users.

7. Integration points

From To Mechanism
SharePointDataService SharePoint lists PnPjs v4 REST
Form customizer Display form Calendar page Link with contractId query parameter
Application customizer menu Calendar page, lists Relative URLs from component properties
Provisioning script Customizers Writes ClientSideComponentProperties, binds field and content type
Web part PDF builder Timesheet template file and its images on the site PnPjs getFileByServerRelativePath with the viewer's permissions, on every generation (ADR-010)
Web part PDF builder Contracts (template columns) getContractTemplateData in shared-core, one item by id
Copilot Chat (declarative agent) MCP server MCP over HTTPS, stateless Streamable HTTP POST /mcp, Entra SSO bearer token (ADR-012)
MCP server Microsoft Entra ID On-behalf-of exchange (msal-node) for a delegated SharePoint token
MCP server SharePoint lists PnPjs v4 REST as the user, through SharePointDataService and the server's contract directory
MCP server Audit sink One JSON line per tool call to stdout or AUDIT_LOG_PATH; Log Analytics in the Azure target (ADR-014)

8. Left to the builder (design §9)