Guide · AWS Transcribe · Medical Transcription
AWS Transcribe Medical in MCP Servers — StartMedicalTranscriptionJob, Specialties, PHI Identification
AWS Transcribe Medical uses a speech model fine-tuned on clinical audio — physician dictation, patient-provider conversations, and specialty clinical notes — where standard Transcribe struggles with drug names, anatomical terms, and medical abbreviations. An MCP server wraps StartMedicalTranscriptionJobCommand so an AI agent can transcribe clinical audio and receive a structured transcript with optional PHI (Protected Health Information) entity markers. Medical Transcribe requires a Specialty (e.g., PRIMARYCARE, CARDIOLOGY) and a Type (CONVERSATION for patient-provider dialogue, DICTATION for single-speaker clinical notes). This guide covers parameters, PHI identification, the medical custom vocabulary (separate from standard vocabularies), streaming medical transcription, and HIPAA compliance considerations.
TL;DR
Call StartMedicalTranscriptionJobCommand with Specialty, Type, Media.MediaFileUri, OutputBucketName, and LanguageCode: 'en-US' (Medical Transcribe currently supports only US English). Set ContentIdentificationType: 'PHI' to have Transcribe annotate PHI entities (patient names, DOBs, MRNs, phone numbers) in the items array with Entities[]. For speaker diarization in a CONVERSATION job, use MedicalTranscriptionSetting.MaxSpeakerLabels. Use CreateMedicalVocabularyCommand (not CreateVocabularyCommand) for domain vocabulary — standard vocabularies are not compatible with medical jobs. Ensure your AWS account has executed a HIPAA BAA before processing real patient audio.
Basic tool implementation
import {
TranscribeClient,
StartMedicalTranscriptionJobCommand,
GetMedicalTranscriptionJobCommand,
MedicalTranscriptionJobStatus,
Specialty,
Type,
} from '@aws-sdk/client-transcribe';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
const transcribe = new TranscribeClient({ region: 'us-east-1' });
const s3 = new S3Client({ region: 'us-east-1' });
const server = new McpServer({ name: 'medical-transcribe-tools', version: '1.0.0' });
server.tool('transcribe_clinical_audio', {
s3_uri: z.string().regex(/^s3:\/\//),
specialty: z.enum([
'PRIMARYCARE', 'CARDIOLOGY', 'NEUROLOGY',
'ONCOLOGY', 'RADIOLOGY', 'UROLOGY',
]).default('PRIMARYCARE'),
type: z.enum(['CONVERSATION', 'DICTATION']).default('CONVERSATION'),
output_bucket: z.string(),
identify_phi: z.boolean().default(false),
max_speakers: z.number().int().min(2).max(10).optional(),
}, async ({ s3_uri, specialty, type, output_bucket, identify_phi, max_speakers }) => {
const jobName = `medical-${Date.now()}`;
await transcribe.send(new StartMedicalTranscriptionJobCommand({
MedicalTranscriptionJobName: jobName,
LanguageCode: 'en-US', // only en-US is supported for Transcribe Medical
Specialty: specialty as Specialty,
Type: type as Type,
Media: { MediaFileUri: s3_uri },
OutputBucketName: output_bucket,
OutputKey: `medical-transcripts/${jobName}.json`,
ContentIdentificationType: identify_phi ? 'PHI' : undefined,
MedicalTranscriptionSetting: (type === 'CONVERSATION' && max_speakers)
? { ShowSpeakerLabels: true, MaxSpeakerLabels: max_speakers }
: undefined,
}));
// Poll for completion
let job;
for (let attempt = 0; attempt < 60; attempt++) {
await new Promise(r => setTimeout(r, 20_000));
const { MedicalTranscriptionJob } = await transcribe.send(
new GetMedicalTranscriptionJobCommand({ MedicalTranscriptionJobName: jobName })
);
job = MedicalTranscriptionJob;
if (job?.MedicalTranscriptionJobStatus === MedicalTranscriptionJobStatus.COMPLETED) break;
if (job?.MedicalTranscriptionJobStatus === MedicalTranscriptionJobStatus.FAILED) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: job.FailureReason }) }],
isError: true,
};
}
}
// Fetch and return the transcript
const transcript = await fetchTranscriptJson(output_bucket, `medical-transcripts/${jobName}.json`);
const text = transcript.results.transcripts[0].transcript;
const phiEntities = identify_phi ? extractPHI(transcript.results.items) : [];
return {
content: [{
type: 'text',
text: JSON.stringify({ job_name: jobName, text, phi_entities: phiEntities }, null, 2),
}],
};
});
async function fetchTranscriptJson(bucket: string, key: string): Promise<any> {
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const chunks: Uint8Array[] = [];
for await (const chunk of Body as AsyncIterable<Uint8Array>) chunks.push(chunk);
return JSON.parse(Buffer.concat(chunks).toString('utf-8'));
}
function extractPHI(items: any[]): { content: string; category: string; type: string }[] {
const entities: { content: string; category: string; type: string }[] = [];
for (const item of items) {
if (!item.entities?.length) continue;
for (const entity of item.entities) {
if (entity.category === 'PHI') {
entities.push({
content: item.alternatives?.[0]?.content ?? '',
category: entity.category,
type: entity.type,
});
}
}
}
return entities;
}
Specialty selection
| Specialty | Optimized for | Example terms improved |
|---|---|---|
PRIMARYCARE |
General practice, family medicine, internal medicine notes and conversations | HbA1c, hypertension, dyslipidemia, COPD exacerbation, prednisone taper |
CARDIOLOGY |
Cardiology notes, echocardiogram reports, cath lab dictation | STEMI, troponin, ejection fraction, aortic stenosis, clopidogrel, atorvastatin |
NEUROLOGY |
Neurology consults, EEG/EMG reports, neurological exam dictation | GCS score, Babinski reflex, MRI FLAIR, levetiracetam, Parkinsonism, aphasia |
ONCOLOGY |
Oncology notes, chemotherapy regimens, staging discussions | FOLFOX, bevacizumab, PD-L1, TNM staging, neutropenic fever, G-CSF |
RADIOLOGY |
Radiology report dictation (CT, MRI, X-ray, ultrasound) | Hounsfield units, lytic lesion, hepatomegaly, pneumothorax, stenosis grade |
UROLOGY |
Urology consults, cystoscopy dictation, prostate notes | PSA velocity, Gleason score, hydronephrosis, cystoscopy with fulguration |
Always choose the specialty that most closely matches the audio content. Using PRIMARYCARE for a cardiology note will still produce a usable transcript but may mishear specialty-specific drug names and procedure terms. If the audio spans multiple specialties (e.g., a multi-system consultation note), choose the dominant specialty and supplement with a medical custom vocabulary for terms outside it.
CONVERSATION vs DICTATION type
| Type | Use when | Speaker diarization |
|---|---|---|
CONVERSATION |
Patient-provider encounter: office visits, telemedicine calls, nurse triage, follow-up calls where multiple speakers alternate | Available via MedicalTranscriptionSetting.ShowSpeakerLabels: true |
DICTATION |
Single-speaker clinical note dictation: physician recording a discharge summary, radiology report, operative note, progress note | Not supported — ShowSpeakerLabels is ignored or returns an error on DICTATION jobs |
Accuracy is higher when the type matches the audio. DICTATION jobs are optimized for fluent monologue with medical vocabulary; they handle pauses and self-correction better. CONVERSATION jobs handle turn-taking, overlapping speech, and varying acoustic conditions (phone audio, telemedicine platforms) better. Do not use DICTATION type for two-party audio — the model will concatenate both speakers' words into a single stream, making the transcript unreadable.
PHI identification
// PHI entity types identified by Transcribe Medical with ContentIdentificationType: 'PHI'
// Annotated in results.items[N].entities[] when present
const PHI_TYPE_EXAMPLES = {
NAME: 'Patient name, provider name',
PHONE: 'Phone number',
EMAIL: 'Email address',
ADDRESS: 'Street address, zip code, state',
AGE: 'Patient age or date of birth',
ID: 'Medical record number, SSN, insurance ID',
DATE: 'Appointment date, DOB, admission date',
DIAGNOSIS: 'Specific diagnosis that could identify the patient',
TREATMENT: 'Specific treatment or procedure tied to patient identity',
PROFESSION: 'Occupation that narrows patient identity',
};
// In the transcript items, PHI-flagged words look like:
// {
// "type": "pronunciation",
// "start_time": "1.23",
// "end_time": "1.89",
// "alternatives": [{ "confidence": "0.9987", "content": "John" }],
// "entities": [{ "content": "John", "category": "PHI", "type": "NAME", "confidence": "0.9987" }]
// }
function redactPHIFromTranscript(items: any[]): string {
return items
.filter(i => i.type === 'pronunciation' || i.type === 'punctuation')
.map(item => {
const content = item.alternatives?.[0]?.content ?? '';
const hasPHI = item.entities?.some((e: any) => e.category === 'PHI');
return hasPHI ? '[PHI]' : content;
})
.join(item => item.type === 'punctuation' ? '' : ' '); // simplified join
}
PHI identification produces annotations — it does not automatically redact the transcript text. The word still appears in results.transcripts[0].transcript unmodified. To build a redacted version, iterate results.items[], check for PHI entities, and substitute [PHI] placeholders. Store the unredacted transcript in a bucket with restricted access (IAM policy permitting only the minimum-privilege read role) and share the redacted version with downstream systems.
PHI identification coverage is not exhaustive — it uses context and position to infer PHI, so it may miss PHI that appears in unusual positions or is phrased atypically. Do not rely solely on automated PHI identification for de-identification workflows that have legal safe-harbour requirements; apply a secondary manual review or rule-based regex pass for high-risk output.
Medical custom vocabulary
import {
CreateMedicalVocabularyCommand,
GetMedicalVocabularyCommand,
VocabularyState,
} from '@aws-sdk/client-transcribe';
// Medical vocabularies use CreateMedicalVocabularyCommand — NOT CreateVocabularyCommand
// Standard vocabularies are incompatible with medical transcription jobs
await transcribe.send(new CreateMedicalVocabularyCommand({
VocabularyName: 'oncology-drug-vocab',
LanguageCode: 'en-US',
VocabularyFileUri: 's3://my-assets/medical-vocabularies/oncology-drugs.tsv',
}));
// Wait for READY state (same polling pattern as standard vocabularies)
async function waitForMedicalVocabReady(name: string): Promise<void> {
for (let i = 0; i < 30; i++) {
await new Promise(r => setTimeout(r, 10_000));
const { VocabularyState: state } = await transcribe.send(
new GetMedicalVocabularyCommand({ VocabularyName: name })
);
if (state === VocabularyState.READY) return;
if (state === VocabularyState.FAILED) throw new Error(`Medical vocabulary ${name} failed`);
}
}
// Use the medical vocabulary in a job:
await transcribe.send(new StartMedicalTranscriptionJobCommand({
MedicalTranscriptionJobName: `medical-${Date.now()}`,
LanguageCode: 'en-US',
Specialty: 'ONCOLOGY',
Type: 'DICTATION',
Media: { MediaFileUri: 's3://recordings/note.wav' },
OutputBucketName: 'transcripts-bucket',
MedicalTranscriptionSetting: {
VocabularyName: 'oncology-drug-vocab', // medical vocabulary, not standard
},
}));
Medical custom vocabularies follow the same TSV format as standard vocabularies (Phrase, IPA, SoundsLike, DisplayAs columns), but they're compiled by a different model and must be created separately. Focus medical vocabularies on terms that are specific to your specialty and genuinely difficult to recognise — rarely spoken drug names, eponymous syndromes, investigational therapies. The base Transcribe Medical model already recognises common drug classes and standard anatomical terms well; adding those to a custom vocabulary has no benefit and can slow compile time.
Medical streaming transcription
import {
TranscribeStreamingClient,
StartMedicalStreamingTranscriptionCommand,
} from '@aws-sdk/client-transcribe-streaming';
const streamingClient = new TranscribeStreamingClient({ region: 'us-east-1' });
const command = new StartMedicalStreamingTranscriptionCommand({
LanguageCode: 'en-US',
MediaEncoding: 'pcm',
MediaSampleRateHertz: 16000,
Specialty: 'PRIMARYCARE',
Type: 'CONVERSATION',
AudioStream: audioStream(), // same generator pattern as standard streaming
EnableChannelIdentification: false,
ShowSpeakerLabel: true,
ContentIdentificationType: 'PHI', // PHI annotation available in streaming
});
const { MedicalTranscriptResultStream } = await streamingClient.send(command);
for await (const event of MedicalTranscriptResultStream as AsyncIterable<any>) {
if (!event.TranscriptEvent) continue;
for (const result of event.TranscriptEvent.Transcript.Results ?? []) {
if (result.IsPartial) continue;
// result.Alternatives[0].Transcript — full text of this utterance
// result.Alternatives[0].Items — word-level timing + entities (PHI annotations)
console.log(result.Alternatives?.[0]?.Transcript);
}
}
Medical streaming uses StartMedicalStreamingTranscriptionCommand (not StartStreamingTranscriptionCommand) and returns MedicalTranscriptResultStream instead of TranscriptResultStream. The partial/final result semantics and stabilization settings work identically to standard streaming. PHI annotations (ContentIdentificationType: 'PHI') are available in medical streaming and annotate items in real time as the stream progresses. Medical streaming is subject to the same 5-concurrent-session default limit per region as standard streaming.
HIPAA compliance checklist
| # | Requirement | Notes |
|---|---|---|
| 1 | AWS Business Associate Addendum (BAA) signed | Required before processing any PHI. Enable in AWS Artifact → Agreements. Transcribe Medical is HIPAA-eligible but only under a signed BAA. |
| 2 | Encrypt audio in S3 at rest | Use SSE-KMS with a customer-managed key. Enable S3 Object Lock for audit immutability on output buckets. |
| 3 | Encrypt transcript in S3 at rest | Apply the same KMS key to the output bucket. Grant Transcribe kms:GenerateDataKey + kms:Decrypt for both input and output keys. |
| 4 | Restrict access to output transcripts | IAM policy limiting s3:GetObject on transcript prefix to only authorised roles. Avoid granting s3:ListBucket broadly. |
| 5 | Enable S3 access logging and CloudTrail for all API calls | Log all transcribe:StartMedicalTranscriptionJob and s3:GetObject calls. Set CloudTrail log retention ≥ 6 years for HIPAA. |
| 6 | Define a data retention and deletion policy | Transcribe does not retain audio or transcripts — your S3 bucket is the system of record. Apply S3 lifecycle rules to transition to Glacier and expire per your retention schedule. |
| 7 | Do not log PHI in application logs | Never log the transcript text, job names derived from patient identifiers, or S3 keys containing MRNs. Log job IDs only. |
| 8 | VPC endpoint for Transcribe in production | Use an interface VPC endpoint (com.amazonaws.us-east-1.transcribe) so PHI audio never traverses the public internet. Same for S3. |