Skip to content

feat(mcp-ts): added mvp v2 prompts, resources methods with paginationToken - #4296

Open
poshinchen wants to merge 2 commits into
strands-agents:mainfrom
poshinchen:feat/mcp-v2-prompts-resources
Open

poshinchen wants to merge 2 commits into
strands-agents:mainfrom
poshinchen:feat/mcp-v2-prompts-resources

Conversation

@poshinchen

@poshinchen poshinchen commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Description

The TypeScript McpClient exposes tools and nothing else. Python already has prompt and resource methods on main (list_prompts_sync, get_prompt_sync, list_resources_sync, read_resource_sync, list_resource_templates_sync), and the only TypeScript route to the same data is the raw client getter.

Therefore, this PR adds the five missing methods.

The methods return the MCP library's result objects unchanged, as Python does. Every list method takes a paginationToken parameter, the TypeScript spelling of Python's pagination_token (PR). With no token the client fetches and aggregates every page, the same way listTools already drains pages. With a token it fetches one page, and the result's nextCursor feeds the next call.

NOTE: The aggregation is a deliberate divergence from Python, whose list methods return page one plus a token. The MCP client caps the aggregate walk at 64 pages and throws ListPaginationExceeded past that, so McpClientOptions gains a listMaxPages passthrough as the escape hatch for large catalogs (0 disables the cap). The cap is stated into the docstrings.

Public API Changes

Added: listPrompts, getPrompt, listResources, readResource, and listResourceTemplates on McpClient, the McpListOptions interface carrying paginationToken, and a listMaxPages option on McpClientOptions capping the no-token aggregation:

const prompts = await client.listPrompts()
const prompt = await client.getPrompt('summarize', { topic: 'AI' })
const page = await client.listResources({ paginationToken: cursor })
const content = await client.readResource('file:///data.txt')
const templates = await client.listResourceTemplates()

The methods connect lazily. When an earlier connect attempt failed with continueOnError set, they throw the same error callTool does.

Related Issues

Closes #3273. Part of #1659.
Unblocks #3277 (completions).

Documentation PR

Site docs for prompts and resources follow with the MCP docs refresh noted in #4093.

Type of Change

New feature

Testing

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@poshinchen
poshinchen requested a review from a team as a code owner September 11, 2026 16:08
@poshinchen
poshinchen requested a review from mehtarac September 11, 2026 16:08
@github-actions github-actions Bot added area-mcp MCP related typescript Pull requests that update typescript code enhancement New feature or request strands-running complexity/low Touched functions have low cognitive complexity (<=10) size/m labels Sep 11, 2026
Comment thread strands-ts/src/mcp/client.ts
Comment thread strands-ts/src/mcp/client.ts
@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Clean, well-documented MVP for MCP v2 prompts/resources. The migration to @modelcontextprotocol/client and the pagination passthrough are correct — I verified that the v2 client auto-aggregates all pages when no cursor is passed, so the "aggregate every page" JSDoc is accurate. Unit tests use full-identity (toBe) assertions and there's a real-server integration test. Main items are around public-API polish and PR hygiene, not correctness.

Review Categories
  • Public API surface: New methods return raw third-party result types that aren't re-exported, coupling the public API to @modelcontextprotocol/client and diverging from callTool's abstracted return. This PR looks like it needs the api/needs-review label plus the API documentation section (use cases, signatures, module exports) the template asks for.
  • Naming: getPrompt(promptId) vs MCP's name terminology (see inline).
  • Behavior change: tasksConfig now throws from callTool while task support is rebuilt ([FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659) — well documented, with integ suites skipped and clear rationale. No concern, just calling it out as a behavior change reviewers should be aware of.
  • PR hygiene: Description is the empty template — no summary, no linked issue ([FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659), testing checklist unchecked. Please fill these in for reviewers and the changelog.

Nice work on the thorough JSDoc and the lazy-connect _connectOrThrow helper keeping the new methods consistent.

@github-actions

Copy link
Copy Markdown
Contributor

Updated assessment: Approve (non-blocking nits)

Re-reviewed after the description was filled in and after cross-checking the Python SDK. This is a faithful parity port of strands-py's already-shipped prompt/resource surface (list_prompts_sync, get_prompt_sync, list_resources_sync, read_resource_sync, list_resource_templates_sync), and it lands correctly against the 2.0 client. My two earlier inline concerns are resolved:

Remaining items are all non-blocking: (1) an optional one-line listMaxPages (64-page) caveat on the aggregate JSDoc, (2) the args required-vs-optional parity divergence from Python worth a quick confirm, and (3) the api/needs-review label is still absent — likely fine since this is a parity port of an already-decided API shape, but worth a maintainer's call.

Nice, tight PR with strong test coverage and clear parity rationale.

@poshinchen

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review the changes and parity between python: #3984

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR — parity with Python holds on names, params and wire shape; one deliberate divergence worth a decision. No blockers. (Reviewed only the PR-authored commit 855cc69; the rest of the file list is #4093, which this is stacked on.)

Parity vs #3984 / mcp_client.py

  • ✅ paginationToken ↔ pagination_token; wire is { cursor } on both sides (paginationParams() ↔ _pagination_params() → PaginatedRequestParams(cursor=…)). No next_cursor compat shim needed in TS — single vendor line, camelCase.
  • ✅ Method names ↔ *_sync; promptId ↔ prompt_id; readResource(string | URL) ↔ read_resource_sync(AnyUrl | str); raw vendor result types returned on both sides.
  • ⚠️ No-token semantics diverge. Python list_prompts_sync() returns page 1 with next_cursor (caller walks). TS listPrompts() hands the vendor client undefined, which aggregates all pages, deletes nextCursor, and throws ListPaginationExceeded at listMaxPages (default 64) — a cap Python's own loop doesn't have. And since page 1 is never returned raw, a TS caller has no way to start the per-page walk the vendor suggests as the fallback. Inline thread with a suggestion.
  • ⚪ args? optional vs Python required — Python's docstring already says "Optional arguments", so TS optional is the better spelling; fine as-is.

5 new public methods + an exported type and no api/needs-review label (#4093 has one) — flagging for the API owner.

✅ Verified (head 855cc69, on top of #4093 fd2e762)
  • npm run build ✅ · tsc --noEmit src + test/integ ✅ · eslint ✅ · prettier --check ✅
  • Unit vitest --project unit-node src/mcp: 164/164 ✅
  • Integ test/integ/mcp/mcp.test.node.ts -t "prompts and resources": 1/1 ✅ against the real stdio fixture server (prompt render, static resource, templated resource via URL).
  • Vendor behaviour read from @modelcontextprotocol/client@2.0.0 dist/index.mjs: listPrompts (~L3558) → params?.cursor !== undefined ⇒ single raw page; else _serveFromCache then _listAllPages (~L3657), which throws SdkError(ListPaginationExceeded) when pages >= listMaxPages and delete acc.nextCursor on success.
  • new Client(...) at client.ts:240-262 passes no listMaxPages, so the vendor default 64 applies and isn't user-configurable.
  • new URL('file:///a b.txt').toString() → file:///a%20b.txt; string input is passed through untouched, so only URL callers get normalised — same posture as Python's AnyUrl.
Questions (non-blocking)
  1. Is aggregate-by-default the intended TS contract (matches listTools()), accepting the divergence from Python's page-1 default? If yes, the JSDoc caveat plus a listMaxPages passthrough on McpClientOptions is the minimum; if no, returning the raw first page on no-token restores parity exactly.
  2. Should the vendor result types (ListPromptsResult, GetPromptResult, …) be re-exported from strands-ts/src/mcp/index.ts so callers can annotate without importing @modelcontextprotocol/client directly? Python users get them from mcp.types, so there's an argument either way.
Appendix — non-blocking (3)
  • ⚪ Server without prompts/resources capability: vendor returns { prompts: [] } with a console.debug, where Python surfaces the server's method-not-found error. Vendor behaviour, just a known asymmetry.
  • ⚪ McpListOptions vs the existing McpListToolsOptions (which has no paginationToken): slightly asymmetric naming, but listTools returns McpTool[] rather than a paginated result, so a shared type wouldn't fit.
  • ⚪ Stacked on #4093 — GitHub's file list shows both; collapses to 6 files once #4093 merges.

Comment thread strands-ts/src/mcp/client.ts Outdated
@poshinchen poshinchen added the api/needs-review Makes changes to the public API surface label Sep 11, 2026
@poshinchen
poshinchen force-pushed the feat/mcp-v2-prompts-resources branch from 855cc69 to 94e6055 Compare September 11, 2026 17:16
@github-actions

Copy link
Copy Markdown
Contributor

@poshinchen Did the parity pass against strands-py (mcp_client.py) and the #3984 pagination change. Checked out the branch and compared each method head-to-head. Verdict: faithful parity — the divergences are deliberate, documented, and defensible.

Signature parity matrix
Python (*_sync) TypeScript Parity
list_prompts_sync(pagination_token=None) -> ListPromptsResult listPrompts(options?) -> ListPromptsResult ✅ name + raw return; pagination_token ↔ paginationToken
get_prompt_sync(prompt_id, args) -> GetPromptResult getPrompt(promptId, args?) -> GetPromptResult ✅ prompt_id ↔ promptId; ⚠️ arg optionality (below)
list_resources_sync(pagination_token=None) -> ListResourcesResult listResources(options?) -> ListResourcesResult ✅
read_resource_sync(uri: AnyUrl | str) -> ReadResourceResult readResource(uri: string | URL) -> ReadResourceResult ✅ AnyUrl ↔ URL, both accept string
list_resource_templates_sync(pagination_token=None) -> ListResourceTemplatesResult listResourceTemplates(options?) -> ListResourceTemplatesResult ✅

Deliberate divergences — all fine:

  1. Aggregation model. Python's list_*_sync return a single page + token (caller drives paging), while these TS methods aggregate every page by default and only single-page when a paginationToken is passed. You call this out in the description, and it's internally consistent with how TS listTools already drains pages, so the TS surface stays uniform. The listMaxPages passthrough (0 disables) is a clean escape hatch for the underlying 64-page ListPaginationExceeded cap — no Python equivalent needed since Python doesn't aggregate these.

  2. Lazy connect. _connectOrThrow() vs Python's context-manager + MCPClientInitializationError. This is an established cross-SDK lifecycle difference and matches TS callTool, not a regression.

One thing worth a quick confirm (non-blocking): getPrompt's args is optional (args?: Record<string, string>), whereas Python's get_prompt_sync(prompt_id, args) takes args as a required positional typed dict[str, Any]. The TS choice is arguably better — it matches Python's own docstring ("Optional arguments") and Record<string, string> more accurately reflects MCP prompt-argument substitution than dict[str, Any]. Just flagging the intentional divergence so it's a conscious call rather than an accident.

Also confirmed the wire mapping is correct: paginationParams maps paginationToken → { cursor }, getPrompt sends { name, arguments }, and readResource stringifies URL. Nice work — the parity story holds up.

@poshinchen poshinchen changed the title feat(mcp-ts): added mvp v2 prompts and resources methods feat(mcp-ts): added mvp v2 prompts, resources methods with paginationToken Sep 11, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api/needs-review Makes changes to the public API surface area-mcp MCP related complexity/low Touched functions have low cognitive complexity (<=10) enhancement New feature or request size/m typescript Pull requests that update typescript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PORT] MCP prompts and resources client APIs (Py→TS)

2 participants