oth-mcp 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,7 @@ Switch environment or check current state.
99
99
 
100
100
  ```
101
101
  oth_env({ environment: "stag" })
102
- oth_env({}) // show current env + auth state
102
+ oth_env({}) // show current env, auth state, and the username calls run as
103
103
  ```
104
104
 
105
105
  ## Limitations
@@ -1 +1 @@
1
- export declare const API_GUIDE = "# OTH API Endpoint Reference\n\n## Key Concepts\n\nLinks are the API's primary navigation mechanism. Every response includes a\nlinks object with absolute URIs. Use these as-is for subsequent requests \u2014\nnever construct URLs from IDs manually. Foreign key references (e.g. patient\nin a measurement, patientGroups in a patient) are always link URIs, not bare IDs.\n\nAuth: the MCP server handles auth transparently. For reference, most read\nendpoints and questionnaire submission accept Basic auth. Acknowledgements,\nignored, and manual measurement-captures require a clinician-scoped Bearer JWT\nobtained via POST /idp2/tokens.\n\n## Common Workflows\n\nReview results:\n GET /clinician/api/patients?uniqueId={nhsNo}\n GET /results/results?patient={patient-link}&acknowledged=false\n GET {result-link}\n PUT {result-link}/acknowledgement (Bearer JWT required)\n\nSubmit questionnaire response:\n GET /clinician/api/questionnaire_definitions/{id}\n GET {links.activeQuestionnaire} (extract node names from response)\n POST {questionnaire-link}/results\n {version:\"1.0\", date:ISO-8601, links:{patient:uri},\n output:[{name:\"{nodeName}.FIELD\", type:\"Integer\"|\"Float\"|\"Object\"|\"Boolean\", value:<typed>}]}\n\nSubmit measurement (clinician-entered):\n POST /measurements/measurement-captures\n {source, sourceId, captureTime, links:{patient:uri},\n measurements:[{type, timestamp, measurement:{<type-specific>},\n origin:{manualMeasurement:{enteredBy} | deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier}}}]}\n Common types: blood_pressure({systolic,diastolic,unit}), pulse({value,unit}),\n saturation({value,unit}), temperature({value,unit}), weight(Float)\n\nPatient lifecycle:\n POST /clinician/api/patients (with links.patientGroups)\n GET+PUT /organizations/patients/{id}/metadata (PUT replaces entire set \u2014 GET first, merge, then PUT)\n PUT /clinician/api/patients/{id} (status:\"discharged\")\n\n## Patients\nGET /clinician/api/patients ?uniqueId= &username= &firstName= &lastName= &search= &phone= &patientGroup= &id= &status= &select= &max= &offset=\nGET /clinician/api/patients/{id}\nGET /clinician/api/patients/me\nGET /clinician/api/patients/{id}/measurements ?from= &to= &max=\nGET /clinician/api/patients/{id}/measurements/aggregate\nGET /clinician/api/patients/{id}/measurement-types\nGET /clinician/api/patients/{id}/questionnaires\nGET /clinician/api/patients/{id}/questionnaire_schedules\nPOST /clinician/api/patients/{id}/questionnaire_schedules\nGET /clinician/api/patients/{id}/contact-info\nPUT /clinician/api/patients/{id}/contact-info\nGET /clinician/api/patients/{id}/acknowledgements\n status enum: all|active|inactive|discharged|discharged_self_managed|deceased (default: active)\n patientGroup and id are repeatable (up to 100\u00D7) for batch queries\n search matches across firstName, lastName, uniqueId, username\n\n## Clinicians\nGET /clinician/api/clinicians ?max= &offset=\nPOST /clinician/api/clinicians\nGET /clinician/api/clinicians/{id}\nPUT /clinician/api/clinicians/{id}\nGET /clinician/api/clinicians/me\nGET /clinician/api/clinicians/me/colleagues\n\n## Questionnaire Definitions\nGET /clinician/api/questionnaire_definitions ?max= &offset=\nGET /clinician/api/questionnaire_definitions/{id}\nPOST /clinician/api/questionnaire_definitions {name, requiresManualInspection}\nPUT /clinician/api/questionnaire_definitions/{id}\n\n## Questionnaires (node graph)\nGET /clinician/api/questionnaires/{id}\nPOST /clinician/api/questionnaires/{id}/results\n\n## Results\nGET /results/results ?patient= &acknowledged= &resultType= &questionnaire= &order= &from= &to= &max=\nGET /results/results/{uuid}\nPUT /results/results/{uuid}/acknowledgement (Bearer JWT required)\nPUT /results/results/{uuid}/ignored (Bearer JWT required)\n\n## Questionnaire Schedules\nGET /clinician/api/questionnaire_schedules ?max= &offset=\n\n## Questionnaire Groups\nGET /questionnaires/questionnaire-groups ?max=\nPOST /questionnaires/questionnaire-groups {name, questionnaireDefinitions}\n\n## Patient Notes\nGET /clinician/api/patient-notes ?max= &offset=\nPOST /clinician/api/patient-notes\nGET /clinician/api/patient-notes/{id}\nPUT /clinician/api/patient-notes/{id}\nDELETE /clinician/api/patient-notes/{id}\nPOST /clinician/api/patient-notes/{id}/read\n\n## Measurements\nGET /measurements/measurement-types\nGET /measurements/measurement-captures ?patient= &source= &since= &from= &to= &max=\nPOST /measurements/measurement-captures\nGET /measurements/measurement-captures/{uuid}\nPOST /measurements/measurement-captures/{uuid}/measurements\nGET /measurements/measurement-streams ?patient= &patientGroup= &from= &to= &max=\nPOST /measurements/measurement-streams\nGET /measurements/measurement-streams/{uuid}\nGET /measurements/measurement-streams/{uuid}/measurements\nGET /measurements/measurement-streams/{uuid}/incidents\nGET /measurements/measurement-streams/incidents\nPOST /measurements/measurement-streams/acknowledgements\nGET /measurements/processors\nGET /measurements/processors/{name}\n\n## Thresholds\nGET /thresholds/patient-thresholds ?patient= &max=\nPOST /thresholds/patient-thresholds\nGET /thresholds/patient-thresholds/{uuid}\nPUT /thresholds/patient-thresholds/{uuid}\nDELETE /thresholds/patient-thresholds/{uuid}\nGET /thresholds/patient-group-thresholds ?max=\nPOST /thresholds/patient-group-thresholds\nGET /thresholds/patient-group-thresholds/{uuid}\nPUT /thresholds/patient-group-thresholds/{uuid}\nDELETE /thresholds/patient-group-thresholds/{uuid}\nPOST /thresholds/evaluation\n\n## Organizations\nGET /organizations/organizations\nGET /organizations/organizations/{uuid}\nPUT /organizations/organizations/{uuid}\nGET /organizations/organizations/{uuid}/metadata-fields\nGET /organizations/patients/{id}/metadata\nPUT /organizations/patients/{id}/metadata (replaces entire set \u2014 GET first, merge, then PUT)\n\n## Object Storage\nPOST /object-storage/objects (raw body, set Content-Type header)\nGET /object-storage/objects/{uuid}\n\n## Users & IDP\nPOST /idp2/tokens (Basic auth, body: {links:{clinician:uri}}) \u2192 201 {token}\nGET /idp2/users/{id}\nPUT /idp2/users/{id}/locked\nDELETE /idp2/users/{id}/password\nGET /idp2/roles\nGET /idp2/permissions\n\n## Patient Groups\nGET /clinician/api/patientgroups ?max= &offset=\nGET /clinician/api/patientgroups/{id}\nPOST /clinician/api/patientgroups\nPUT /clinician/api/patientgroups/{id}\nDELETE /clinician/api/patientgroups/{id}\n\n## Audit\nGET /audit/resources/{resourceUrl} ?max= &offset=\n\n## Calendar\nGET /calendar/events ?patient= &from= &to=\nPOST /calendar/events\nGET /calendar/events/{uuid}\nPUT /calendar/events/{uuid}\nDELETE /calendar/events/{uuid}\n\n## Chat\nGET /chat/threads ?patient=\nGET /chat/threads/{uuid}\nGET /chat/threads/{uuid}/messages\nPOST /chat/threads/{uuid}/read\nPOST /chat/messages\nGET /chat/messages/{uuid}\nPOST /chat/messages/{uuid}/read\nGET /chat/snippets\nPOST /chat/snippets\nPUT /chat/snippets/{uuid}\nDELETE /chat/snippets/{uuid}\n\n## Notifications\nGET /notifications/devices ?user=\nPOST /notifications/devices\nGET /notifications/subscriptions\nPOST /notifications/subscriptions\nDELETE /notifications/subscriptions/{uuid}\n\n## Mutation Request Bodies\n\nPOST /clinician/api/patients\n required: uniqueId, username, firstName, lastName, sex(male|female|unknown),\n address, postalCode, city, links.patientGroups[uri...]\n optional: dateOfBirth(YYYY-MM-DD), phone, mobilePhone, email, comment,\n dueDate, profilePicture, place, relatives[{firstName,lastName,relation,phone}]\n\nPUT /clinician/api/patients/{id}\n required: uniqueId, username, firstName, lastName, sex, address, postalCode, city,\n status(active|inactive|discharged|discharged_self_managed|deceased),\n links.patientGroups[uri...]\n optional: dateOfBirth, phone, mobilePhone, email, comment, dueDate, profilePicture,\n place, relatives[{firstName,lastName,relation,phone}]\n note: PUT shape differs from GET \u2014 GET returns patientGroups as nested objects and\n links with all navigation URIs; PUT expects only links.patientGroups as a flat\n array of URIs. Omit fields rather than sending null \u2014 some null values are\n stored as the string \"null\". uniqueId must be unique across the system;\n duplicates return 422 \"Patient not replaced\".\n\nPOST /clinician/api/clinicians\n required: username, firstName, lastName, links.patientGroups[uri...], links.roles[uri...]\n optional: email, phone, mobilePhone, uniqueId\n\nPOST /clinician/api/patient-notes\n required: note, type(normal|important), links.patient(uri)\n optional: reminderDate(date-time)\n\nPOST /clinician/api/patientgroups\n required: name, messagingEnabled, blueAlarmsEnabled, links.organization(uri)\n optional: dueDateVisible, calculateBloodPressureWeeklyAverage, copdPrediction\n\nPOST /clinician/api/patients/{id}/questionnaire_schedules\n required: links.questionnaireDefinition(uri)\n optional: scheduledTime (varies by type \u2014 WEEKDAYS, MONTHLY, EVERY_NTH_DAY, SPECIFIC_DATE, etc.)\n\nPOST /measurements/measurement-captures\n required: source, sourceId, captureTime(date-time), links.patient(uri),\n measurements[{type, timestamp, measurement:{<type-specific fields>},\n origin:{manualMeasurement:{enteredBy} | deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier}}}]\n optional: historic\n note: source+sourceId must be unique per capture\n\nPOST /measurements/measurement-streams\n required: inputType(e.g. \"discrete\"), links.patient(uri),\n measurements[{type, timestamp, measurement:{value,unit},\n origin:{deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier:{macAddress}}}}]\n\nPUT /results/results/{uuid}/acknowledgement\n required: note, date(date-time), visibleForPatient(bool)\n optional: closureNotes[string...]\n\nPUT /results/results/{uuid}/ignored\n required: replyIds[string...] (empty array to unignore)\n optional: reason\n\nPOST /chat/messages\n required: body, links.organization(uri)\n variant 1: links.patient(uri), optional links.attachments[uri...]\n variant 2: links.patientGroup(uri)\n\nPOST /calendar/events\n required: type, description, schedule.startTime(date-time),\n party.clinicians[{links.clinician(uri)}],\n party.patients[{links.patient(uri)}],\n links.origin(uri)\n optional: schedule.endTime(date-time), party.name\n\n## Notes\n- Dates use ISO 8601: 2024-01-01T00:00:00.000Z\n- Default max is 100, default offset is 0\n- All list endpoints support pagination via max/offset\n- Clinician-service resources use numeric {id}; other services use {uuid}\n- Links are always absolute URIs: https://doccla-dev.oth.io/clinician/api/patients/123\n- Schemas discoverable via each service's root links.schemas; filenames vary by endpoint\n";
1
+ export declare const API_GUIDE = "# OTH API Endpoint Reference\n\n## Key Concepts\n\nLinks are the API's primary navigation mechanism. Every response includes a\nlinks object with absolute URIs. Follow these rather than constructing URLs\nfrom IDs manually. Foreign key references (e.g. patient in a measurement,\npatientGroups in a patient) are always link URIs, not bare IDs.\n - In request bodies, use link URIs exactly as returned.\n - As the path for oth_get/oth_mutate, strip the origin and pass only the\n path: absolute URLs are rejected. e.g. a link of\n http://host.docker.internal:7100/clinician/api/patients/15\n is requested as path /clinician/api/patients/15\n\nAuth: the MCP server handles auth transparently. For reference, most read\nendpoints and questionnaire submission accept Basic auth. Acknowledgements,\nignored, and manual measurement-captures require a clinician-scoped Bearer JWT\nobtained via POST /idp2/tokens.\n\nActing user: every call runs as the MCP's configured user (oth_env shows the\nusername), and is bound by that user's roles and patient groups. GET\n/clinician/api/clinicians/me shows both. Patient reads and writes are limited\nto that user's patient groups: e.g. creating a patient in a group the user is\nnot in returns 422 \"Clinician does not have access to patient group\".\n\n## Common Workflows\n\nReview results:\n GET /clinician/api/patients?uniqueId={nhsNo}\n GET /results/results?patient={patient-link}&acknowledged=false\n GET {result-link}\n PUT {result-link}/acknowledgement (Bearer JWT required)\n\nSubmit questionnaire response:\n GET /clinician/api/questionnaire_definitions/{id}\n GET {links.activeQuestionnaire} (extract node names from response)\n POST {questionnaire-link}/results\n {version:\"1.0\", date:ISO-8601, links:{patient:uri},\n output:[{name:\"{nodeName}.FIELD\", type:\"Integer\"|\"Float\"|\"Object\"|\"Boolean\", value:<typed>}]}\n\nSubmit measurement (clinician-entered):\n POST /measurements/measurement-captures\n {source, sourceId, captureTime, links:{patient:uri},\n measurements:[{type, timestamp, measurement:{<type-specific>},\n origin:{manualMeasurement:{enteredBy} | deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier}}}]}\n Common types: blood_pressure({systolic,diastolic,unit}), pulse({value,unit}),\n saturation({value,unit}), temperature({value,unit}), weight(Float)\n\nPatient lifecycle:\n POST /clinician/api/patients (with links.patientGroups)\n GET+PUT /organizations/patients/{id}/metadata (PUT replaces entire set \u2014 GET first, merge, then PUT)\n PUT /clinician/api/patients/{id} (status:\"discharged\")\n\nSet up an organization, patient group and role:\n POST /organizations/organizations {name, metadataFields:[]}\n POST /clinician/api/patientgroups (links.organization from the org's links.self)\n POST /thresholds/patient-group-thresholds (optional, per measurement type)\n POST /idp2/roles {name, permissions:[\"ROLE_...\"]}\n PUT /clinician/api/clinicians/{me-id} add the new group to the acting user,\n so patients can be created in it\n\nCreate a clinician or patient that can log in:\n POST /clinician/api/clinicians or POST /clinician/api/patients\n \u2192 response includes temporaryPassword; the account cannot be used until\n its password is changed\n POST /idp2/users/auth as that user (see Users & IDP): NOT possible through\n the MCP, which always authenticates as its own user. Do it outside the MCP,\n with a single request: repeated failed logins lock the account.\n\nCreate and publish a questionnaire:\n POST /clinician/api/questionnaire_definitions {name, requiresManualInspection}\n \u2192 links.self ends in the definition's numeric id\n POST /clinician/api/questionnaire_definitions/{id}/draft\n {active:true, nodes, connections, links:{creator:clinician-uri}}\n \u2192 active:true publishes immediately (response has links.activeQuestionnaire);\n active:false leaves a draft (links.draftQuestionnaire)\n\nCheck or fix a user's login state (no login attempt needed):\n GET /idp2/users?url={clinician-or-patient-uri} \u2192 users[].links.user\n GET {links.user} accountLocked, temporaryPassword,\n passwordExpired, passwordLastChanged\n PUT {links.user}/locked {accountLocked:false}\n\n## Patients\nGET /clinician/api/patients ?uniqueId= &username= &firstName= &lastName= &search= &phone= &patientGroup= &id= &status= &select= &max= &offset=\nGET /clinician/api/patients/{id}\nGET /clinician/api/patients/me\nGET /clinician/api/patients/{id}/measurements ?from= &to= &max=\nGET /clinician/api/patients/{id}/measurements/aggregate\nGET /clinician/api/patients/{id}/measurement-types\nGET /clinician/api/patients/{id}/questionnaires\nGET /clinician/api/patients/{id}/questionnaire_schedules\nPOST /clinician/api/patients/{id}/questionnaire_schedules\nGET /clinician/api/patients/{id}/contact-info\nPUT /clinician/api/patients/{id}/contact-info\nGET /clinician/api/patients/{id}/acknowledgements\n status enum: all|active|inactive|discharged|discharged_self_managed|deceased (default: active)\n patientGroup and id are repeatable (up to 100\u00D7) for batch queries\n search matches across firstName, lastName, uniqueId, username\n\n## Clinicians\nGET /clinician/api/clinicians ?username= &max= &offset=\nPOST /clinician/api/clinicians\nGET /clinician/api/clinicians/{id}\nPUT /clinician/api/clinicians/{id}\nGET /clinician/api/clinicians/me\nGET /clinician/api/clinicians/me/colleagues\n\n## Questionnaire Definitions\nGET /clinician/api/questionnaire_definitions ?max= &offset=\nGET /clinician/api/questionnaire_definitions/{id}\nPOST /clinician/api/questionnaire_definitions {name, nameShownToPatient?, requiresManualInspection}\nPUT /clinician/api/questionnaire_definitions/{id}\nPOST /clinician/api/questionnaire_definitions/{id}/draft (new version; see Mutation Request Bodies)\n\n## Questionnaires (node graph)\nGET /clinician/api/questionnaires/{id}\nPOST /clinician/api/questionnaires/{id}/results\n\n## Results\nGET /results/results ?patient= &acknowledged= &resultType= &questionnaire= &order= &from= &to= &max=\nGET /results/results/{uuid}\nPUT /results/results/{uuid}/acknowledgement (Bearer JWT required)\nPUT /results/results/{uuid}/ignored (Bearer JWT required)\n\n## Questionnaire Schedules\nGET /clinician/api/questionnaire_schedules ?max= &offset=\n\n## Questionnaire Groups\nGET /questionnaires/questionnaire-groups ?max=\nPOST /questionnaires/questionnaire-groups {name, questionnaireDefinitions}\n\n## Patient Notes\nGET /clinician/api/patient-notes ?max= &offset=\nPOST /clinician/api/patient-notes\nGET /clinician/api/patient-notes/{id}\nPUT /clinician/api/patient-notes/{id}\nDELETE /clinician/api/patient-notes/{id}\nPOST /clinician/api/patient-notes/{id}/read\n\n## Measurements\nGET /measurements/measurement-types\nGET /measurements/measurement-captures ?patient= &source= &since= &from= &to= &max=\nPOST /measurements/measurement-captures\nGET /measurements/measurement-captures/{uuid}\nPOST /measurements/measurement-captures/{uuid}/measurements\nGET /measurements/measurement-streams ?patient= &patientGroup= &from= &to= &max=\nPOST /measurements/measurement-streams\nGET /measurements/measurement-streams/{uuid}\nGET /measurements/measurement-streams/{uuid}/measurements\nGET /measurements/measurement-streams/{uuid}/incidents\nGET /measurements/measurement-streams/incidents\nPOST /measurements/measurement-streams/acknowledgements\nGET /measurements/processors\nGET /measurements/processors/{name}\n\n## Thresholds\nGET /thresholds/patient-thresholds ?patient= &max=\nPOST /thresholds/patient-thresholds\nGET /thresholds/patient-thresholds/{uuid}\nPUT /thresholds/patient-thresholds/{uuid}\nDELETE /thresholds/patient-thresholds/{uuid}\nGET /thresholds/patient-group-thresholds ?max=\nPOST /thresholds/patient-group-thresholds\nGET /thresholds/patient-group-thresholds/{uuid}\nPUT /thresholds/patient-group-thresholds/{uuid}\nDELETE /thresholds/patient-group-thresholds/{uuid}\nPOST /thresholds/evaluation\n\n## Organizations\nGET /organizations/organizations\nPOST /organizations/organizations {name, metadataFields:[]}\nGET /organizations/organizations/{uuid}\nPUT /organizations/organizations/{uuid}\nGET /organizations/organizations/{uuid}/metadata-fields\nGET /organizations/patients/{id}/metadata\nPUT /organizations/patients/{id}/metadata (replaces entire set \u2014 GET first, merge, then PUT)\n\n## Object Storage\nPOST /object-storage/objects (raw body, set Content-Type header)\nGET /object-storage/objects/{uuid}\n\n## Users & IDP\nPOST /idp2/tokens (Basic auth, body: {links:{clinician:uri}}) \u2192 201 {token}\nGET /idp2/users ?url={clinician-or-patient-uri} (find the idp2 user behind a clinician/patient)\nGET /idp2/users/{id} includes accountLocked, temporaryPassword, passwordExpired\nPUT /idp2/users/{id}/locked {accountLocked:bool}\nDELETE /idp2/users/{id}/password\nPOST /idp2/users/auth change own password: Basic auth as the user with their\n current (e.g. temporary) password, body {password:new}\n \u2192 200. Not usable via the MCP (it authenticates as itself).\nGET /idp2/roles\nPOST /idp2/roles {name, permissions:[\"ROLE_PATIENT_READ\", ...]}\nGET /idp2/permissions permission names to use in roles\n\n Lockout: repeated failed Basic auth attempts lock the account (\"Account locked\n for user ...\"). Check state with GET /idp2/users/{id}, unlock with PUT .../locked.\n\n## Patient Groups\nGET /clinician/api/patientgroups ?max= &offset=\nGET /clinician/api/patientgroups/{id}\nPOST /clinician/api/patientgroups\nPUT /clinician/api/patientgroups/{id}\nDELETE /clinician/api/patientgroups/{id}\n\n## Audit\nGET /audit/resources/{resourceUrl} ?max= &offset=\n\n## Calendar\nGET /calendar/events ?patient= &from= &to=\nPOST /calendar/events\nGET /calendar/events/{uuid}\nPUT /calendar/events/{uuid}\nDELETE /calendar/events/{uuid}\n\n## Chat\nGET /chat/threads ?patient=\nGET /chat/threads/{uuid}\nGET /chat/threads/{uuid}/messages\nPOST /chat/threads/{uuid}/read\nPOST /chat/messages\nGET /chat/messages/{uuid}\nPOST /chat/messages/{uuid}/read\nGET /chat/snippets\nPOST /chat/snippets\nPUT /chat/snippets/{uuid}\nDELETE /chat/snippets/{uuid}\n\n## Notifications\nGET /notifications/devices ?user=\nPOST /notifications/devices\nGET /notifications/subscriptions\nPOST /notifications/subscriptions\nDELETE /notifications/subscriptions/{uuid}\n\n## Mutation Request Bodies\n\nPOST /clinician/api/patients\n required: uniqueId, username, firstName, lastName, sex(male|female|unknown),\n address, postalCode, city, links.patientGroups[uri...]\n optional: dateOfBirth(YYYY-MM-DD), phone, mobilePhone, email, comment,\n dueDate, profilePicture, place, relatives[{firstName,lastName,relation,phone}]\n note: the acting user must belong to every group in links.patientGroups (else 422).\n Response includes temporaryPassword (see Common Workflows).\n\nPUT /clinician/api/patients/{id}\n required: uniqueId, username, firstName, lastName, sex, address, postalCode, city,\n status(active|inactive|discharged|discharged_self_managed|deceased),\n links.patientGroups[uri...]\n optional: dateOfBirth, phone, mobilePhone, email, comment, dueDate, profilePicture,\n place, relatives[{firstName,lastName,relation,phone}]\n note: PUT shape differs from GET \u2014 GET returns patientGroups as nested objects and\n links with all navigation URIs; PUT expects only links.patientGroups as a flat\n array of URIs. Omit fields rather than sending null \u2014 some null values are\n stored as the string \"null\". uniqueId must be unique across the system;\n duplicates return 422 \"Patient not replaced\".\n\nPOST /clinician/api/clinicians\n required: username, firstName, lastName, links.patientGroups[uri...], links.roles[uri...]\n optional: email, phone, mobilePhone, uniqueId\n note: response includes temporaryPassword (see Common Workflows).\n\nPUT /clinician/api/clinicians/{id}\n send: firstName, lastName, username, email, uniqueId, phone, mobilePhone,\n links.patientGroups[uri...], links.roles[uri...]\n note: REPLACES the clinician. Any field left out is cleared (e.g. omitting\n uniqueId sets it to null). GET first and send every field back; to add\n a group, send the existing group URIs plus the new one (same for roles).\n\nPOST /thresholds/patient-group-thresholds\n required: measurementType(e.g. \"saturation\"), links.patientGroup(uri),\n limits[{name:\"value\", warningLow, warningHigh, alertLow, alertHigh}] (null for unused),\n thresholdType:{scope:\"point\", point:{type:\"absolute\"}}\n\nPOST /clinician/api/questionnaire_definitions/{id}/draft\n required: active(bool: true publishes), nodes, connections\n optional: links.creator(clinician uri)\n nodes: object keyed by node id, e.g.\n {\"start1\":{id:\"start1\", type:\"start\", position:{top,left}},\n \"input2\":{id:\"input2\", type:\"input\", question, shortText, dataType:\"BOOLEAN\", position},\n \"end5\":{id:\"end5\", type:\"end\", position}}\n connections: [{source, target, type:\"normal\"|\"choice\", choiceValue?, severity:\"\"|\"RED\"|..., frozen?}]\n note: this is the questionnaire editor's graph format (what its Export produces).\n GET {links.activeQuestionnaire} returns a different, compiled form\n (IONode/AssignmentNode/EndNode, nodes renumbered, e.g. \"input2\" becomes\n \"31\") that cannot be posted back as a draft. Take output names for\n result submission (e.g. \"31.FIELD\") from the compiled form.\n\nPOST /clinician/api/patient-notes\n required: note, type(normal|important), links.patient(uri)\n optional: reminderDate(date-time)\n\nPOST /clinician/api/patientgroups\n required: name, messagingEnabled, blueAlarmsEnabled, links.organization(uri)\n optional: dueDateVisible, calculateBloodPressureWeeklyAverage, copdPrediction\n\nPOST /clinician/api/patients/{id}/questionnaire_schedules\n required: links.questionnaireDefinition(uri)\n optional: scheduledTime (varies by type \u2014 WEEKDAYS, MONTHLY, EVERY_NTH_DAY, SPECIFIC_DATE, etc.)\n\nPOST /measurements/measurement-captures\n required: source, sourceId, captureTime(date-time), links.patient(uri),\n measurements[{type, timestamp, measurement:{<type-specific fields>},\n origin:{manualMeasurement:{enteredBy} | deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier}}}]\n optional: historic\n note: source+sourceId must be unique per capture\n\nPOST /measurements/measurement-streams\n required: inputType(e.g. \"discrete\"), links.patient(uri),\n measurements[{type, timestamp, measurement:{value,unit},\n origin:{deviceMeasurement:{connectionType,manufacturer,model,primaryDeviceIdentifier:{macAddress}}}}]\n\nPUT /results/results/{uuid}/acknowledgement\n required: note, date(date-time), visibleForPatient(bool)\n optional: closureNotes[string...]\n\nPUT /results/results/{uuid}/ignored\n required: replyIds[string...] (empty array to unignore)\n optional: reason\n\nPOST /chat/messages\n required: body, links.organization(uri)\n variant 1: links.patient(uri), optional links.attachments[uri...]\n variant 2: links.patientGroup(uri)\n\nPOST /calendar/events\n required: type, description, schedule.startTime(date-time),\n party.clinicians[{links.clinician(uri)}],\n party.patients[{links.patient(uri)}],\n links.origin(uri)\n optional: schedule.endTime(date-time), party.name\n\n## Notes\n- Dates use ISO 8601: 2024-01-01T00:00:00.000Z\n- Default max is 100, default offset is 0\n- All list endpoints support pagination via max/offset\n- Clinician-service resources use numeric {id}; other services use {uuid}\n- Links are always absolute URIs: https://doccla-dev.oth.io/clinician/api/patients/123\n (on local they use the gateway's external host, e.g. http://host.docker.internal:7100,\n which can differ from the MCP's base URL; that is fine, use them as described above)\n- Schemas discoverable via each service's root links.schemas; filenames vary by endpoint\n";
package/dist/api-guide.js CHANGED
@@ -3,15 +3,26 @@ export const API_GUIDE = `# OTH API Endpoint Reference
3
3
  ## Key Concepts
4
4
 
5
5
  Links are the API's primary navigation mechanism. Every response includes a
6
- links object with absolute URIs. Use these as-is for subsequent requests —
7
- never construct URLs from IDs manually. Foreign key references (e.g. patient
8
- in a measurement, patientGroups in a patient) are always link URIs, not bare IDs.
6
+ links object with absolute URIs. Follow these rather than constructing URLs
7
+ from IDs manually. Foreign key references (e.g. patient in a measurement,
8
+ patientGroups in a patient) are always link URIs, not bare IDs.
9
+ - In request bodies, use link URIs exactly as returned.
10
+ - As the path for oth_get/oth_mutate, strip the origin and pass only the
11
+ path: absolute URLs are rejected. e.g. a link of
12
+ http://host.docker.internal:7100/clinician/api/patients/15
13
+ is requested as path /clinician/api/patients/15
9
14
 
10
15
  Auth: the MCP server handles auth transparently. For reference, most read
11
16
  endpoints and questionnaire submission accept Basic auth. Acknowledgements,
12
17
  ignored, and manual measurement-captures require a clinician-scoped Bearer JWT
13
18
  obtained via POST /idp2/tokens.
14
19
 
20
+ Acting user: every call runs as the MCP's configured user (oth_env shows the
21
+ username), and is bound by that user's roles and patient groups. GET
22
+ /clinician/api/clinicians/me shows both. Patient reads and writes are limited
23
+ to that user's patient groups: e.g. creating a patient in a group the user is
24
+ not in returns 422 "Clinician does not have access to patient group".
25
+
15
26
  ## Common Workflows
16
27
 
17
28
  Review results:
@@ -40,6 +51,36 @@ Patient lifecycle:
40
51
  GET+PUT /organizations/patients/{id}/metadata (PUT replaces entire set — GET first, merge, then PUT)
41
52
  PUT /clinician/api/patients/{id} (status:"discharged")
42
53
 
54
+ Set up an organization, patient group and role:
55
+ POST /organizations/organizations {name, metadataFields:[]}
56
+ POST /clinician/api/patientgroups (links.organization from the org's links.self)
57
+ POST /thresholds/patient-group-thresholds (optional, per measurement type)
58
+ POST /idp2/roles {name, permissions:["ROLE_..."]}
59
+ PUT /clinician/api/clinicians/{me-id} add the new group to the acting user,
60
+ so patients can be created in it
61
+
62
+ Create a clinician or patient that can log in:
63
+ POST /clinician/api/clinicians or POST /clinician/api/patients
64
+ → response includes temporaryPassword; the account cannot be used until
65
+ its password is changed
66
+ POST /idp2/users/auth as that user (see Users & IDP): NOT possible through
67
+ the MCP, which always authenticates as its own user. Do it outside the MCP,
68
+ with a single request: repeated failed logins lock the account.
69
+
70
+ Create and publish a questionnaire:
71
+ POST /clinician/api/questionnaire_definitions {name, requiresManualInspection}
72
+ → links.self ends in the definition's numeric id
73
+ POST /clinician/api/questionnaire_definitions/{id}/draft
74
+ {active:true, nodes, connections, links:{creator:clinician-uri}}
75
+ → active:true publishes immediately (response has links.activeQuestionnaire);
76
+ active:false leaves a draft (links.draftQuestionnaire)
77
+
78
+ Check or fix a user's login state (no login attempt needed):
79
+ GET /idp2/users?url={clinician-or-patient-uri} → users[].links.user
80
+ GET {links.user} accountLocked, temporaryPassword,
81
+ passwordExpired, passwordLastChanged
82
+ PUT {links.user}/locked {accountLocked:false}
83
+
43
84
  ## Patients
44
85
  GET /clinician/api/patients ?uniqueId= &username= &firstName= &lastName= &search= &phone= &patientGroup= &id= &status= &select= &max= &offset=
45
86
  GET /clinician/api/patients/{id}
@@ -58,7 +99,7 @@ GET /clinician/api/patients/{id}/acknowledgements
58
99
  search matches across firstName, lastName, uniqueId, username
59
100
 
60
101
  ## Clinicians
61
- GET /clinician/api/clinicians ?max= &offset=
102
+ GET /clinician/api/clinicians ?username= &max= &offset=
62
103
  POST /clinician/api/clinicians
63
104
  GET /clinician/api/clinicians/{id}
64
105
  PUT /clinician/api/clinicians/{id}
@@ -68,8 +109,9 @@ GET /clinician/api/clinicians/me/colleagues
68
109
  ## Questionnaire Definitions
69
110
  GET /clinician/api/questionnaire_definitions ?max= &offset=
70
111
  GET /clinician/api/questionnaire_definitions/{id}
71
- POST /clinician/api/questionnaire_definitions {name, requiresManualInspection}
112
+ POST /clinician/api/questionnaire_definitions {name, nameShownToPatient?, requiresManualInspection}
72
113
  PUT /clinician/api/questionnaire_definitions/{id}
114
+ POST /clinician/api/questionnaire_definitions/{id}/draft (new version; see Mutation Request Bodies)
73
115
 
74
116
  ## Questionnaires (node graph)
75
117
  GET /clinician/api/questionnaires/{id}
@@ -127,6 +169,7 @@ POST /thresholds/evaluation
127
169
 
128
170
  ## Organizations
129
171
  GET /organizations/organizations
172
+ POST /organizations/organizations {name, metadataFields:[]}
130
173
  GET /organizations/organizations/{uuid}
131
174
  PUT /organizations/organizations/{uuid}
132
175
  GET /organizations/organizations/{uuid}/metadata-fields
@@ -139,11 +182,19 @@ GET /object-storage/objects/{uuid}
139
182
 
140
183
  ## Users & IDP
141
184
  POST /idp2/tokens (Basic auth, body: {links:{clinician:uri}}) → 201 {token}
142
- GET /idp2/users/{id}
143
- PUT /idp2/users/{id}/locked
185
+ GET /idp2/users ?url={clinician-or-patient-uri} (find the idp2 user behind a clinician/patient)
186
+ GET /idp2/users/{id} includes accountLocked, temporaryPassword, passwordExpired
187
+ PUT /idp2/users/{id}/locked {accountLocked:bool}
144
188
  DELETE /idp2/users/{id}/password
189
+ POST /idp2/users/auth change own password: Basic auth as the user with their
190
+ current (e.g. temporary) password, body {password:new}
191
+ → 200. Not usable via the MCP (it authenticates as itself).
145
192
  GET /idp2/roles
146
- GET /idp2/permissions
193
+ POST /idp2/roles {name, permissions:["ROLE_PATIENT_READ", ...]}
194
+ GET /idp2/permissions permission names to use in roles
195
+
196
+ Lockout: repeated failed Basic auth attempts lock the account ("Account locked
197
+ for user ..."). Check state with GET /idp2/users/{id}, unlock with PUT .../locked.
147
198
 
148
199
  ## Patient Groups
149
200
  GET /clinician/api/patientgroups ?max= &offset=
@@ -189,6 +240,8 @@ POST /clinician/api/patients
189
240
  address, postalCode, city, links.patientGroups[uri...]
190
241
  optional: dateOfBirth(YYYY-MM-DD), phone, mobilePhone, email, comment,
191
242
  dueDate, profilePicture, place, relatives[{firstName,lastName,relation,phone}]
243
+ note: the acting user must belong to every group in links.patientGroups (else 422).
244
+ Response includes temporaryPassword (see Common Workflows).
192
245
 
193
246
  PUT /clinician/api/patients/{id}
194
247
  required: uniqueId, username, firstName, lastName, sex, address, postalCode, city,
@@ -205,6 +258,33 @@ PUT /clinician/api/patients/{id}
205
258
  POST /clinician/api/clinicians
206
259
  required: username, firstName, lastName, links.patientGroups[uri...], links.roles[uri...]
207
260
  optional: email, phone, mobilePhone, uniqueId
261
+ note: response includes temporaryPassword (see Common Workflows).
262
+
263
+ PUT /clinician/api/clinicians/{id}
264
+ send: firstName, lastName, username, email, uniqueId, phone, mobilePhone,
265
+ links.patientGroups[uri...], links.roles[uri...]
266
+ note: REPLACES the clinician. Any field left out is cleared (e.g. omitting
267
+ uniqueId sets it to null). GET first and send every field back; to add
268
+ a group, send the existing group URIs plus the new one (same for roles).
269
+
270
+ POST /thresholds/patient-group-thresholds
271
+ required: measurementType(e.g. "saturation"), links.patientGroup(uri),
272
+ limits[{name:"value", warningLow, warningHigh, alertLow, alertHigh}] (null for unused),
273
+ thresholdType:{scope:"point", point:{type:"absolute"}}
274
+
275
+ POST /clinician/api/questionnaire_definitions/{id}/draft
276
+ required: active(bool: true publishes), nodes, connections
277
+ optional: links.creator(clinician uri)
278
+ nodes: object keyed by node id, e.g.
279
+ {"start1":{id:"start1", type:"start", position:{top,left}},
280
+ "input2":{id:"input2", type:"input", question, shortText, dataType:"BOOLEAN", position},
281
+ "end5":{id:"end5", type:"end", position}}
282
+ connections: [{source, target, type:"normal"|"choice", choiceValue?, severity:""|"RED"|..., frozen?}]
283
+ note: this is the questionnaire editor's graph format (what its Export produces).
284
+ GET {links.activeQuestionnaire} returns a different, compiled form
285
+ (IONode/AssignmentNode/EndNode, nodes renumbered, e.g. "input2" becomes
286
+ "31") that cannot be posted back as a draft. Take output names for
287
+ result submission (e.g. "31.FIELD") from the compiled form.
208
288
 
209
289
  POST /clinician/api/patient-notes
210
290
  required: note, type(normal|important), links.patient(uri)
@@ -256,5 +336,7 @@ POST /calendar/events
256
336
  - All list endpoints support pagination via max/offset
257
337
  - Clinician-service resources use numeric {id}; other services use {uuid}
258
338
  - Links are always absolute URIs: https://doccla-dev.oth.io/clinician/api/patients/123
339
+ (on local they use the gateway's external host, e.g. http://host.docker.internal:7100,
340
+ which can differ from the MCP's base URL; that is fine, use them as described above)
259
341
  - Schemas discoverable via each service's root links.schemas; filenames vary by endpoint
260
342
  `;
package/dist/auth.d.ts CHANGED
@@ -11,9 +11,7 @@ export declare class AuthManager {
11
11
  get environment(): Environment;
12
12
  get baseUrl(): string;
13
13
  get isAuthenticated(): boolean;
14
- get userInfo(): {
15
- username: string | null;
16
- } | null;
14
+ configuredUsername(): Promise<string | null>;
17
15
  switchEnvironment(env: Environment): void;
18
16
  getToken(): Promise<string>;
19
17
  private isExpiringSoon;
package/dist/auth.js CHANGED
@@ -35,10 +35,16 @@ export class AuthManager {
35
35
  get isAuthenticated() {
36
36
  return this.state !== null;
37
37
  }
38
- get userInfo() {
39
- if (!this.state)
38
+ // The user calls will run as, without authenticating. Null if no credentials are configured.
39
+ async configuredUsername() {
40
+ if (this.state)
41
+ return this.state.username;
42
+ try {
43
+ return (await this.resolveCredentials()).username;
44
+ }
45
+ catch {
40
46
  return null;
41
- return { username: this.state.username };
47
+ }
42
48
  }
43
49
  switchEnvironment(env) {
44
50
  if (env === this.env)
package/dist/client.d.ts CHANGED
@@ -3,7 +3,7 @@ export declare class OthClient {
3
3
  private auth;
4
4
  constructor(auth: AuthManager);
5
5
  get(path: string, query?: Record<string, string>): Promise<unknown>;
6
- mutate(method: "POST" | "PUT" | "PATCH" | "DELETE", path: string, body?: Record<string, unknown>): Promise<unknown>;
6
+ mutate(method: "POST" | "PUT" | "PATCH" | "DELETE", path: string, body?: Record<string, unknown> | unknown[]): Promise<unknown>;
7
7
  private validatePath;
8
8
  private buildUrl;
9
9
  private processResponse;
package/dist/index.js CHANGED
File without changes
package/dist/tools.js CHANGED
@@ -89,9 +89,9 @@ Read the oth://api-guide resource for full endpoint reference.`,
89
89
  .describe("HTTP method"),
90
90
  path: z.string().describe("API path"),
91
91
  body: z
92
- .record(z.unknown())
92
+ .union([z.record(z.unknown()), z.array(z.unknown())])
93
93
  .optional()
94
- .describe("Request body (JSON)"),
94
+ .describe("Request body (JSON object or array)"),
95
95
  },
96
96
  }, async ({ method, path, body }) => {
97
97
  try {
@@ -124,7 +124,7 @@ Read the oth://api-guide resource for full endpoint reference.`,
124
124
  });
125
125
  server.registerTool("oth_env", {
126
126
  title: "OTH Environment",
127
- description: "Switch OTH environment or show current state. Returns environment, base URL, and authenticated user info.",
127
+ description: "Switch OTH environment or show current state. Returns environment, base URL, whether a login has happened yet, and the username every call runs as (known before the first login). Calls are limited to that user's roles and patient groups.",
128
128
  annotations: {
129
129
  readOnlyHint: false,
130
130
  destructiveHint: false,
@@ -145,10 +145,8 @@ Read the oth://api-guide resource for full endpoint reference.`,
145
145
  environment: auth.environment,
146
146
  baseUrl: auth.baseUrl,
147
147
  authenticated: auth.isAuthenticated,
148
+ user: { username: await auth.configuredUsername() },
148
149
  };
149
- if (auth.isAuthenticated) {
150
- info.user = auth.userInfo;
151
- }
152
150
  return {
153
151
  content: [
154
152
  { type: "text", text: JSON.stringify(info, null, 2) },
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "oth-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "MCP server for the OTH (OpenTeleHealth) API",
5
5
  "type": "module",
6
6
  "bin": {
7
- "oth-mcp": "./dist/index.js"
7
+ "oth-mcp": "dist/index.js"
8
8
  },
9
9
  "files": [
10
10
  "dist"