oth-mcp 0.1.0
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 +88 -0
- package/dist/api-guide.d.ts +1 -0
- package/dist/api-guide.js +187 -0
- package/dist/auth.d.ts +26 -0
- package/dist/auth.js +192 -0
- package/dist/client.d.ts +11 -0
- package/dist/client.js +90 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +150 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# oth-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for the [OTH (OpenTeleHealth) API](https://doccla-dev.oth.io/api/docs). Gives Claude direct access to query and manage OTH resources across environments.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### 1. Configure credentials
|
|
8
|
+
|
|
9
|
+
**Environment variables** (works everywhere):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export OTH_USERNAME="your-username"
|
|
13
|
+
export OTH_PASSWORD="your-password"
|
|
14
|
+
export OTH_ENV="dev" # optional, defaults to dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**macOS Keychain** (no plaintext on disk):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# For each environment (dev, stag, demo):
|
|
21
|
+
security add-generic-password -a "oth-dev" -s "oth-mcp-username" -w
|
|
22
|
+
security add-generic-password -a "oth-dev" -s "oth-mcp-password" -w
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Add to Claude Code
|
|
26
|
+
|
|
27
|
+
Add to your Claude Code MCP settings (`~/.claude/settings.json` or project `.claude/settings.json`):
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"mcpServers": {
|
|
32
|
+
"oth": {
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "oth-mcp"]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Or with inline credentials:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"oth": {
|
|
46
|
+
"command": "npx",
|
|
47
|
+
"args": ["-y", "oth-mcp"],
|
|
48
|
+
"env": {
|
|
49
|
+
"OTH_USERNAME": "your-username",
|
|
50
|
+
"OTH_PASSWORD": "your-password",
|
|
51
|
+
"OTH_ENV": "dev"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Tools
|
|
59
|
+
|
|
60
|
+
### `oth_get`
|
|
61
|
+
|
|
62
|
+
GET any OTH API endpoint. Common paths are listed in the tool description; read the `oth://api-guide` resource for the full reference.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
oth_get({ path: "/clinician/api/patients", query: { max: "10" } })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### `oth_mutate`
|
|
69
|
+
|
|
70
|
+
POST, PUT, PATCH, or DELETE to any OTH API endpoint.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
oth_mutate({ method: "POST", path: "/clinician/api/patient-notes", body: { note: "Test", type: "normal", links: { patient: "https://doccla-dev.oth.io/clinician/api/patients/123" } } })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `oth_env`
|
|
77
|
+
|
|
78
|
+
Switch environment or check current state.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
oth_env({ environment: "stag" })
|
|
82
|
+
oth_env({}) // show current env + auth state
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Limitations
|
|
86
|
+
|
|
87
|
+
- **MFA not supported** — accounts with MFA enabled will fail to authenticate. Use an account without MFA.
|
|
88
|
+
- **macOS Keychain only on macOS** — Linux/Windows users must use environment variables.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const API_GUIDE = "# OTH API Endpoint Reference\n\n## Patients\nGET /clinician/api/patients ?username= &firstName= &lastName= &uniqueId= &status= &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-results ?from= &to= &max=\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\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## Questionnaire Results\nGET /clinician/api/questionnaire-results ?from= &to= &max= &offset=\nGET /clinician/api/questionnaire-results/{id}\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= &from= &to= &max=\nPOST /measurements/measurement-captures\nGET /measurements/measurement-captures/{uuid}\nPOST /measurements/measurement-captures/{uuid}/measurements\nGET /measurements/measurement-streams ?patient= &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\n\n## Users & IDP\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 same fields as create, all optional except links.patientGroups\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), measurements[], links.patient(uri)\n optional: historic\n note: source+sourceId must be unique per capture\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";
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
export const API_GUIDE = `# OTH API Endpoint Reference
|
|
2
|
+
|
|
3
|
+
## Patients
|
|
4
|
+
GET /clinician/api/patients ?username= &firstName= &lastName= &uniqueId= &status= &max= &offset=
|
|
5
|
+
GET /clinician/api/patients/{id}
|
|
6
|
+
GET /clinician/api/patients/me
|
|
7
|
+
GET /clinician/api/patients/{id}/measurements ?from= &to= &max=
|
|
8
|
+
GET /clinician/api/patients/{id}/measurements/aggregate
|
|
9
|
+
GET /clinician/api/patients/{id}/measurement-types
|
|
10
|
+
GET /clinician/api/patients/{id}/questionnaires
|
|
11
|
+
GET /clinician/api/patients/{id}/questionnaire-results ?from= &to= &max=
|
|
12
|
+
GET /clinician/api/patients/{id}/questionnaire_schedules
|
|
13
|
+
POST /clinician/api/patients/{id}/questionnaire_schedules
|
|
14
|
+
GET /clinician/api/patients/{id}/contact-info
|
|
15
|
+
PUT /clinician/api/patients/{id}/contact-info
|
|
16
|
+
GET /clinician/api/patients/{id}/acknowledgements
|
|
17
|
+
|
|
18
|
+
## Clinicians
|
|
19
|
+
GET /clinician/api/clinicians ?max= &offset=
|
|
20
|
+
POST /clinician/api/clinicians
|
|
21
|
+
GET /clinician/api/clinicians/{id}
|
|
22
|
+
PUT /clinician/api/clinicians/{id}
|
|
23
|
+
GET /clinician/api/clinicians/me
|
|
24
|
+
GET /clinician/api/clinicians/me/colleagues
|
|
25
|
+
|
|
26
|
+
## Questionnaire Definitions
|
|
27
|
+
GET /clinician/api/questionnaire_definitions ?max= &offset=
|
|
28
|
+
GET /clinician/api/questionnaire_definitions/{id}
|
|
29
|
+
POST /clinician/api/questionnaire_definitions {name, requiresManualInspection}
|
|
30
|
+
PUT /clinician/api/questionnaire_definitions/{id}
|
|
31
|
+
|
|
32
|
+
## Questionnaires (node graph)
|
|
33
|
+
GET /clinician/api/questionnaires/{id}
|
|
34
|
+
POST /clinician/api/questionnaires/{id}/results
|
|
35
|
+
|
|
36
|
+
## Questionnaire Results
|
|
37
|
+
GET /clinician/api/questionnaire-results ?from= &to= &max= &offset=
|
|
38
|
+
GET /clinician/api/questionnaire-results/{id}
|
|
39
|
+
|
|
40
|
+
## Questionnaire Schedules
|
|
41
|
+
GET /clinician/api/questionnaire_schedules ?max= &offset=
|
|
42
|
+
|
|
43
|
+
## Questionnaire Groups
|
|
44
|
+
GET /questionnaires/questionnaire-groups ?max=
|
|
45
|
+
POST /questionnaires/questionnaire-groups {name, questionnaireDefinitions}
|
|
46
|
+
|
|
47
|
+
## Patient Notes
|
|
48
|
+
GET /clinician/api/patient-notes ?max= &offset=
|
|
49
|
+
POST /clinician/api/patient-notes
|
|
50
|
+
GET /clinician/api/patient-notes/{id}
|
|
51
|
+
PUT /clinician/api/patient-notes/{id}
|
|
52
|
+
DELETE /clinician/api/patient-notes/{id}
|
|
53
|
+
POST /clinician/api/patient-notes/{id}/read
|
|
54
|
+
|
|
55
|
+
## Measurements
|
|
56
|
+
GET /measurements/measurement-types
|
|
57
|
+
GET /measurements/measurement-captures ?patient= &from= &to= &max=
|
|
58
|
+
POST /measurements/measurement-captures
|
|
59
|
+
GET /measurements/measurement-captures/{uuid}
|
|
60
|
+
POST /measurements/measurement-captures/{uuid}/measurements
|
|
61
|
+
GET /measurements/measurement-streams ?patient= &from= &to= &max=
|
|
62
|
+
POST /measurements/measurement-streams
|
|
63
|
+
GET /measurements/measurement-streams/{uuid}
|
|
64
|
+
GET /measurements/measurement-streams/{uuid}/measurements
|
|
65
|
+
GET /measurements/measurement-streams/{uuid}/incidents
|
|
66
|
+
GET /measurements/measurement-streams/incidents
|
|
67
|
+
POST /measurements/measurement-streams/acknowledgements
|
|
68
|
+
GET /measurements/processors
|
|
69
|
+
GET /measurements/processors/{name}
|
|
70
|
+
|
|
71
|
+
## Thresholds
|
|
72
|
+
GET /thresholds/patient-thresholds ?patient= &max=
|
|
73
|
+
POST /thresholds/patient-thresholds
|
|
74
|
+
GET /thresholds/patient-thresholds/{uuid}
|
|
75
|
+
PUT /thresholds/patient-thresholds/{uuid}
|
|
76
|
+
DELETE /thresholds/patient-thresholds/{uuid}
|
|
77
|
+
GET /thresholds/patient-group-thresholds ?max=
|
|
78
|
+
POST /thresholds/patient-group-thresholds
|
|
79
|
+
GET /thresholds/patient-group-thresholds/{uuid}
|
|
80
|
+
PUT /thresholds/patient-group-thresholds/{uuid}
|
|
81
|
+
DELETE /thresholds/patient-group-thresholds/{uuid}
|
|
82
|
+
POST /thresholds/evaluation
|
|
83
|
+
|
|
84
|
+
## Organizations
|
|
85
|
+
GET /organizations/organizations
|
|
86
|
+
GET /organizations/organizations/{uuid}
|
|
87
|
+
PUT /organizations/organizations/{uuid}
|
|
88
|
+
GET /organizations/organizations/{uuid}/metadata-fields
|
|
89
|
+
GET /organizations/patients/{id}/metadata
|
|
90
|
+
PUT /organizations/patients/{id}/metadata
|
|
91
|
+
|
|
92
|
+
## Users & IDP
|
|
93
|
+
GET /idp2/users/{id}
|
|
94
|
+
PUT /idp2/users/{id}/locked
|
|
95
|
+
DELETE /idp2/users/{id}/password
|
|
96
|
+
GET /idp2/roles
|
|
97
|
+
GET /idp2/permissions
|
|
98
|
+
|
|
99
|
+
## Patient Groups
|
|
100
|
+
GET /clinician/api/patientgroups ?max= &offset=
|
|
101
|
+
GET /clinician/api/patientgroups/{id}
|
|
102
|
+
POST /clinician/api/patientgroups
|
|
103
|
+
PUT /clinician/api/patientgroups/{id}
|
|
104
|
+
DELETE /clinician/api/patientgroups/{id}
|
|
105
|
+
|
|
106
|
+
## Audit
|
|
107
|
+
GET /audit/resources/{resourceUrl} ?max= &offset=
|
|
108
|
+
|
|
109
|
+
## Calendar
|
|
110
|
+
GET /calendar/events ?patient= &from= &to=
|
|
111
|
+
POST /calendar/events
|
|
112
|
+
GET /calendar/events/{uuid}
|
|
113
|
+
PUT /calendar/events/{uuid}
|
|
114
|
+
DELETE /calendar/events/{uuid}
|
|
115
|
+
|
|
116
|
+
## Chat
|
|
117
|
+
GET /chat/threads ?patient=
|
|
118
|
+
GET /chat/threads/{uuid}
|
|
119
|
+
GET /chat/threads/{uuid}/messages
|
|
120
|
+
POST /chat/threads/{uuid}/read
|
|
121
|
+
POST /chat/messages
|
|
122
|
+
GET /chat/messages/{uuid}
|
|
123
|
+
POST /chat/messages/{uuid}/read
|
|
124
|
+
GET /chat/snippets
|
|
125
|
+
POST /chat/snippets
|
|
126
|
+
PUT /chat/snippets/{uuid}
|
|
127
|
+
DELETE /chat/snippets/{uuid}
|
|
128
|
+
|
|
129
|
+
## Notifications
|
|
130
|
+
GET /notifications/devices ?user=
|
|
131
|
+
POST /notifications/devices
|
|
132
|
+
GET /notifications/subscriptions
|
|
133
|
+
POST /notifications/subscriptions
|
|
134
|
+
DELETE /notifications/subscriptions/{uuid}
|
|
135
|
+
|
|
136
|
+
## Mutation Request Bodies
|
|
137
|
+
|
|
138
|
+
POST /clinician/api/patients
|
|
139
|
+
required: uniqueId, username, firstName, lastName, sex(male|female|unknown),
|
|
140
|
+
address, postalCode, city, links.patientGroups[uri...]
|
|
141
|
+
optional: dateOfBirth(YYYY-MM-DD), phone, mobilePhone, email, comment,
|
|
142
|
+
dueDate, profilePicture, place, relatives[{firstName,lastName,relation,phone}]
|
|
143
|
+
|
|
144
|
+
PUT /clinician/api/patients/{id}
|
|
145
|
+
same fields as create, all optional except links.patientGroups
|
|
146
|
+
|
|
147
|
+
POST /clinician/api/clinicians
|
|
148
|
+
required: username, firstName, lastName, links.patientGroups[uri...], links.roles[uri...]
|
|
149
|
+
optional: email, phone, mobilePhone, uniqueId
|
|
150
|
+
|
|
151
|
+
POST /clinician/api/patient-notes
|
|
152
|
+
required: note, type(normal|important), links.patient(uri)
|
|
153
|
+
optional: reminderDate(date-time)
|
|
154
|
+
|
|
155
|
+
POST /clinician/api/patientgroups
|
|
156
|
+
required: name, messagingEnabled, blueAlarmsEnabled, links.organization(uri)
|
|
157
|
+
optional: dueDateVisible, calculateBloodPressureWeeklyAverage, copdPrediction
|
|
158
|
+
|
|
159
|
+
POST /clinician/api/patients/{id}/questionnaire_schedules
|
|
160
|
+
required: links.questionnaireDefinition(uri)
|
|
161
|
+
optional: scheduledTime (varies by type — WEEKDAYS, MONTHLY, EVERY_NTH_DAY, SPECIFIC_DATE, etc.)
|
|
162
|
+
|
|
163
|
+
POST /measurements/measurement-captures
|
|
164
|
+
required: source, sourceId, captureTime(date-time), measurements[], links.patient(uri)
|
|
165
|
+
optional: historic
|
|
166
|
+
note: source+sourceId must be unique per capture
|
|
167
|
+
|
|
168
|
+
POST /chat/messages
|
|
169
|
+
required: body, links.organization(uri)
|
|
170
|
+
variant 1: links.patient(uri), optional links.attachments[uri...]
|
|
171
|
+
variant 2: links.patientGroup(uri)
|
|
172
|
+
|
|
173
|
+
POST /calendar/events
|
|
174
|
+
required: type, description, schedule.startTime(date-time),
|
|
175
|
+
party.clinicians[{links.clinician(uri)}],
|
|
176
|
+
party.patients[{links.patient(uri)}],
|
|
177
|
+
links.origin(uri)
|
|
178
|
+
optional: schedule.endTime(date-time), party.name
|
|
179
|
+
|
|
180
|
+
## Notes
|
|
181
|
+
- Dates use ISO 8601: 2024-01-01T00:00:00.000Z
|
|
182
|
+
- Default max is 100, default offset is 0
|
|
183
|
+
- All list endpoints support pagination via max/offset
|
|
184
|
+
- Clinician-service resources use numeric {id}; other services use {uuid}
|
|
185
|
+
- Links are always absolute URIs: https://doccla-dev.oth.io/clinician/api/patients/123
|
|
186
|
+
- Schemas discoverable via each service's root links.schemas; filenames vary by endpoint
|
|
187
|
+
`;
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type Environment = string;
|
|
2
|
+
interface Credentials {
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class AuthManager {
|
|
7
|
+
private env;
|
|
8
|
+
private state;
|
|
9
|
+
private fetchFn;
|
|
10
|
+
constructor(env?: Environment, fetchFn?: typeof fetch);
|
|
11
|
+
get environment(): Environment;
|
|
12
|
+
get baseUrl(): string;
|
|
13
|
+
get isAuthenticated(): boolean;
|
|
14
|
+
get userInfo(): {
|
|
15
|
+
username: string | null;
|
|
16
|
+
} | null;
|
|
17
|
+
switchEnvironment(env: Environment): void;
|
|
18
|
+
getToken(): Promise<string>;
|
|
19
|
+
private isExpiringSoon;
|
|
20
|
+
private authenticate;
|
|
21
|
+
private refresh;
|
|
22
|
+
private handleAuthError;
|
|
23
|
+
private extractTokenVerifier;
|
|
24
|
+
resolveCredentials(): Promise<Credentials>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
function envBaseUrl(env) {
|
|
5
|
+
return `https://doccla-${env}.oth.io`;
|
|
6
|
+
}
|
|
7
|
+
const REFRESH_BUFFER_SECONDS = 60;
|
|
8
|
+
const AUTH_ERROR_MESSAGES = {
|
|
9
|
+
locked: "Account is locked. Contact your administrator.",
|
|
10
|
+
inactive: "Account is deactivated.",
|
|
11
|
+
invalid: "Invalid credentials. Check your username and password.",
|
|
12
|
+
missing: "User not found.",
|
|
13
|
+
unknown: "User not found.",
|
|
14
|
+
expired: "Password has expired. Reset your password via the OTH web UI.",
|
|
15
|
+
pincode_required: "MFA is enabled on this account. The MCP server does not support MFA. " +
|
|
16
|
+
"Use an account without MFA enabled, or disable MFA for this account.",
|
|
17
|
+
};
|
|
18
|
+
export class AuthManager {
|
|
19
|
+
env;
|
|
20
|
+
state = null;
|
|
21
|
+
fetchFn;
|
|
22
|
+
constructor(env, fetchFn) {
|
|
23
|
+
this.env = env ?? process.env.OTH_ENV ?? "dev";
|
|
24
|
+
this.fetchFn = fetchFn ?? fetch;
|
|
25
|
+
}
|
|
26
|
+
get environment() {
|
|
27
|
+
return this.env;
|
|
28
|
+
}
|
|
29
|
+
get baseUrl() {
|
|
30
|
+
return envBaseUrl(this.env);
|
|
31
|
+
}
|
|
32
|
+
get isAuthenticated() {
|
|
33
|
+
return this.state !== null;
|
|
34
|
+
}
|
|
35
|
+
get userInfo() {
|
|
36
|
+
if (!this.state)
|
|
37
|
+
return null;
|
|
38
|
+
return { username: this.state.username };
|
|
39
|
+
}
|
|
40
|
+
switchEnvironment(env) {
|
|
41
|
+
if (env === this.env)
|
|
42
|
+
return;
|
|
43
|
+
this.env = env;
|
|
44
|
+
this.state = null;
|
|
45
|
+
}
|
|
46
|
+
async getToken() {
|
|
47
|
+
if (this.state && !this.isExpiringSoon()) {
|
|
48
|
+
return this.state.token;
|
|
49
|
+
}
|
|
50
|
+
if (this.state && this.isExpiringSoon()) {
|
|
51
|
+
try {
|
|
52
|
+
await this.refresh();
|
|
53
|
+
return this.state.token;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Refresh failed — fall through to full re-auth
|
|
57
|
+
this.state = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
await this.authenticate();
|
|
61
|
+
return this.state.token;
|
|
62
|
+
}
|
|
63
|
+
isExpiringSoon() {
|
|
64
|
+
if (!this.state)
|
|
65
|
+
return true;
|
|
66
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
67
|
+
return this.state.exp - nowSeconds < REFRESH_BUFFER_SECONDS;
|
|
68
|
+
}
|
|
69
|
+
async authenticate() {
|
|
70
|
+
const creds = await this.resolveCredentials();
|
|
71
|
+
const basic = Buffer.from(`${creds.username}:${creds.password}`).toString("base64");
|
|
72
|
+
const response = await this.fetchFn(`${this.baseUrl}/idp2/users/auth`, {
|
|
73
|
+
method: "GET",
|
|
74
|
+
headers: {
|
|
75
|
+
Authorization: `Basic ${basic}`,
|
|
76
|
+
Accept: "application/json",
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
await this.handleAuthError(response);
|
|
81
|
+
}
|
|
82
|
+
const cookieHeader = response.headers.get("set-cookie");
|
|
83
|
+
const tokenVerifier = this.extractTokenVerifier(cookieHeader);
|
|
84
|
+
const body = await response.json();
|
|
85
|
+
this.state = {
|
|
86
|
+
token: body.token,
|
|
87
|
+
refreshToken: body.refreshToken ?? null,
|
|
88
|
+
tokenVerifier,
|
|
89
|
+
exp: body.exp,
|
|
90
|
+
username: creds.username,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async refresh() {
|
|
94
|
+
if (!this.state?.refreshToken) {
|
|
95
|
+
throw new Error("No refresh token available");
|
|
96
|
+
}
|
|
97
|
+
const headers = {
|
|
98
|
+
Authorization: `Refresh ${this.state.refreshToken}`,
|
|
99
|
+
Accept: "application/json",
|
|
100
|
+
};
|
|
101
|
+
if (this.state.tokenVerifier) {
|
|
102
|
+
headers["Cookie"] = `tokenverifier=${this.state.tokenVerifier}`;
|
|
103
|
+
}
|
|
104
|
+
const response = await this.fetchFn(`${this.baseUrl}/idp2/users/auth`, { method: "GET", headers });
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`Refresh failed: ${response.status}`);
|
|
107
|
+
}
|
|
108
|
+
const cookieHeader = response.headers.get("set-cookie");
|
|
109
|
+
const tokenVerifier = this.extractTokenVerifier(cookieHeader);
|
|
110
|
+
const body = await response.json();
|
|
111
|
+
this.state = {
|
|
112
|
+
...this.state,
|
|
113
|
+
token: body.token,
|
|
114
|
+
refreshToken: body.refreshToken ?? this.state.refreshToken,
|
|
115
|
+
tokenVerifier: tokenVerifier ?? this.state.tokenVerifier,
|
|
116
|
+
exp: body.exp,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async handleAuthError(response) {
|
|
120
|
+
let errorCode = "unknown";
|
|
121
|
+
try {
|
|
122
|
+
const body = await response.json();
|
|
123
|
+
if (body.errors?.[0]?.error) {
|
|
124
|
+
errorCode = body.errors[0].error;
|
|
125
|
+
}
|
|
126
|
+
const message = AUTH_ERROR_MESSAGES[errorCode] ??
|
|
127
|
+
body.message ??
|
|
128
|
+
`Authentication failed (${response.status})`;
|
|
129
|
+
throw new Error(message);
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
if (e instanceof Error && AUTH_ERROR_MESSAGES[errorCode])
|
|
133
|
+
throw e;
|
|
134
|
+
throw new Error(`Authentication failed (${response.status})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
extractTokenVerifier(cookieHeader) {
|
|
138
|
+
if (!cookieHeader)
|
|
139
|
+
return null;
|
|
140
|
+
const match = cookieHeader.match(/tokenverifier=([^;]+)/);
|
|
141
|
+
return match?.[1] ?? null;
|
|
142
|
+
}
|
|
143
|
+
async resolveCredentials() {
|
|
144
|
+
// 1. Environment variables
|
|
145
|
+
const envUser = process.env.OTH_USERNAME;
|
|
146
|
+
const envPass = process.env.OTH_PASSWORD;
|
|
147
|
+
if (envUser && envPass) {
|
|
148
|
+
return { username: envUser, password: envPass };
|
|
149
|
+
}
|
|
150
|
+
// 2. macOS Keychain
|
|
151
|
+
if (process.platform === "darwin") {
|
|
152
|
+
try {
|
|
153
|
+
const account = `oth-${this.env}`;
|
|
154
|
+
const [userResult, passResult] = await Promise.all([
|
|
155
|
+
execFileAsync("security", [
|
|
156
|
+
"find-generic-password",
|
|
157
|
+
"-a",
|
|
158
|
+
account,
|
|
159
|
+
"-s",
|
|
160
|
+
"oth-mcp-username",
|
|
161
|
+
"-w",
|
|
162
|
+
]),
|
|
163
|
+
execFileAsync("security", [
|
|
164
|
+
"find-generic-password",
|
|
165
|
+
"-a",
|
|
166
|
+
account,
|
|
167
|
+
"-s",
|
|
168
|
+
"oth-mcp-password",
|
|
169
|
+
"-w",
|
|
170
|
+
]),
|
|
171
|
+
]);
|
|
172
|
+
const username = userResult.stdout.trim();
|
|
173
|
+
const password = passResult.stdout.trim();
|
|
174
|
+
if (username && password) {
|
|
175
|
+
return { username, password };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Keychain lookup failed — fall through to error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// 3. Fail with instructions
|
|
183
|
+
throw new Error(`No OTH credentials found for environment "${this.env}".\n\n` +
|
|
184
|
+
"Set credentials via environment variables:\n" +
|
|
185
|
+
" OTH_USERNAME=your-username\n" +
|
|
186
|
+
" OTH_PASSWORD=your-password\n" +
|
|
187
|
+
" OTH_ENV=dev (optional, defaults to dev)\n\n" +
|
|
188
|
+
"Or on macOS, store in Keychain:\n" +
|
|
189
|
+
` security add-generic-password -a "oth-${this.env}" -s "oth-mcp-username" -w\n` +
|
|
190
|
+
` security add-generic-password -a "oth-${this.env}" -s "oth-mcp-password" -w`);
|
|
191
|
+
}
|
|
192
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AuthManager } from "./auth.js";
|
|
2
|
+
export declare class OthClient {
|
|
3
|
+
private auth;
|
|
4
|
+
constructor(auth: AuthManager);
|
|
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>;
|
|
7
|
+
private validatePath;
|
|
8
|
+
private buildUrl;
|
|
9
|
+
private processResponse;
|
|
10
|
+
private normalizeError;
|
|
11
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export class OthClient {
|
|
2
|
+
auth;
|
|
3
|
+
constructor(auth) {
|
|
4
|
+
this.auth = auth;
|
|
5
|
+
}
|
|
6
|
+
async get(path, query) {
|
|
7
|
+
this.validatePath(path);
|
|
8
|
+
const url = this.buildUrl(path, query);
|
|
9
|
+
const token = await this.auth.getToken();
|
|
10
|
+
const response = await fetch(url.toString(), {
|
|
11
|
+
method: "GET",
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${token}`,
|
|
14
|
+
Accept: "application/json",
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
return this.processResponse(response);
|
|
18
|
+
}
|
|
19
|
+
async mutate(method, path, body) {
|
|
20
|
+
this.validatePath(path);
|
|
21
|
+
const url = this.buildUrl(path);
|
|
22
|
+
const token = await this.auth.getToken();
|
|
23
|
+
const headers = {
|
|
24
|
+
Authorization: `Bearer ${token}`,
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
};
|
|
27
|
+
const init = { method, headers };
|
|
28
|
+
if (body !== undefined) {
|
|
29
|
+
headers["Content-Type"] = "application/json";
|
|
30
|
+
init.body = JSON.stringify(body);
|
|
31
|
+
}
|
|
32
|
+
const response = await fetch(url.toString(), init);
|
|
33
|
+
return this.processResponse(response);
|
|
34
|
+
}
|
|
35
|
+
validatePath(path) {
|
|
36
|
+
if (/^https?:\/\//i.test(path)) {
|
|
37
|
+
throw new Error("Absolute URLs are not allowed. Use a relative path like /clinician/api/patients");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
buildUrl(path, query) {
|
|
41
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
42
|
+
const url = new URL(normalized, this.auth.baseUrl);
|
|
43
|
+
if (query) {
|
|
44
|
+
for (const [key, value] of Object.entries(query)) {
|
|
45
|
+
url.searchParams.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
async processResponse(response) {
|
|
51
|
+
if (response.ok) {
|
|
52
|
+
if (response.status === 204)
|
|
53
|
+
return { success: true };
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
return this.normalizeError(response);
|
|
57
|
+
}
|
|
58
|
+
async normalizeError(response) {
|
|
59
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
60
|
+
let message;
|
|
61
|
+
try {
|
|
62
|
+
if (contentType.includes("application/json")) {
|
|
63
|
+
const body = (await response.json());
|
|
64
|
+
if (body.message) {
|
|
65
|
+
message = body.message;
|
|
66
|
+
}
|
|
67
|
+
else if (body.errors?.length) {
|
|
68
|
+
message = body.errors
|
|
69
|
+
.map((e) => `${e.error}${e.field ? ` (${e.field})` : ""}${e.resource ? ` on ${e.resource}` : ""}`)
|
|
70
|
+
.join("; ");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
message = `HTTP ${response.status} ${response.statusText}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const text = await response.text();
|
|
78
|
+
// Strip HTML tags if present
|
|
79
|
+
message = text.replace(/<[^>]*>/g, "").trim();
|
|
80
|
+
if (!message || message.length > 500) {
|
|
81
|
+
message = `HTTP ${response.status} ${response.statusText}`;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
message = `HTTP ${response.status} ${response.statusText}`;
|
|
87
|
+
}
|
|
88
|
+
return { error: true, status: response.status, message };
|
|
89
|
+
}
|
|
90
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { AuthManager } from "./auth.js";
|
|
5
|
+
import { OthClient } from "./client.js";
|
|
6
|
+
import { registerTools } from "./tools.js";
|
|
7
|
+
import { API_GUIDE } from "./api-guide.js";
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "oth-mcp",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
});
|
|
12
|
+
const auth = new AuthManager();
|
|
13
|
+
const client = new OthClient(auth);
|
|
14
|
+
registerTools(server, auth, client);
|
|
15
|
+
server.registerResource("api-guide", "oth://api-guide", {
|
|
16
|
+
description: "OTH API endpoint reference — paths, query params, and request body schemas",
|
|
17
|
+
mimeType: "text/markdown",
|
|
18
|
+
annotations: {
|
|
19
|
+
audience: ["assistant"],
|
|
20
|
+
},
|
|
21
|
+
}, async () => ({
|
|
22
|
+
contents: [
|
|
23
|
+
{
|
|
24
|
+
uri: "oth://api-guide",
|
|
25
|
+
mimeType: "text/markdown",
|
|
26
|
+
text: API_GUIDE,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
}));
|
|
30
|
+
async function main() {
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
console.error("oth-mcp server running on stdio");
|
|
34
|
+
}
|
|
35
|
+
main().catch((err) => {
|
|
36
|
+
console.error("Fatal error:", err);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
package/dist/tools.d.ts
ADDED
package/dist/tools.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function isApiError(result) {
|
|
3
|
+
return (typeof result === "object" &&
|
|
4
|
+
result !== null &&
|
|
5
|
+
"error" in result &&
|
|
6
|
+
result.error === true);
|
|
7
|
+
}
|
|
8
|
+
export function registerTools(server, auth, client) {
|
|
9
|
+
server.registerTool("oth_get", {
|
|
10
|
+
title: "OTH GET Request",
|
|
11
|
+
description: `GET any OTH API endpoint. Auth handled automatically.
|
|
12
|
+
Common paths:
|
|
13
|
+
/clinician/api/patients?username=&firstName=&max=
|
|
14
|
+
/clinician/api/patients/{id}
|
|
15
|
+
/clinician/api/questionnaire_definitions?max=
|
|
16
|
+
/clinician/api/questionnaires/{id}
|
|
17
|
+
/clinician/api/questionnaire-results?from=&to=&max=
|
|
18
|
+
/measurements/measurement-types
|
|
19
|
+
/organizations/organizations
|
|
20
|
+
Read the oth://api-guide resource for full endpoint reference.`,
|
|
21
|
+
annotations: {
|
|
22
|
+
readOnlyHint: true,
|
|
23
|
+
destructiveHint: false,
|
|
24
|
+
idempotentHint: true,
|
|
25
|
+
openWorldHint: true,
|
|
26
|
+
},
|
|
27
|
+
inputSchema: {
|
|
28
|
+
path: z
|
|
29
|
+
.string()
|
|
30
|
+
.describe("API path, e.g. /clinician/api/patients/123"),
|
|
31
|
+
query: z
|
|
32
|
+
.record(z.union([z.string(), z.number(), z.boolean()]))
|
|
33
|
+
.optional()
|
|
34
|
+
.describe("Query parameters as key-value pairs"),
|
|
35
|
+
},
|
|
36
|
+
}, async ({ path, query }) => {
|
|
37
|
+
try {
|
|
38
|
+
const coerced = query
|
|
39
|
+
? Object.fromEntries(Object.entries(query).map(([k, v]) => [k, String(v)]))
|
|
40
|
+
: undefined;
|
|
41
|
+
const result = await client.get(path, coerced);
|
|
42
|
+
if (isApiError(result)) {
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
46
|
+
],
|
|
47
|
+
isError: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: e instanceof Error ? e.message : String(e),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
server.registerTool("oth_mutate", {
|
|
69
|
+
title: "OTH Mutate Request",
|
|
70
|
+
description: "POST, PUT, PATCH, or DELETE to any OTH API endpoint. Auth handled automatically.",
|
|
71
|
+
annotations: {
|
|
72
|
+
readOnlyHint: false,
|
|
73
|
+
destructiveHint: true,
|
|
74
|
+
idempotentHint: false,
|
|
75
|
+
openWorldHint: true,
|
|
76
|
+
},
|
|
77
|
+
inputSchema: {
|
|
78
|
+
method: z
|
|
79
|
+
.enum(["POST", "PUT", "PATCH", "DELETE"])
|
|
80
|
+
.default("POST")
|
|
81
|
+
.describe("HTTP method"),
|
|
82
|
+
path: z.string().describe("API path"),
|
|
83
|
+
body: z
|
|
84
|
+
.record(z.unknown())
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Request body (JSON)"),
|
|
87
|
+
},
|
|
88
|
+
}, async ({ method, path, body }) => {
|
|
89
|
+
try {
|
|
90
|
+
const result = await client.mutate(method, path, body);
|
|
91
|
+
if (isApiError(result)) {
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
95
|
+
],
|
|
96
|
+
isError: true,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: [
|
|
101
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
return {
|
|
107
|
+
content: [
|
|
108
|
+
{
|
|
109
|
+
type: "text",
|
|
110
|
+
text: e instanceof Error ? e.message : String(e),
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
isError: true,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
server.registerTool("oth_env", {
|
|
118
|
+
title: "OTH Environment",
|
|
119
|
+
description: "Switch OTH environment or show current state. Returns environment, base URL, and authenticated user info.",
|
|
120
|
+
annotations: {
|
|
121
|
+
readOnlyHint: false,
|
|
122
|
+
destructiveHint: false,
|
|
123
|
+
idempotentHint: true,
|
|
124
|
+
openWorldHint: false,
|
|
125
|
+
},
|
|
126
|
+
inputSchema: {
|
|
127
|
+
environment: z
|
|
128
|
+
.enum(["dev", "stag", "demo"])
|
|
129
|
+
.optional()
|
|
130
|
+
.describe("Target environment. Omit to show current state."),
|
|
131
|
+
},
|
|
132
|
+
}, async ({ environment }) => {
|
|
133
|
+
if (environment) {
|
|
134
|
+
auth.switchEnvironment(environment);
|
|
135
|
+
}
|
|
136
|
+
const info = {
|
|
137
|
+
environment: auth.environment,
|
|
138
|
+
baseUrl: auth.baseUrl,
|
|
139
|
+
authenticated: auth.isAuthenticated,
|
|
140
|
+
};
|
|
141
|
+
if (auth.isAuthenticated) {
|
|
142
|
+
info.user = auth.userInfo;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
content: [
|
|
146
|
+
{ type: "text", text: JSON.stringify(info, null, 2) },
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oth-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for the OTH (OpenTeleHealth) API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"oth-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
+
"zod": "^3.23.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^22.0.0",
|
|
29
|
+
"typescript": "^5.7.0",
|
|
30
|
+
"vitest": "^3.0.0"
|
|
31
|
+
}
|
|
32
|
+
}
|