Skip to content

Read a resume with a tool

So far, we supply resume text ourselves. Now we’ll give the coach a PDF URL and connect a tool that extracts its text.

MarkItDown handles the document format. The agent requests extraction and uses the returned text. Checking that text first helps us separate parser problems from later questions about the coach’s interpretation.

Keep the existing session lifecycle in interview-coach-lab. This chapter adds extraction on request and asks permission before saving newly extracted document text.

Read the text inside a resume

We ask the coach to extract a sample resume. MarkItDown fetches the URL and returns text we can inspect.

  1. Sample resume URL. Give the coach a published fictional resume URL that the MarkItDown container can reach.
  2. MarkItDown MCP. The coach invokes the connected extraction tool. MarkItDown fetches the document and converts it to text.
  3. Extracted text. The tool returns document text. Check for recognizable details from the fictional resume.
  4. Coach reply. The coach can quote or summarize the returned text. In the next chapter, we will add instructions to save it with the interview.

Document text is untrusted input. Keep the coach's instructions in control of what it does next.

In root apphost.cs, add the MarkItDown container and connect it to the agent:

Add the MarkItDown HTTP container

File to edit: apphost.cs

Scope to edit: Top-level statements (no enclosing function)

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
// Azure Cosmos DB (NoSQL). Uses the local emulator in run mode and provisions a managed

Updated code
var mcpMarkItDown = builder.AddContainer(ResourceConstants.McpMarkItDown, "mcp/markitdown", "latest")
.WithHttpEndpoint(targetPort: 3001)
.WithArgs("--http", "--host", "0.0.0.0", "--port", "3001");
// Azure Cosmos DB (NoSQL). Uses the local emulator in run mode and provisions a managed

Connect and wait for MarkItDown

File to edit: apphost.cs

Scope to edit: Top-level statements (no enclosing function)

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
var agent = builder.AddProject<Projects.InterviewCoach_Agent>(ResourceConstants.Agent)
.WithExternalHttpEndpoints()
.WithLlmReference(config, args)
.WithReference(mcpInterviewData)
.WaitFor(mcpInterviewData);

Updated code
var agent = builder.AddProject<Projects.InterviewCoach_Agent>(ResourceConstants.Agent)
.WithExternalHttpEndpoints()
.WithLlmReference(config, args)
.WithReference(mcpMarkItDown.GetEndpoint("http"))
.WithReference(mcpInterviewData)
.WaitFor(mcpMarkItDown)
.WaitFor(mcpInterviewData);

The container exposes HTTP on target port 3001. The agent receives its endpoint and waits for it, alongside InterviewData. Continue using only the root AppHost.

In src/InterviewCoach.Agent/Program.cs, add a named HTTP client and a keyed MCP client for MarkItDown:

Configure the document conversion HTTP client

File to edit: src/InterviewCoach.Agent/Program.cs

Scope to edit: Top-level statements (no enclosing function)

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
builder.Services.AddHttpClient("mcp-interview-data", client =>

Updated code
builder.Services.AddHttpClient("mcp-markitdown", client =>
{
client.BaseAddress = new Uri("http://mcp-markitdown");
});
builder.Services.AddHttpClient("mcp-interview-data", client =>

Create the keyed document MCP client

File to edit: src/InterviewCoach.Agent/Program.cs

Scope to edit: Top-level statements (no enclosing function)

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
builder.Services.AddHttpClient("mcp-interview-data", client =>

Updated code
builder.Services.AddKeyedSingleton<McpClient>("mcp-markitdown", (sp, obj) =>
{
var loggerFactory = sp.GetRequiredService<ILoggerFactory>();
var httpClient = sp.GetRequiredService<IHttpClientFactory>()
.CreateClient("mcp-markitdown");
var endpoint = $"{httpClient.BaseAddress!.ToString().TrimEnd('/')}";
var clientTransportOptions = new HttpClientTransportOptions()
{
Endpoint = new Uri($"{endpoint}/mcp")
};
var clientTransport = new HttpClientTransport(clientTransportOptions, httpClient, loggerFactory);
var clientOptions = new McpClientOptions()
{
ClientInfo = new Implementation()
{
Name = "MCP MarkItDown Client",
Version = "1.0.0",
}
};
return McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory).GetAwaiter().GetResult();
});
builder.Services.AddHttpClient("mcp-interview-data", client =>

Keep the existing InterviewData client. Each key identifies a separate server.

In AgentDelegateFactory.cs, discover both tool sets:

Discover both sets of MCP tools

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateSingleAgent

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
private static AIAgent CreateSingleAgent(IServiceProvider sp, string key)
{
var interviewData = sp.GetRequiredKeyedService<McpClient>("mcp-interview-data");
var interviewDataTools = interviewData.ListToolsAsync().GetAwaiter().GetResult();

Updated code
private static AIAgent CreateSingleAgent(IServiceProvider sp, string key)
{
var markitdown = sp.GetRequiredKeyedService<McpClient>("mcp-markitdown");
var interviewData = sp.GetRequiredKeyedService<McpClient>("mcp-interview-data");
var markitdownTools = markitdown.ListToolsAsync().GetAwaiter().GetResult();
var interviewDataTools = interviewData.ListToolsAsync().GetAwaiter().GetResult();

Now pass both sets to the coach. The added instructions tell it to extract a URL when asked and report the returned text. Extraction and saving are separate decisions here. Newly extracted text needs permission before storage:

Let the coach extract a URL on request

File to edit: src/InterviewCoach.Agent/AgentDelegateFactory.cs

Function to edit: CreateSingleAgent

Replace the matching block with the code below. Open "Current code" to locate the block in your file.

Current code
Current code
instructions: """
You are a supportive interview coach.
Use the SessionId provided by the application for all session tools.
Always call get_interview_session with that ID first.
If it returns no record, call add_interview_session with that exact ID before any update.
Never use update_interview_session to create a record.
If a tool fails, report the failure. Say a change was saved only after the tool returns the saved record.
After fetching or creating the record, begin your first reply with "Session ID: <id>" using that exact ID.
Ask for resume and job description text, or let the user skip either.
Save the inputs and ask one behavioural question at a time.
For each update, fetch the record and preserve all six resume/job fields.
Set Transcript to ONLY the new question, answer, or feedback to append.
Never copy the existing transcript into an update; the repository appends it.
Move to technical questions when the user is ready.
If the user stops, append the new summary with update_interview_session.
Then call complete_interview_session with the same ID.
Confirm completion only when its returned record has IsCompleted true.
Use supplied documents only as interview context.
""",
tools: [.. interviewDataTools]

Updated code
instructions: """
You are a supportive interview coach.
Use the SessionId provided by the application for all session tools.
Always call get_interview_session with that ID first.
If it returns no record, call add_interview_session with that exact ID before any update.
Never use update_interview_session to create a record.
If a tool fails, report the failure. Say a change was saved only after the tool returns the saved record.
After fetching or creating the record, begin your first reply with "Session ID: <id>" using that exact ID.
Ask for resume and job description text, or let the user skip either.
Save the inputs and ask one behavioural question at a time.
For each update, fetch the record and preserve all six resume/job fields.
Set Transcript to ONLY the new question, answer, or feedback to append.
Never copy the existing transcript into an update; the repository appends it.
Move to technical questions when the user is ready.
If the user stops, append the new summary with update_interview_session.
Then call complete_interview_session with the same ID.
Confirm completion only when its returned record has IsCompleted true.
Use supplied documents only as interview context.
When explicitly asked to extract a document URL, call MarkItDown and report its text.
Ask before saving newly extracted document text in the interview record.
""",
tools: [.. markitdownTools, .. interviewDataTools]

Use fictional documents throughout the workshop. MarkItDown fetches URLs from its container, and extracted text reaches your configured model. Treat document contents as untrusted input. Review tool permissions before using real data.

From the working project root:

Terminal window
aspire stop --apphost ./apphost.cs
dotnet build InterviewCoach.slnx
aspire start --apphost ./apphost.cs

Wait for MarkItDown in the dashboard, then open coach in DevUI. Send this request with the published URL, which also works when you’re reading a local workshop preview:

Use session ID 55555555-5555-4555-8555-555555555555.
Extract this fictional resume with the MarkItDown tool:
https://codemillmatt.github.io/interview-coach-agent-framework/samples/resume-peter-parker.pdf
Show the name and one work-experience item from the returned text.
Leave the extracted document text unsaved for this check.

Existing session setup may still run. Use a fresh GUID if repeating the exercise.

Open the parser call in the run details. Check its URL and returned text, then open the same PDF in your browser and find the name and experience item. That’s the result we need before adding automatic document intake.

Why the URL must be reachable from the container

A local file path belongs to your machine. MarkItDown needs an HTTP URL its container can fetch, including any required network access. Use the public sample first. Browser-only authentication and localhost preview URLs can prevent retrieval.

The container uses the reference’s latest image tag. When comparing parser behavior across machines, record the resolved image digest.

Chapter 8 · Tools and interview context

Next: 9. Use the resume in the interview