diff --git a/.github/actions/conformance/client.py b/.github/actions/conformance/client.py index 18e59a8a..daad2769 100644 --- a/.github/actions/conformance/client.py +++ b/.github/actions/conformance/client.py @@ -20,7 +20,7 @@ Scenarios: json-schema-ref-no-deref - Connect, list tools (no $ref deref) json-schema-2020-12-preservation - List tools, echo the focal inputSchema back verbatim request-metadata - Connect with all callbacks; client stamps _meta - http-standard-headers - Connect, call a tool (Mcp-* headers checked) + http-standard-headers - Tool, resource and prompt round-trips (Mcp-* headers checked) http-invalid-tool-headers - List tools, call every surfaced tool (x-mcp-header filter) http-custom-headers - Replay the harness's toolCalls (x-mcp-header -> Mcp-Param-*) elicitation-sep1034-client-defaults - Elicitation with default accept callback @@ -311,11 +311,24 @@ async def run_request_metadata(server_url: str) -> None: @register("http-standard-headers") async def run_http_standard_headers(server_url: str) -> None: - """Connect on the modern path so Mcp-Method / Mcp-Name / MCP-Protocol-Version are sent (SEP-2243).""" + """Touch tools, resources and prompts on the modern path so each standard header is checked (SEP-2243). + + The scenario inspects Mcp-Method on each request and Mcp-Name on tools/call, + resources/read and prompts/get, and reports methods the client never sent as + SKIPPED rather than failed, so exercise one of each. initialize and + notifications/initialized stay SKIPPED: the modern path discovers via + server/discover and never sends them. + """ async with Client(server_url, mode=client_mode()) as client: - await client.list_tools() - result = await client.call_tool("add_numbers", {"a": 5, "b": 3}) - logger.debug(f"add_numbers result: {result}") + tools = await client.list_tools() + if tools.tools: + await client.call_tool(tools.tools[0].name, _stub_required_args(tools.tools[0].input_schema)) + resources = await client.list_resources() + if resources.resources: + await client.read_resource(resources.resources[0].uri) + prompts = await client.list_prompts() + if prompts.prompts: + await client.get_prompt(prompts.prompts[0].name) def _stub_required_args(input_schema: dict[str, Any]) -> dict[str, Any]: diff --git a/.github/scripts/docs_preview.js b/.github/scripts/docs_preview.js new file mode 100644 index 00000000..fae56022 --- /dev/null +++ b/.github/scripts/docs_preview.js @@ -0,0 +1,117 @@ +// Docs preview gate and PR comment. .github/workflows/docs-preview.yml wires +// these up: `authorize` decides whether a run may build and deploy a preview +// (and for which commit), `comment` reports the outcome on the pull request. +// The security model is described in the workflow's header. +'use strict'; + +const MARKER = ''; +const BOT_LOGIN = 'github-actions[bot]'; + +// Sets the job outputs `authorized`, `pr_number`, `head_sha` and +// `slash_attempt` for a pull_request_target or /preview-docs issue_comment run. +async function authorize({ github, context, core }) { + const { owner, repo } = context.repo; + + async function permissionFor(username) { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); + return { level: data.permission, role: data.role_name }; + } + + let authorized = false; + let prNumber = ''; + let headSha = ''; + let slashAttempt = false; + + if (context.eventName === 'pull_request_target') { + const pr = context.payload.pull_request; + prNumber = String(pr.number); + headSha = pr.head.sha; + // No automatic preview for fork PRs: actions/checkout refuses to fetch a + // fork's head in a pull_request_target run. A maintainer can still request + // one with /preview-docs. (head.repo is null once the fork is deleted.) + const headRepo = pr.head.repo; + if (!headRepo || headRepo.id !== context.payload.repository.id) { + core.info(`PR #${prNumber} head is on ${headRepo ? headRepo.full_name : 'a deleted fork'}; fork PRs are previewed via /preview-docs only.`); + } else { + // Gate on the *sender* (whoever caused this run — on synchronize that + // is the pusher), not the PR author, so a non-admin pushing to an + // admin-opened branch does not get an automatic build. + const actor = context.payload.sender.login; + const perm = await permissionFor(actor); + authorized = perm.level === 'admin'; + core.info(`pull_request_target by ${actor} (level=${perm.level}, role=${perm.role}) → authorized=${authorized}`); + } + } else { + // issue_comment: the job-level `if:` already guarantees this is a PR + // comment starting with /preview-docs. + slashAttempt = true; + const actor = context.payload.comment.user.login; + prNumber = String(context.payload.issue.number); + const perm = await permissionFor(actor); + authorized = perm.level === 'admin' || perm.role === 'maintain'; + if (authorized) { + const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(prNumber) }); + if (pr.state !== 'open') { + authorized = false; + core.info(`PR #${prNumber} is ${pr.state}; refusing to preview.`); + } else { + headSha = pr.head.sha; + } + } + core.info(`/preview-docs by ${actor} (level=${perm.level}, role=${perm.role}) → authorized=${authorized}`); + } + + core.setOutput('authorized', String(authorized)); + core.setOutput('pr_number', prNumber); + core.setOutput('head_sha', headSha); + core.setOutput('slash_attempt', String(slashAttempt)); +} + +// Posts or updates the preview comment on the PR. Reads the outcome of the +// earlier jobs from the step's env: AUTHORIZED, PR_NUMBER, HEAD_SHA, +// DEPLOY_RESULT, DEPLOYMENT_URL, ALIAS_URL, RUN_URL. +async function comment({ github, context }) { + const { owner, repo } = context.repo; + const env = process.env; + const issue_number = Number(env.PR_NUMBER); + + async function upsert(body) { + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); + const existing = comments.find((c) => c.user?.login === BOT_LOGIN && c.body?.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); + } else { + await github.rest.issues.createComment({ owner, repo, issue_number, body }); + } + } + + if (env.AUTHORIZED !== 'true') { + await github.rest.issues.createComment({ + owner, repo, issue_number, + body: `@${context.actor} — only repository admins or maintainers can run \`/preview-docs\` (and the PR must be open).`, + }); + return; + } + + if (env.DEPLOY_RESULT !== 'success') { + await upsert( + `${MARKER}\n### šŸ“š Documentation preview\n\n` + + `āŒ Preview build **failed** for \`${env.HEAD_SHA.slice(0, 7)}\` — [workflow logs](${env.RUN_URL}).` + ); + return; + } + + const previewUrl = env.ALIAS_URL || env.DEPLOYMENT_URL; + const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); + await upsert( + `${MARKER}\n### šŸ“š Documentation preview\n\n` + + `| | |\n|---|---|\n` + + `| **Preview** | ${previewUrl} |\n` + + `| **Deployment** | ${env.DEPLOYMENT_URL} |\n` + + `| **Commit** | \`${env.HEAD_SHA.slice(0, 7)}\` |\n` + + `| **Triggered by** | @${context.actor} |\n` + + `| **Updated** | ${ts} |\n` + ); +} + +module.exports = { authorize, comment }; diff --git a/.github/scripts/docs_preview.test.js b/.github/scripts/docs_preview.test.js new file mode 100644 index 00000000..cb178357 --- /dev/null +++ b/.github/scripts/docs_preview.test.js @@ -0,0 +1,241 @@ +// Scenario tests for docs_preview.js: who gets a preview build (`authorize`) +// and what the pull request shows afterwards (`comment`). +// +// node --test .github/scripts/docs_preview.test.js +// +// No dependencies; the GitHub client is a small fake defined at the bottom. +// CI runs it in the checks job (.github/workflows/shared.yml). +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { authorize, comment } = require('./docs_preview.js'); + +const REPO = { owner: 'modelcontextprotocol', repo: 'python-sdk' }; +const BASE_REPO = { id: 1, full_name: 'modelcontextprotocol/python-sdk' }; +const HEAD = 'e4dfda7baa127ab00ebcd1d5324560cbe3cdfe42'; +const MARKER = ''; + +// `permission` / `role_name` as the collaborators API reports them. +const PEOPLE = { + admin: { permission: 'admin', role_name: 'admin' }, + maintainer: { permission: 'write', role_name: 'maintain' }, + writer: { permission: 'write', role_name: 'write' }, + outsider: { permission: 'read', role_name: 'read' }, +}; + +// ── authorize ────────────────────────────────────────────────────────────── +// `expect` is the full set of job outputs the step writes. + +const authorizeScenarios = [ + { + name: 'admin pushes to (or opens) a PR → automatic preview of that head', + event: pushed(7, 'admin'), + expect: { authorized: 'true', pr_number: '7', head_sha: HEAD, slash_attempt: 'false' }, + }, + { + name: 'someone with write but not admin pushes → no automatic preview', + event: pushed(7, 'writer'), + expect: { authorized: 'false', pr_number: '7', head_sha: HEAD, slash_attempt: 'false' }, + }, + { + // actions/checkout refuses a fork's head under pull_request_target, so + // the run stops here instead of failing in `build`; /preview-docs still works. + name: 'admin pushes to or reopens a fork PR → no automatic preview', + event: pushed(7, 'admin', { fork: 'someone/python-sdk' }), + expect: { authorized: 'false', pr_number: '7', head_sha: HEAD, slash_attempt: 'false' }, + permissionLookups: 0, + }, + { + name: 'fork PR whose fork has since been deleted → no automatic preview', + event: pushed(7, 'admin', { fork: null }), + expect: { authorized: 'false', pr_number: '7', head_sha: HEAD, slash_attempt: 'false' }, + permissionLookups: 0, + }, + { + name: 'maintainer comments /preview-docs on a fork PR → previewed like any other', + pr: { fork: 'someone/python-sdk' }, + event: slash(7, 'maintainer'), + expect: { authorized: 'true', pr_number: '7', head_sha: HEAD, slash_attempt: 'true' }, + }, + { + name: 'maintainer comments /preview-docs on an open PR → preview of its current head', + event: slash(7, 'maintainer'), + expect: { authorized: 'true', pr_number: '7', head_sha: HEAD, slash_attempt: 'true' }, + }, + { + name: 'admin comments /preview-docs → authorized as well', + event: slash(7, 'admin'), + expect: { authorized: 'true', pr_number: '7', head_sha: HEAD, slash_attempt: 'true' }, + }, + { + name: 'writer without the maintain role comments /preview-docs → refused, recorded as an attempt', + event: slash(7, 'writer'), + expect: { authorized: 'false', pr_number: '7', head_sha: '', slash_attempt: 'true' }, + }, + { + name: 'outsider comments /preview-docs → refused, recorded as an attempt', + event: slash(7, 'outsider'), + expect: { authorized: 'false', pr_number: '7', head_sha: '', slash_attempt: 'true' }, + }, + { + name: '/preview-docs on a closed PR → refused even for a maintainer', + pr: { state: 'closed' }, + event: slash(7, 'maintainer'), + expect: { authorized: 'false', pr_number: '7', head_sha: '', slash_attempt: 'true' }, + }, +]; + +for (const s of authorizeScenarios) { + test(`authorize: ${s.name}`, async () => { + const world = makeWorld({ pr: { number: 7, ...s.pr } }); + assert.deepEqual(await runAuthorize(world, s.event), s.expect); + assert.equal(world.writes.length, 0); + if (s.permissionLookups !== undefined) assert.equal(world.permissionLookups, s.permissionLookups); + }); +} + +test('authorize: a failing permission lookup fails the step instead of deciding either way', async () => { + const world = makeWorld({ pr: { number: 7 } }); + world.failPermissionLookup = true; + await assert.rejects(runAuthorize(world, pushed(7, 'admin')), /boom/); +}); + +// ── comment ──────────────────────────────────────────────────────────────── + +const DEPLOYED = { + AUTHORIZED: 'true', + PR_NUMBER: '7', + HEAD_SHA: HEAD, + DEPLOY_RESULT: 'success', + DEPLOYMENT_URL: 'https://1a2b3c.mcp-python-sdk-docs.pages.dev', + ALIAS_URL: 'https://pr-7.mcp-python-sdk-docs.pages.dev', + RUN_URL: 'https://github.com/modelcontextprotocol/python-sdk/actions/runs/1', +}; + +test('comment: a refused /preview-docs gets a plain reply to the commenter, not a preview comment', async () => { + const world = makeWorld({ pr: { number: 7 } }); + await runComment(world, { ...DEPLOYED, AUTHORIZED: 'false', HEAD_SHA: '', DEPLOY_RESULT: 'skipped' }, 'outsider'); + assert.equal(world.comments.length, 1); + assert.match(world.comments[0].body, /^@outsider — only repository admins or maintainers can run `\/preview-docs`/); + assert.ok(!world.comments[0].body.includes(MARKER)); +}); + +test('comment: first successful deploy posts one preview comment linking the alias URL and the commit', async () => { + const world = makeWorld({ pr: { number: 7 } }); + await runComment(world, DEPLOYED, 'admin'); + assert.equal(world.comments.length, 1); + const body = world.comments[0].body; + assert.ok(body.startsWith(`${MARKER}\n### šŸ“š Documentation preview`)); + assert.match(body, /\| \*\*Preview\*\* \| https:\/\/pr-7\.mcp-python-sdk-docs\.pages\.dev \|/); + assert.match(body, /\| \*\*Deployment\*\* \| https:\/\/1a2b3c\.mcp-python-sdk-docs\.pages\.dev \|/); + assert.match(body, /\| \*\*Commit\*\* \| `e4dfda7` \|/); + assert.match(body, /\| \*\*Triggered by\*\* \| @admin \|/); + assert.deepEqual(world.writes, ['comment on #7']); +}); + +test('comment: a later deploy edits the existing preview comment instead of adding another', async () => { + const world = makeWorld({ pr: { number: 7 }, comments: [{ user: 'someone', body: 'LGTM' }, { user: 'github-actions[bot]', body: `${MARKER}\nold table` }] }); + await runComment(world, { ...DEPLOYED, HEAD_SHA: 'f'.repeat(40) }, 'admin'); + assert.equal(world.comments.length, 2); + assert.match(world.comments[1].body, /\| \*\*Commit\*\* \| `fffffff` \|/); + assert.deepEqual(world.writes, ['edit comment 101']); +}); + +test("comment: someone else's comment that happens to contain the marker is left alone", async () => { + const world = makeWorld({ pr: { number: 7 }, comments: [{ user: 'someone', body: `quoting ${MARKER} here` }] }); + await runComment(world, DEPLOYED, 'admin'); + assert.equal(world.comments.length, 2); + assert.equal(world.comments[0].body, `quoting ${MARKER} here`); + assert.deepEqual(world.writes, ['comment on #7']); +}); + +test('comment: with no alias URL the preview link falls back to the deployment URL', async () => { + const world = makeWorld({ pr: { number: 7 } }); + await runComment(world, { ...DEPLOYED, ALIAS_URL: '' }, 'admin'); + assert.match(world.comments[0].body, /\| \*\*Preview\*\* \| https:\/\/1a2b3c\.mcp-python-sdk-docs\.pages\.dev \|/); +}); + +test('comment: a build or deploy that did not succeed is reported with the short SHA and a link to the run', async () => { + const world = makeWorld({ pr: { number: 7 }, comments: [{ user: 'github-actions[bot]', body: `${MARKER}\nold table` }] }); + await runComment(world, { ...DEPLOYED, DEPLOY_RESULT: 'skipped', DEPLOYMENT_URL: '', ALIAS_URL: '' }, 'admin'); + assert.equal(world.comments.length, 1); + assert.equal( + world.comments[0].body, + `${MARKER}\n### šŸ“š Documentation preview\n\nāŒ Preview build **failed** for \`e4dfda7\` — [workflow logs](${DEPLOYED.RUN_URL}).` + ); +}); + +// ── Harness ──────────────────────────────────────────────────────────────── + +// `fork`: full name of the fork the head lives on; null for a deleted fork; omitted for a same-repo branch. +function pushed(number, sender, { fork } = {}) { + const repo = fork === undefined ? BASE_REPO : fork === null ? null : { id: 2, full_name: fork }; + return { eventName: 'pull_request_target', actor: sender, payload: { action: 'synchronize', repository: BASE_REPO, pull_request: { number, head: { sha: HEAD, repo } }, sender: { login: sender } } }; +} +function slash(number, commenter) { + return { eventName: 'issue_comment', actor: commenter, payload: { action: 'created', issue: { number, pull_request: {} }, comment: { body: '/preview-docs', user: { login: commenter } } } }; +} + +async function runAuthorize(world, event) { + const outputs = {}; + const core = { info: () => {}, setOutput: (k, v) => { outputs[k] = v; } }; + await authorize({ github: world.github, context: { repo: REPO, ...event }, core }); + return outputs; +} + +async function runComment(world, env, actor) { + const saved = {}; + for (const [k, v] of Object.entries(env)) { saved[k] = process.env[k]; process.env[k] = v; } + try { + await comment({ github: world.github, context: { repo: REPO, actor }, core: {} }); + } finally { + for (const [k, v] of Object.entries(saved)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; } + } +} + +// ── A tiny in-memory GitHub ──────────────────────────────────────────────── + +function makeWorld({ pr, comments = [] }) { + const world = { pr: { state: 'open', ...pr }, comments: [], writes: [], failPermissionLookup: false, permissionLookups: 0, nextCommentId: 100 }; + for (const c of comments) world.comments.push({ id: world.nextCommentId++, ...c }); + + const err = (status, message = 'fake error') => Object.assign(new Error(message), { status }); + const write = (what) => world.writes.push(what); + const checkPr = (n) => { if (n !== world.pr.number) throw err(404); }; + + const rest = { + repos: { + getCollaboratorPermissionLevel: async ({ username }) => { + world.permissionLookups++; + if (world.failPermissionLookup) throw err(500, 'boom'); + const person = PEOPLE[username]; + if (!person) throw err(404, 'not a user'); + return { data: { ...person, user: { login: username } } }; + }, + }, + pulls: { + get: async ({ pull_number }) => { + checkPr(pull_number); + const repo = world.pr.fork ? { id: 2, full_name: world.pr.fork } : BASE_REPO; + return { data: { number: pull_number, state: world.pr.state, head: { sha: HEAD, repo } } }; + }, + }, + issues: { + listComments: async ({ issue_number }) => { checkPr(issue_number); return { data: world.comments.map((c) => ({ id: c.id, body: c.body, user: { login: c.user } })) }; }, + createComment: async ({ issue_number, body }) => { + checkPr(issue_number); + write(`comment on #${issue_number}`); + world.comments.push({ id: world.nextCommentId++, user: 'github-actions[bot]', body }); + }, + updateComment: async ({ comment_id, body }) => { + write(`edit comment ${comment_id}`); + const c = world.comments.find((x) => x.id === comment_id); + if (!c) throw err(404); + c.body = body; + }, + }, + }; + world.github = { rest, paginate: async (fn, args) => (await fn(args)).data }; + return world; +} diff --git a/.github/scripts/pr_intake_gate.js b/.github/scripts/pr_intake_gate.js index 40f429d6..0fb2864d 100644 --- a/.github/scripts/pr_intake_gate.js +++ b/.github/scripts/pr_intake_gate.js @@ -78,11 +78,10 @@ module.exports = async function run({ github, context, core }) { if (pr.merged_at) return log('merged — nothing to do'); if (pr.state === 'closed' && !gated) return log('closed by someone else — not ours'); - // 1. Exempt authors: bots, anyone with triage or better, and drafts (which - // are checked again on ready_for_review). + // 1. Exempt authors: bots and anyone with triage or better. Drafts are + // gated like any other PR. if (pr.user.type === 'Bot') return log('author is a bot — exempt'); if (await isTrusted(pr.user.login)) return pass('author has triage+ on this repo'); - if (pr.draft) return log('draft — skipped until ready for review'); // 2. Overrides: a triage+ user reopening the PR or removing the label wants // it open. Anyone else doing so just triggers a re-check. @@ -122,7 +121,7 @@ module.exports = async function run({ github, context, core }) { async function fail(linkedIssues) { console.log(`FAIL: ${linkedIssues.length ? `not assigned to ${linkedIssues.map((n) => `#${n}`).join(', ')}` : 'no usable issue link'}`); await addLabel(prNumber, LABEL); - await upsertGateComment(prNumber, closedComment(linkedIssues)); + await upsertGateComment(prNumber, closedComment(pr.draft, linkedIssues)); if (pr.state === 'open') { await mutate(`close PR #${prNumber}`, () => github.rest.pulls.update({ owner, repo, pull_number: prNumber, state: 'closed' })); } @@ -135,23 +134,37 @@ module.exports = async function run({ github, context, core }) { // ── Comment text ───────────────────────────────────────────────────────── - function closedComment(linkedIssues) { + function closedComment(draft, linkedIssues) { const issues = linkedIssues.map((n) => `#${n}`).join(', '); - const why = linkedIssues.length - ? `you aren't currently assigned to ${issues}` - : "its description doesn't yet link an open issue in this repository (with `Fixes #123` or similar)"; - const next = linkedIssues.length - ? `If a maintainer would like this change as a PR from you, they'll assign you to ${issues} and this PR will reopen automatically — there's nothing more you need to do. (If you opened the issue, this PR already shows up on its timeline.)` - : `If there isn't an issue for this yet, please [open one](https://github.com/${owner}/${repo}/issues/new/choose) — a clear description of the problem is genuinely the most useful thing for us. Then add \`Fixes #\` to this PR's description. If a maintainer would like the change as a PR from you, they'll assign you to the issue and this PR will reopen automatically.`; + const opener = draft + ? "This PR has been closed automatically. It's still a draft, but we close those early so you don't put in more time only to have it closed the moment you mark it ready.\n\n" + : 'This PR has been closed automatically. '; + const rule = `${opener}This repo only keeps pull requests open when they come from a maintainer, or from a contributor a maintainer has assigned to the linked issue`; + const situation = linkedIssues.length + ? [ + `${rule}, and you aren't currently assigned to ${issues}.`, + '', + `If a maintainer assigns you to ${issues}, this PR reopens on its own and there's nothing more you need to do here. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. What does help is engaging on the issue itself by confirming the repro, explaining why it matters for your use case, or describing the approach you'd take.`, + ] + : [ + `${rule}, and this PR doesn't link an open issue yet.`, + '', + "- **If you're already assigned to an issue for this**, add `Fixes #` to the description and the PR will reopen on its own.", + `- **If there's no issue yet**, please [open one](https://github.com/${owner}/${repo}/issues/new/choose) instead: what you ran into, why it matters for your use case, and a minimal reproduction. That context is super important to us and is what we use to decide what to prioritise.`, + "- **If there's an issue but you're not assigned**, add `Fixes #` anyway so they're linked, then engage on the issue itself by confirming the repro or describing the approach you'd take. Assignment is a maintainer call based on capacity; comments that only ask to be assigned don't factor in. If you are assigned, this PR reopens automatically.", + ]; return [ MARKER, - `Thanks for the contribution. This repository only keeps pull requests open when they're linked to an issue that a maintainer has assigned to the author — [CONTRIBUTING.md](${contributingUrl}) explains why and how we work. This PR has been closed for now because ${why}.`, + ...situation, '', - next, + "You're welcome to keep pushing commits here (just avoid force-pushing, since GitHub can't reopen a rewritten branch), but that on its own won't get the PR reviewed or the issue assigned, and realistically most auto-closed PRs stay closed. There's no need to open a new PR either way.", '', - "There's no need to open a new PR — this one will be reopened. While it's closed, please push any updates as new commits rather than force-pushing, since GitHub can't reopen a PR whose branch has been rewritten.", + `[CONTRIBUTING.md](${contributingUrl}) has the full reasoning, but in short:`, '', - `*Maintainers: reopening this PR, removing the \`${LABEL}\` label, or adding \`${BYPASS_LABEL}\` bypasses the check.*`, + "- We're a small team with very little capacity to review community PRs right now.", + '- Many recent PRs are AI-generated with little human review, and reviewing one carefully still costs a maintainer as much time as it ever did. A well-described issue is usually more useful to us than the code.', + '', + `*Maintainers: reopen, remove \`${LABEL}\`, or add \`${BYPASS_LABEL}\` to override.*`, ].join('\n'); } diff --git a/.github/scripts/pr_intake_gate.test.js b/.github/scripts/pr_intake_gate.test.js index 75c96247..a7cde4bf 100644 --- a/.github/scripts/pr_intake_gate.test.js +++ b/.github/scripts/pr_intake_gate.test.js @@ -28,7 +28,8 @@ const PEOPLE = { // ── Scenarios ────────────────────────────────────────────────────────────── // `prs` / `issues` describe the world before the event; `expect` describes each // PR afterwards: state, labels, and comment ('closed' = the "this PR has been -// closed" comment, 'cannot-reopen' = the refused-reopen comment, null = none). +// closed" comment, 'closed-draft' = its draft wording, 'cannot-reopen' = the +// refused-reopen comment, null = none). // `writes: 0` additionally asserts the gate touched nothing at all. const scenarios = [ @@ -146,14 +147,13 @@ const scenarios = [ writes: 0, }, { - name: 'draft PR is skipped until it is marked ready for review', + name: 'draft PR with no issue link is closed like any other, with a note about why drafts are closed early', prs: [pr(3300, 'outsider', { draft: true })], event: opened(3300, 'outsider'), - expect: { 3300: { state: 'open', labels: [], comment: null } }, - writes: 0, + expect: { 3300: { state: 'closed', labels: [LABEL], comment: 'closed-draft' } }, }, { - name: 'draft marked ready for review with no link → closed', + name: 'pre-existing draft marked ready for review with no link → closed', prs: [pr(3300, 'outsider')], event: readyForReview(3300, 'outsider'), expect: { 3300: { state: 'closed', labels: [LABEL], comment: 'closed' } }, @@ -299,7 +299,8 @@ function observe(world, expect) { const p = world.prs.get(Number(num)); const gateComments = p.comments.filter((c) => c.user === 'github-actions[bot]' && c.body.includes('')); assert.ok(gateComments.length <= 1, `PR #${num} has ${gateComments.length} gate comments`); - const kind = !gateComments.length ? null : gateComments[0].body.includes("won't let it be reopened") ? 'cannot-reopen' : 'closed'; + const body = gateComments[0]?.body; + const kind = !body ? null : body.includes("won't let it be reopened") ? 'cannot-reopen' : body.includes('still a draft') ? 'closed-draft' : 'closed'; out[num] = { state: p.state, labels: [...p.labels].sort(), comment: kind }; if ('foreignComments' in expect[num]) out[num].foreignComments = p.comments.length - gateComments.length; } diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 859055da..05e4964c 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -27,14 +27,14 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 1 persist-credentials: false - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@d5726de019ec4498aa667642bc3a80fca83aa102 # v1.0.148 + uses: anthropics/claude-code-action@d40ddef4c030e508327d6e35a9c45f3368482c50 # v1.0.195 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # zizmor: ignore[secrets-outside-env] use_commit_signing: true diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 65b014b6..64c334ef 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -38,14 +38,14 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 - run: uv sync --frozen --all-extras --package mcp-everything-server @@ -84,14 +84,14 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 24 # --compile-bytecode: without it, ~40 concurrently spawned interpreters diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 28ae351f..eaf1c167 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -42,12 +42,12 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 278aa442..e54f5d16 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -4,11 +4,16 @@ name: Docs Preview # # Security: the build executes Python from the PR (mkdocstrings imports # src/mcp, `!!python/name:` config directives run, and heads may ship their -# own build scripts). The build is gated by `authorize` (admin sender for -# auto-preview, admin/maintainer commenter for /preview-docs) and isolated -# from Cloudflare secrets — `build` runs PR code with no secrets and hands -# the static site to `deploy` via an artifact, so PR code never shares a -# runner with the Cloudflare token. +# own build scripts). The build is gated by `authorize` (admin sender on a +# same-repo branch for auto-preview, admin/maintainer commenter for +# /preview-docs) and isolated from Cloudflare secrets — `build` runs PR code +# with no secrets and hands the static site to `deploy` via an artifact, so +# PR code never shares a runner with the Cloudflare token. Fork PRs get no +# automatic preview: actions/checkout refuses to fetch a fork's head in a +# pull_request_target run, so a maintainer requests one with /preview-docs, +# which runs under issue_comment with the same gating and isolation. +# `authorize` and `comment` run .github/scripts/docs_preview.js, checked out +# from the default branch only; those two checkouts must never take a `ref:`. # # Required configuration: # - secrets.CLOUDFLARE_API_TOKEN (scope: Account → Cloudflare Pages → Edit) @@ -58,57 +63,20 @@ jobs: head_sha: ${{ steps.check.outputs.head_sha }} slash_attempt: ${{ steps.check.outputs.slash_attempt }} steps: + - name: Check out the scripts (default branch) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # No `ref:` here, ever: this job trusts what it checks out (see header). + persist-credentials: false + sparse-checkout: .github/scripts + - name: Determine authorization id: check - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { owner, repo } = context.repo; - - async function permissionFor(username) { - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username }); - return { level: data.permission, role: data.role_name }; - } - - let authorized = false; - let prNumber = ''; - let headSha = ''; - let slashAttempt = false; - - if (context.eventName === 'pull_request_target') { - // Gate on the *sender* (whoever caused this run — on synchronize that - // is the pusher), not the PR author, so a non-admin pushing to an - // admin-opened branch does not get an automatic build. - const actor = context.payload.sender.login; - prNumber = String(context.payload.pull_request.number); - headSha = context.payload.pull_request.head.sha; - const perm = await permissionFor(actor); - authorized = perm.level === 'admin'; - core.info(`pull_request_target by ${actor} (level=${perm.level}, role=${perm.role}) → authorized=${authorized}`); - } else { - // issue_comment: the job-level `if:` already guarantees this is a PR - // comment starting with /preview-docs. - slashAttempt = true; - const actor = context.payload.comment.user.login; - prNumber = String(context.payload.issue.number); - const perm = await permissionFor(actor); - authorized = perm.level === 'admin' || perm.role === 'maintain'; - if (authorized) { - const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: Number(prNumber) }); - if (pr.state !== 'open') { - authorized = false; - core.info(`PR #${prNumber} is ${pr.state}; refusing to preview.`); - } else { - headSha = pr.head.sha; - } - } - core.info(`/preview-docs by ${actor} (level=${perm.level}, role=${perm.role}) → authorized=${authorized}`); - } - - core.setOutput('authorized', String(authorized)); - core.setOutput('pr_number', prNumber); - core.setOutput('head_sha', headSha); - core.setOutput('slash_attempt', String(slashAttempt)); + const { authorize } = require('./.github/scripts/docs_preview.js'); + await authorize({ github, context, core }); build: needs: authorize @@ -117,27 +85,28 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.authorize.outputs.head_sha }} persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: - # pull_request_target runs share the base-branch Actions cache; saving - # a cache populated while untrusted PR code ran would let it poison - # later trusted workflows. Mirrors publish-pypi.yml. + # Keep the untrusted build away from the shared Actions cache. GitHub + # already limits pull_request_target and issue_comment runs to + # read-only access in the default branch's cache scope, so this is + # defence in depth (and avoids a refused save in the post step). + # Mirrors publish-pypi.yml. enable-cache: false version: 0.9.5 - # pull_request_target runs this workflow file from the base branch, so - # the whole recipe — dependency sync included — must come from the - # checkout itself: heads that ship scripts/docs/build.sh (the Zensical - # toolchain) build with it; older heads, and v1.x heads previewed via - # /preview-docs, still build with MkDocs. Both arms must write the site - # to site/. Keep the detection in sync with build_site() in - # scripts/build-docs.sh. + # Both triggers run this workflow file from the default branch (whatever + # the PR targets), so the whole recipe — dependency sync included — must + # come from the checkout itself: heads that ship scripts/docs/build.sh + # (the Zensical toolchain) build with it; older heads and v1.x heads still + # build with MkDocs. Both arms must write the site to site/. Keep the + # detection in sync with build_site() in scripts/build-docs.sh. - run: | if [ -f scripts/docs/build.sh ]; then bash scripts/docs/build.sh @@ -192,10 +161,18 @@ jobs: (needs.authorize.outputs.authorized == 'true' || needs.authorize.outputs.slash_attempt == 'true') runs-on: ubuntu-latest permissions: + contents: read # check out the script from the default branch pull-requests: write steps: + - name: Check out the scripts (default branch) + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + # No `ref:` here, ever: this job trusts what it checks out (see header). + persist-credentials: false + sparse-checkout: .github/scripts + - name: Post or update preview comment - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: AUTHORIZED: ${{ needs.authorize.outputs.authorized }} PR_NUMBER: ${{ needs.authorize.outputs.pr_number }} @@ -206,45 +183,5 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} with: script: | - const { owner, repo } = context.repo; - const env = process.env; - const issue_number = Number(env.PR_NUMBER); - const marker = ''; - - async function upsert(body) { - const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number, per_page: 100 }); - const existing = comments.find(c => c.user?.login === 'github-actions[bot]' && c.body?.includes(marker)); - if (existing) { - await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }); - } else { - await github.rest.issues.createComment({ owner, repo, issue_number, body }); - } - } - - if (env.AUTHORIZED !== 'true') { - await github.rest.issues.createComment({ - owner, repo, issue_number, - body: `@${context.actor} — only repository admins or maintainers can run \`/preview-docs\` (and the PR must be open).`, - }); - return; - } - - if (env.DEPLOY_RESULT !== 'success') { - await upsert( - `${marker}\n### šŸ“š Documentation preview\n\n` + - `āŒ Preview build **failed** for \`${env.HEAD_SHA.slice(0, 7)}\` — [workflow logs](${env.RUN_URL}).` - ); - return; - } - - const previewUrl = env.ALIAS_URL || env.DEPLOYMENT_URL; - const ts = new Date().toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC'); - await upsert( - `${marker}\n### šŸ“š Documentation preview\n\n` + - `| | |\n|---|---|\n` + - `| **Preview** | ${previewUrl} |\n` + - `| **Deployment** | ${env.DEPLOYMENT_URL} |\n` + - `| **Commit** | \`${env.HEAD_SHA.slice(0, 7)}\` |\n` + - `| **Triggered by** | @${context.actor} |\n` + - `| **Updated** | ${ts} |\n` - ); + const { comment } = require('./.github/scripts/docs_preview.js'); + await comment({ github, context, core }); diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 946ac3db..73b8f87a 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -13,12 +13,12 @@ jobs: runs-on: ubuntu-latest needs: [checks] steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install uv - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: false version: 0.12.5 diff --git a/.github/workflows/require-linked-issue.yml b/.github/workflows/require-linked-issue.yml index de036ebf..309978e8 100644 --- a/.github/workflows/require-linked-issue.yml +++ b/.github/workflows/require-linked-issue.yml @@ -4,9 +4,9 @@ # In short: a PR from someone without triage rights stays open only if it links # an open issue here that is assigned to them (or labeled `help wanted`); # otherwise it is labeled `missing-issue-link`, gets one comment, and is closed, -# and it reopens automatically once the author is assigned. Bots and drafts are -# skipped. A triage+ user reopening the PR, removing the label, or adding -# `bypass-issue-check` overrides. +# and it reopens automatically once the author is assigned. Drafts are gated +# too; bots are skipped. A triage+ user reopening the PR, removing the label, +# or adding `bypass-issue-check` overrides. # # Operating it: # - Live by default. To pause it without a revert, set the repository @@ -72,13 +72,13 @@ jobs: PR_NUMBER_INPUT: ${{ inputs.pr_number }} steps: - name: Check out the gate script (default branch) - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: .github/scripts - name: Evaluate - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const run = require('./.github/scripts/pr_intake_gate.js'); diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index 5d363730..ef6eb25a 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -13,23 +13,12 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # setup-uv's manifest fetch is a single request with a hard 5s timeout - # (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry. - name: Install uv - id: setup-uv - continue-on-error: true - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - version: 0.9.5 - - - name: Install uv (retry) - if: steps.setup-uv.outcome == 'failure' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 @@ -41,11 +30,11 @@ jobs: with: extra_args: --all-files --verbose - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24 # match the runtime actions/github-script@v8 uses - - name: PR intake gate scenarios - run: node --test .github/scripts/pr_intake_gate.test.js + node-version: 24 # match actions/github-script's node24 runtime + - name: Workflow script tests + run: node --test '.github/scripts/*.test.js' - name: Surface types match vendored schema run: | @@ -75,23 +64,12 @@ jobs: os: [ubuntu-latest, windows-latest] steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # setup-uv's manifest fetch is a single request with a hard 5s timeout - # (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry. - name: Install uv - id: setup-uv - continue-on-error: true - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - version: 0.9.5 - - - name: Install uv (retry) - if: steps.setup-uv.outcome == 'failure' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 @@ -121,23 +99,12 @@ jobs: readme-snippets: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # setup-uv's manifest fetch is a single request with a hard 5s timeout - # (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry. - name: Install uv - id: setup-uv - continue-on-error: true - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - version: 0.9.5 - - - name: Install uv (retry) - if: steps.setup-uv.outcome == 'failure' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 @@ -161,23 +128,12 @@ jobs: docs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # setup-uv's manifest fetch is a single request with a hard 5s timeout - # (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry. - name: Install uv - id: setup-uv - continue-on-error: true - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 - with: - enable-cache: true - version: 0.9.5 - - - name: Install uv (retry) - if: steps.setup-uv.outcome == 'failure' - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: enable-cache: true version: 0.9.5 diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index f66ff742..598e76df 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -17,9 +17,9 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run zizmor 🌈 - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57c2b14a..a5864c63 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ Whether to assign an issue, and to whom, is a [maintainer](https://github.com/mo We use AI tooling constantly and have no problem with you using it too. What matters is that a person is accountable for the result: -- **Disclose it.** One line in the PR or issue description. +- **Disclose it.** One line in the PR description, issue, or comment. - **Own it.** You can explain the change and the reasoning in your own words, and when a maintainer asks a question the answer comes from you rather than being pasted from a chat window. - **Keep a human in the loop.** Issues, PRs, and comments generated by an agent without a person who has actually hit the problem and read the output may be closed without warning. If you have an agent filing PRs against open issues autonomously, please turn it off for this repository. - **Keep issues short and factual.** What happened, what you expected, and how to reproduce it. diff --git a/docs/advanced/apps.md b/docs/advanced/apps.md index a60c997b..3b6232ca 100644 --- a/docs/advanced/apps.md +++ b/docs/advanced/apps.md @@ -20,7 +20,7 @@ then come back. ## A clock with a face -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -51,15 +51,36 @@ The model reads `content`; the iframe is for humans. A UI-capable host still fee the text result to the model, and a text-only client gets *only* that. So the canonical pattern is one tool, two answers. Look at `get_time` again: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` `client_supports_apps(ctx)` is `True` only when the client declared the `io.modelcontextprotocol/ui` extension **and** listed `text/html;profile=mcp-app` in its `mimeTypes` settings. The field is required, so a client that omits it -does not count. That is exactly what `main()` in the same file declares: the -client half of the negotiation, and the rich answer comes back. +does not count. Here is the client half of the negotiation: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Serve `server.py` over HTTP, then run the client from a second terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +The rich answer came back. Drop `extensions=[APPS_SUPPORT]` from the `Client` call +and the same program prints `The time is 2026-06-26T12:00:00Z.` instead, which is +all a text-only client ever sees. !!! warning Never return a placeholder like `"[Rendered UI]"` as the only content. If the diff --git a/docs/advanced/extensions.md b/docs/advanced/extensions.md index de7937fc..b8372da3 100644 --- a/docs/advanced/extensions.md +++ b/docs/advanced/extensions.md @@ -57,7 +57,7 @@ specified by the MCP project itself. The smallest useful extension is one tool and a settings map: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -69,18 +69,25 @@ The smallest useful extension is one tool and a settings map: * The extension never receives the server. It declares contributions as data; `MCPServer` consumes them. There is no `self.server` to mutate. -And `main()` is the proof, an in-memory client straight against `mcp`: +Serve it over HTTP, and a client is the proof: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Every `server.py` on this page is served with that command, and every `client.py` +runs beside it with `python client.py` from a second terminal. + ### Serving your own methods An extension can register **new request methods**: its own verbs, served next to the spec's: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -107,10 +114,10 @@ runtime: ### The client side -The same file's `main()` is the whole client story, both halves of it: +The client is its own program, and it carries both halves of the client story: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` declares the extension. The @@ -122,6 +129,9 @@ The same file's `main()` is the whole client story, both halves of it: * Vendor methods drop one layer to `client.session.send_request(...)`; `Client` only grows first-class methods for spec verbs. `send_request` accepts any `Request` subclass, so the vendor request passes as-is. +* `SearchRequest` and the two models it carries are the extension's wire contract, + so the client declares them for itself. A published extension would ship them in + a package that both sides import. ### Intercepting `tools/call` @@ -155,13 +165,20 @@ The hook wraps `tools/call` and nothing else. For every-message concerns, use ## Using a client extension A **client extension** is the same contract from the consuming side: a bundle of -client-side behaviour behind one identifier. Pass instances to -`Client(extensions=[...])` and call tools normally: +client-side behaviour behind one identifier. The server here answers `buy` with a +receipt to redeem instead of the goods, and only for a client that declared the +extension: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +On the client, pass instances to `Client(extensions=[...])` and call tools normally: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` returns a plain `CallToolResult`, like every other call. What the extension changed: the server may now answer `buy` with a `receipt` **result shape** instead of a final result, and `Receipts` finishes it (here by redeeming the @@ -180,7 +197,7 @@ the capability, the client does nothing, as in the search client above), use ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## Writing a client extension @@ -188,8 +205,8 @@ client = Client(mcp, extensions=[advertise("com.example/search")]) Subclass `ClientExtension` and override only what you need. Three contribution kinds, each with a default: `settings()`, `claims()`, and `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * The identifier follows the same grammar as the server's, validated when the class @@ -227,14 +244,21 @@ claimed shape reaching a session-tier caller raises `UnexpectedClaimedResult`. An extension's own request methods need no client-side registration. A vendor request type subclasses `mcp.types.Request` and goes through `client.session.send_request`, -as in [Serving your own methods](#serving-your-own-methods). One addition: when a -params key must ride the `Mcp-Name` header (extension specs such as tasks require -this for their verbs), the request type declares `name_param`: +as in [Serving your own methods](#serving-your-own-methods). Take a server whose +extension serves one verb about a named job: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +One addition on the client: when a params key must ride the `Mcp-Name` header +(extension specs such as tasks require this for their verbs), the request type +declares `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + The session mirrors `params["jobId"]` into `Mcp-Name` on every send path, and a missing value fails loudly rather than silently omitting a required header. diff --git a/docs/advanced/low-level-server.md b/docs/advanced/low-level-server.md index 575ecb8f..5d49846b 100644 --- a/docs/advanced/low-level-server.md +++ b/docs/advanced/low-level-server.md @@ -31,18 +31,22 @@ Three things changed, and they are the whole low-level API: ### Try it -There is no Inspector for this one: `mcp dev` and `mcp run` only accept an `MCPServer`. The in-memory `Client` doesn't care; it takes a low-level `Server` exactly like it takes an `MCPServer`: +`mcp dev` and `mcp run` only accept an `MCPServer`, so you serve this one yourself. The last line of `server.py` builds an ordinary ASGI app from it, and uvicorn runs that: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Point the Inspector, or any client, at `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -59,6 +63,8 @@ The same text the `@mcp.tool()` version produced. Two honest differences: * `result.structured_content` is `None`. The high-level server wraps a `-> str` into `{"result": ...}` for you; here nobody builds what you didn't build. * `list_tools` returns the schema **you** typed, character for character. The high-level version had `"title": "Query"` on every property and a `"title": "search_booksArguments"` at the root: Pydantic artifacts. Down here, if it's on the wire, you put it there. +In a test you skip uvicorn and the port: `Client(server)` takes a low-level `Server` in-process exactly like it takes an `MCPServer`, and **[Testing](../get-started/testing.md)** is that pattern. + ## Nothing is checked for you `MCPServer` rejects a bad argument before your function ever runs, validating the call against the schema it generated (**[Tools](../servers/tools.md)**). @@ -210,4 +216,4 @@ Each of these is one idea you now have the vocabulary for; each has its own page * `add_request_handler(method, params_type, handler)` serves any method. `initialize` is reserved. * The capabilities a `Server` advertises are derived from which handlers you registered. -`Client(server)` treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **[Middleware](middleware.md)**. +The client treated both servers identically because they *are* the same protocol, which is the whole point. The next layer down isn't a class at all: it's **[Middleware](middleware.md)**. diff --git a/docs/advanced/pagination.md b/docs/advanced/pagination.md index 9f807a8e..1549f39d 100644 --- a/docs/advanced/pagination.md +++ b/docs/advanced/pagination.md @@ -26,9 +26,13 @@ Pagination is for the server whose resource list is really a database: thousands ### Try it -`Client(server)` connects to a low-level `Server` in memory exactly as it connects to an `MCPServer`. +`mcp run` only accepts an `MCPServer`, so you serve this one yourself. The last line of `server.py` builds an ordinary ASGI app from the `Server`, and uvicorn runs that: -Call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Point any client (**[The Client](../client/index.md)**, or the Inspector) at `http://localhost:8000/mcp` and call `list_resources()` with no arguments. You get ten resources, `book-1` through `book-10`, and `next_cursor` is the string `"10"`. Hand it back with `list_resources(cursor="10")` and the first resource is `book-11`, the new `next_cursor` is `"20"`. @@ -38,7 +42,7 @@ The tenth page comes back with `next_cursor` set to `None`. Done. Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) takes a `cursor=` keyword. Draining a paged list is one `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -46,7 +50,7 @@ Every `list_*` method on `Client` (`list_tools`, `list_resources`, `list_resourc * Extend **before** you look at `next_cursor`: the last page has resources too. * `next_cursor is None` is the exit. Anything else goes straight back into `cursor=`, untouched. -Run its `main()` and it prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages. +With uvicorn still serving `server.py`, run `python client.py` in a second terminal. It prints `100 resources`: ten pages of ten, stitched together by a loop that never knew there were ten pages. This is the same loop **[The Client](../client/index.md)** shows for every `list_*` verb, and it costs nothing against a server that doesn't page: `next_cursor` is `None` on the first response and the loop runs once. diff --git a/docs/client/caching.md b/docs/client/caching.md index 5d83cfa9..544fe96a 100644 --- a/docs/client/caching.md +++ b/docs/client/caching.md @@ -25,7 +25,7 @@ Out of the box every result says `ttlMs: 0, cacheScope: "private"`: immediately On the low-level `Server`, handlers build their results by hand, and `ttl_ms` / `cache_scope` are just fields on the result models. A handler that sets them explicitly always wins over the constructor map, field by field: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -39,10 +39,24 @@ One caveat on paginated lists: the protocol requires the **same `cacheScope` on On a 2026-07-28 session, `Client` honors the hints for you: it has a built-in response cache, on by default. A result that arrives carrying a `ttlMs` is stored, and an identical call within that TTL is served from the cache with no round trip. A result that carries *no* hint is not cached: hint-less results get `CacheConfig.default_ttl_ms`, which defaults to `0` (immediately stale), so a server that declares nothing sees exactly the call-for-call traffic it always did. -```python title="client.py" hl_lines="33 35 38" +To watch that happen, serve the `server.py` from the previous section with uvicorn (its last line builds the ASGI app). The handler prints a line every time it actually runs: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Run `python client.py` from a second terminal. It prints the hints the first result carried, the handler's `ttlMs` next to the map's `cacheScope`: + +```text +1000 public +``` + +The server's terminal tells the rest of the story: between uvicorn's request logs, `tools/list served` appears three times. + Four calls, three fetches. The second call found a fresh entry and never reached the server; advancing the (injected) clock past the TTL made the third fetch again; the fourth said `cache_mode="refresh"`. That kwarg exists on the five caching verbs (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (the default) serves a fresh entry if there is one, and stores the fetch if not. @@ -51,7 +65,7 @@ Four calls, three fetches. The second call found a fresh entry and never reached One rule sits above `"use"`: **calls carrying `meta` always reach the server.** A request with `meta` set (a progress token, tracing fields) expects a wire request, so under `cache_mode="use"` it is treated as `"refresh"`: the cache read is skipped, and the fetched result still replaces the cached entry. `"bypass"` and an explicit `"refresh"` behave as they always do. -To turn caching off entirely, construct with `Client(server, cache=None)`: every call is a round trip again, and `cache_mode`, while still accepted, does nothing. +To turn caching off entirely, pass `cache=None` when constructing the `Client`: every call is a round trip again, and `cache_mode`, while still accepted, does nothing. Scope is honored automatically too: `"private"` entries are keyed to the cache's *partition* (below), while `"public"` ones may opt into wider sharing. And **notifications beat TTL** for the exact entries they name: a `list_changed` notification evicts the matching cached listing, and `resources/updated` evicts the cached read stored under exactly its URI, however fresh they were. On a 2026-07-28 connection those notifications arrive on a `subscriptions/listen` stream you open with `client.listen(...)`, and eviction completes before your watcher sees the event; **[Subscriptions](subscriptions.md)** is that page. diff --git a/docs/client/callbacks.md b/docs/client/callbacks.md index 5f1dd194..4ebdd26d 100644 --- a/docs/client/callbacks.md +++ b/docs/client/callbacks.md @@ -58,7 +58,7 @@ One `tools/call` from you, one `elicitation/create` back from the server, answer `mode="legacy"` on the `Client(...)` call is doing real work. By default `Client(...)` negotiates the modern protocol path, and that path has no back-channel for server-to-client requests: `ctx.elicit` fails before your callback ever runs. The transport doesn't decide that; the negotiated - protocol does, in-memory and over a URL alike. Pin `mode="legacy"` whenever your client has + protocol does. Pin `mode="legacy"` whenever your client has to answer one; every test behind this page does. **[Protocol versions](../protocol-versions.md)** has the whole story. On a 2026-07-28 session the callback isn't dead, it's fed differently: when a tool returns an @@ -146,4 +146,4 @@ Two more. Neither declares anything. * `sampling_callback` and `list_roots_callback` work the same way but serve deprecated features; modern servers use multi-round-trip requests instead. * `logging_callback` and `message_handler` receive notifications. They declare nothing. -The first argument to `Client(...)` is a transport object. **[Client transports](transports.md)** covers every kind. +The first argument to `Client(...)` picks the transport. **[Client transports](transports.md)** covers every kind. diff --git a/docs/client/index.md b/docs/client/index.md index 767f9eb0..9433e703 100644 --- a/docs/client/index.md +++ b/docs/client/index.md @@ -6,13 +6,23 @@ It is one object with one lifecycle: construct it, enter `async with`, call meth ## Your first client -```python title="client.py" hl_lines="14-18" +A client needs a server to talk to. This Bookshop is the one every snippet on this page connects to. Save it as `server.py` and leave it running over HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -The server at the top is only there so you have something to connect to. The client is the five highlighted lines. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` is given the **server object itself**. That is the in-memory transport: no subprocess, no port, no HTTP. It is how every example on this page, and every test you write, connects. +That serves it at `http://localhost:8000/mcp`. The client is its own program. Save it as `client.py` and run `python client.py` in a second terminal: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` is given a **URL**, so it connects over Streamable HTTP to the server you just started. * `async with` is the **lifecycle**. Entering it connects and negotiates; leaving it disconnects. There is no `connect()` / `close()` pair, and a `Client` cannot be reused after the block ends. * Inside the block the connection facts are already there as plain properties. @@ -20,10 +30,10 @@ The server at the top is only there so you have something to connect to. The cli `Client` takes one positional argument and resolves the transport from its type: -* An `MCPServer` (or low-level `Server`) instance: connected **in-process**. -* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the production path. -* A `StdioServerParameters`: the command to launch as a **subprocess**, spoken to over its stdin and stdout. +* A URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, the transport you deploy behind. +* A `StdioServerParameters`: the command to launch as a local **subprocess**, spoken to over its stdin and stdout. * A **transport**: anything you can `async with ... as (read, write)`, such as `streamable_http_client(url, http_client=...)` around your own HTTP client. +* An `MCPServer` (or low-level `Server`) instance: connected **in-process**, with no subprocess and no port. That one is for tests, and **[Testing](../get-started/testing.md)** builds on it. Everything else on this page is identical across all four. Headers, subprocesses, timeouts, and the `Transport` protocol get their own page: **[Client transports](transports.md)**. @@ -44,11 +54,11 @@ You never picked a protocol version. By default the `Client` probes the server a ## Listing tools -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` returns a `ListToolsResult`; the tools are in `.tools`. Each one is the complete definition a host would hand to a model: +`list_tools()` returns a `ListToolsResult`; the tools are in `.tools`. Each one is the complete definition a host would hand to a model. Here is the first: ```python tool.name # 'search_books' @@ -72,6 +82,8 @@ and `tool.input_schema` is the JSON Schema the server derived from the function' That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments. +The second tool, `lookup_book`, was registered without a `title=`, so its `tool.title` is `None`. + !!! tip `title` is optional, so a UI showing tools to a human has to pick: the `title` if there is one, the `name` if not. `from mcp.shared.metadata_utils import get_display_name` does exactly that, @@ -81,7 +93,7 @@ That schema is everything a UI needs to render an argument form, and everything `call_tool(name, arguments)` runs the tool and gives you back a `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -137,7 +149,7 @@ A tool that raises does **not** raise in your client. It comes back as an ordina The resource verbs come in pairs: two ways to list, one way to read. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -151,7 +163,7 @@ A client can also be told when a resource changes. On 2025-era connections that ## Prompts -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -176,7 +188,7 @@ A host hands those messages straight to the model. That is the whole feature. A server with a completion handler can autocomplete prompt and resource-template arguments as the user types. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -189,21 +201,21 @@ The answer is in `result.completion.values`. Type `"p"` and the server comes bac Every `list_*` method takes a `cursor=` keyword and every result carries a `next_cursor`. When `next_cursor` is `None`, you have everything. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -This loop is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **[Pagination](../advanced/pagination.md)**. +`list_all_tools` is correct against every server. `MCPServer` returns everything in one page, so `next_cursor` is `None` and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in **[Pagination](../advanced/pagination.md)**. ## In tests -`Client(mcp)` with no process and no port is already a test harness for your server. +Every `client.py` on this page reached `server.py` over HTTP. In a test you skip the network and hand `Client` the server object itself: `from server import mcp`, then `Client(mcp)`. No process, no port, and every method above works the same. -There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-memory connections, and **[Testing](../get-started/testing.md)** is the page that explains it and builds the whole pattern around it. +There is one constructor flag built for that: `Client(mcp, raise_exceptions=True)`. It only has an effect on in-process connections, and **[Testing](../get-started/testing.md)** is the page that explains it and builds the whole pattern around it. ## Recap -* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport. +* `Client(x)` connects over Streamable HTTP to a URL string, launches a subprocess for a `StdioServerParameters`, enters a transport directly, and in tests takes the server object itself. * `async with` is the whole lifecycle. Inside it, `server_capabilities` and `protocol_version` are already populated; `server_info` and `instructions` are too when the server provides them. * `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`. * `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception. diff --git a/docs/client/oauth-clients.md b/docs/client/oauth-clients.md index cd7de356..54578439 100644 --- a/docs/client/oauth-clients.md +++ b/docs/client/oauth-clients.md @@ -76,18 +76,20 @@ Look at `main()`. The provider goes on the **httpx2 client**, the httpx2 client The first time `Client` sends a request, the server answers `401`. The provider takes over: -1. **Discovery.** It reads the `WWW-Authenticate` header, fetches the server's Protected Resource Metadata from `/.well-known/oauth-protected-resource`, learns which authorization server protects this resource, and fetches *that* server's metadata. +1. **Discovery.** It reads the `WWW-Authenticate` header, fetches the server's Protected Resource Metadata from `/.well-known/oauth-protected-resource`, learns which authorization server protects this resource, and fetches *that* server's metadata. (An older server that publishes no resource metadata is asked for authorization server metadata at its own origin instead.) Either way the metadata must name, as its `issuer`, the server it was fetched for; anything else is refused. 2. **Registration.** Nothing in storage? It registers you dynamically with your `OAuthClientMetadata` and stores the result. 3. **Authorization.** It generates the PKCE pair and a `state`, builds the authorization URL, awaits your `redirect_handler`, then awaits your `callback_handler` for the code. 4. **Exchange.** It trades the code for an `OAuthToken`, stores it, and replays your original request with `Authorization: Bearer ...`. After that it is quiet. Tokens come out of storage, an expired access token is refreshed with the refresh token, and only when none of that works does it run the flow again. +One transport rule applies to all of these requests: like the MCP request they run inside, they follow a redirect only when it stays on the same origin and keeps the method (a trailing-slash 307/308, say), and treat any other redirect as that URL not answering. + You wrote none of it. Two keyword arguments remain (`client_metadata_url` and `validate_resource_url`), and this file needs neither. `client_metadata_url` is the one worth knowing about; it gets its own section below. ### Try it -Most examples in these docs you can check with an in-memory `Client(server)`. Not this: the whole point of the flow is an HTTP `401`, and there is no HTTP between an in-memory client and its server. +The in-memory `Client(server)` your tests use is no help here: the whole point of the flow is an HTTP `401`, and there is no HTTP between an in-memory client and its server. The repository ships the live version. `examples/servers/simple-auth/` runs a standalone authorization server and a protected MCP server; `examples/clients/simple-auth-client/` is this page's client grown into a small CLI. Its README has the two commands: start the servers, run the client against them, and you watch the four steps go by. @@ -105,13 +107,14 @@ A nightly job, a CI step, another service. There is no browser and nobody to cli `ClientCredentialsOAuthProvider` is the same `httpx2.Auth`, minus the human: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` What changed: * No `OAuthClientMetadata`, no handlers. You pass `client_id` and `client_secret`; the provider builds a minimal `client_credentials` registration around them and skips dynamic registration entirely. +* `issuer` names the authorization server that issued those credentials; use the `issuer` value its `/.well-known/oauth-authorization-server` document returns. Discovery still runs as above, but token requests are only ever built from metadata for *that* issuer; if the MCP server points anywhere else, the flow stops with an `OAuthFlowError` instead. Leaving it out is deprecated and it becomes required in 3.0 (see **[Deprecated features](../deprecated.md#deprecated-sdk-helpers)**); until then the provider warns and uses whichever authorization server discovery finds. * `scope` is a space-separated string, the OAuth wire format. * Everything downstream is identical: the same `TokenStorage`, the same `httpx2.AsyncClient(auth=...)`, the same `streamable_http_client`. @@ -124,7 +127,7 @@ By default the secret travels as HTTP Basic auth on the token request (`client_s One more provider lives in `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, for clients that authenticate with a JWT instead of a shared secret (`private_key_jwt`, the key-pair and workload-identity flavour). It follows - the same pattern: construct one, put it on `auth=`. The same module ships + the same pattern: construct one (it takes the same optional `issuer`), put it on `auth=`. The same module ships `SignedJWTParameters` and `static_assertion_provider`, two helpers that build its assertion. There is one more no-human situation: the client belongs to an enterprise whose identity provider, not the user, decides which MCP servers it may reach. That is a different grant with its own trust model and its own page, **[Identity assertion](identity-assertion.md)**. diff --git a/docs/client/session-groups.md b/docs/client/session-groups.md index 70ac02e8..de43e748 100644 --- a/docs/client/session-groups.md +++ b/docs/client/session-groups.md @@ -73,7 +73,7 @@ If you already hold a connected `ClientSession` (`Client.session` is one), hand ## Recap * `ClientSessionGroup` holds many server connections and merges their tools, resources, and prompts into one `dict` each. -* `connect_to_server(params)` per server. It takes transport parameters, never the server object or URL a `Client` takes. +* `connect_to_server(params)` per server. It takes transport parameters, never the URL or `Transport` a `Client` takes. * `group.call_tool(name, arguments)` routes to the owning server for you. * Names must be unique across the whole group; two servers with a `search` tool cannot coexist on their own. * `component_name_hook=` rewrites every registered name. The dict key changes, the wire name does not. diff --git a/docs/client/transports.md b/docs/client/transports.md index afb33caf..6d9d30f9 100644 --- a/docs/client/transports.md +++ b/docs/client/transports.md @@ -6,30 +6,15 @@ You never configure one separately. `Client` takes a single positional argument The *server* side of each (what `mcp.run()` does and what you deploy) is **[Running your server](../run/index.md)**. -## In memory - -Pass the server object itself: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -No subprocess, no port, no bytes on a wire. The client and the server are two objects in the same process, and the call still goes through the real protocol layer: `search_books` is listed, validated and invoked exactly as it would be over HTTP. - -That makes it two things at once: - -* **A test harness.** Every example in this documentation is exercised this way, and the **[Testing](../get-started/testing.md)** page builds the whole pattern around it. -* **An embedding API.** An application that constructs the server doesn't need a network hop to call its tools. - ## Streamable HTTP -Pass a URL string and you get **Streamable HTTP**, the transport you deploy behind: +Pass a URL string and you get **Streamable HTTP**, the transport you deploy behind and the one to reach for first: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx2.AsyncClient` configured the way MCP needs: `follow_redirects=True`, a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. +That is the whole production client. `Client` wraps the URL in `streamable_http_client(...)` for you, on top of an `httpx2.AsyncClient` configured the way MCP needs: a 30-second timeout for connect/write/pool, and a 300-second read timeout because the server may hold a response stream open. !!! check A `Client` you have constructed is **not** connected. Construction only picks the transport; @@ -45,7 +30,7 @@ That is the whole production client. `Client` wraps the URL in `streamable_http_ The moment you need an `Authorization` header, a cookie, a proxy, mTLS, or a different timeout, build the `httpx2.AsyncClient` yourself and hand it to `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -75,9 +60,30 @@ environment variables or pass an explicit `verify=ssl_context` to your `httpx2.A !!! info `httpx2` keeps the familiar `httpx` API, so if you know `httpx` you already know how to do auth, proxies, event hooks, retries and connection limits here. The SDK adds nothing on top and takes - nothing away. It is also where OAuth plugs in: + nothing away, except [redirect handling](#redirects). It is also where OAuth plugs in: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. That whole flow is **[OAuth clients](oauth-clients.md)**. +### Redirects + +The transport connects to the URL you gave it, and only that origin. + +* A `307`/`308` redirect that stays on the same scheme, host and port is followed, and so is `http://` → `https://` on the same host. That covers the usual `/mcp` → `/mcp/` trailing-slash redirect. +* A redirect anywhere else is **not** followed. The call fails with: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + If that URL is the server you meant, put it in your config. If it isn't, the server or a proxy in front of it is misconfigured. + +This holds for any `httpx2.AsyncClient` you pass in: its `follow_redirects` setting is not consulted for MCP requests, in either direction. The SDK's OAuth providers apply the same rule to their own requests. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` means the + server sits behind a TLS-terminating proxy it doesn't know about and is issuing `http://` redirects. + That is fixed on the server (**[Deploy & scale](../run/deploy.md#behind-a-tls-terminating-proxy)**), + or by using the exact `https://…/` URL the message suggests. + ## stdio A **stdio** server is a subprocess. The client launches it, writes JSON-RPC to its stdin and reads JSON-RPC from its stdout. It is how a desktop host runs a server on your machine: a host *is* this code plus a UI, and **[Connect to a real host](../get-started/real-host.md)** is the same relationship seen from the host's side, as a config file. @@ -100,6 +106,18 @@ The child's stderr goes to yours. To send it somewhere else, build the transport A server that needs an API key won't find it there. Pass it explicitly with `env=`; those variables are merged on top of the allow-list. That is what `BOOKSHOP_API_KEY` is doing above. +## In memory + +In a test there is nothing to deploy and nothing to launch. Pass the server object itself: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +No subprocess, no port, no bytes on a wire. The client and the server are two objects in the same process, and the call still goes through the real protocol layer: `search_books` is listed, validated and invoked exactly as it would be over HTTP. **[Testing](../get-started/testing.md)** builds the whole pattern around it. + +The same form doubles as an embedding API: an application that constructs the server itself can call its tools without a network hop. + ## SSE `sse_client(url)`, from `mcp.client.sse`, is the HTTP transport that Streamable HTTP superseded. Wrap it the same way, `Client(sse_client("http://localhost:8000/sse"))`, to talk to a server that still speaks it, and don't build anything new on it. @@ -108,15 +126,16 @@ The child's stderr goes to yours. To send it somewhere else, build the transport To `Client`, all of the above are the same thing. -A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a server object connects in-process, a `str` becomes `streamable_http_client(url)`, a `StdioServerParameters` becomes `stdio_client(params)`, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own. +A **transport** is any async context manager that yields a `(read, write)` pair of message streams: formally, the `Transport` protocol in `mcp.client`. `Client` resolves its argument by type: a `str` becomes `streamable_http_client(url)`, a `StdioServerParameters` becomes `stdio_client(params)`, a server object connects in-process, and anything else is entered as a transport directly. That last rule is why `stdio_client(...)`, `streamable_http_client(...)` and `sse_client(...)` all drop into the same slot, and why you can write your own. ## Recap -* `Client(mcp)` (the server object) connects in memory. Use it for tests and for embedding. * `Client("http://.../mcp")` (a URL) connects over Streamable HTTP, the production transport. * Headers, auth, proxies and timeouts belong on an `httpx2.AsyncClient` you pass to `streamable_http_client(url, http_client=...)`. There is no `headers=` keyword. +* Redirects are followed only within the URL's own origin (a trailing-slash `307`/`308`), plus `http`→`https` on the same host. Anything else fails with `Redirect to … not followed`; configure the final URL. * stdio is `Client(StdioServerParameters(...))`. Wrap it in `stdio_client(...)` yourself only to redirect the child's stderr. * The subprocess gets an allow-listed environment, not yours; `env=` adds to it. +* `Client(mcp)` (the server object) connects in memory. Use it in tests, or to embed a server in the application that built it. * A transport is anything you can `async with x as (read, write)`. `Client` hands anything that isn't a server object, a URL or `StdioServerParameters` straight to that protocol. * Constructing a `Client` picks the transport. `async with` opens it. diff --git a/docs/deprecated.md b/docs/deprecated.md index d1d724fa..f48f07ac 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -1,6 +1,6 @@ # Deprecated features -The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. One SDK helper is deprecated on its own account and is listed [at the end](#deprecated-sdk-helpers). +The 2026-07-28 spec retires five things. The SDK still implements every one of them, and every one of them now carries a **deprecation warning**. A few SDK-level deprecations stand on their own account and are listed [at the end](#deprecated-sdk-helpers). The table below names each deprecated feature, why it is going away, and the replacement to build on. @@ -131,11 +131,13 @@ That is the whole API. There is no per-method switch, and you don't want one: th ## Deprecated SDK helpers -These are not spec changes, only SDK internals with a better replacement. They warn with the same `MCPDeprecationWarning` and will be removed in 3.0. +These are not spec changes, only SDK usage with a better replacement. They warn with the same `MCPDeprecationWarning`, and 3.0 removes the old form. | Deprecated | What you do instead | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` and then `FuncMetadata.call_fn()`. Only code that drives `FuncMetadata` directly (a custom `Tool` subclass, say) ever called it. | +| `AuthSettings(resource_server_url=...)` without `validate_token_resource=` | Set it: `True` has the server refuse bearer tokens your verifier does not report as issued for `resource_server_url`, `False` says your verifier checks the token's audience itself (see **[Authorization](run/authorization.md#a-token-verifier)**). Unset behaves as `False`; 3.0 makes `True` the default whenever `resource_server_url` is set. | +| `ClientCredentialsOAuthProvider(...)` or `PrivateKeyJWTOAuthProvider(...)` without `issuer=` | Pass `issuer=` naming the authorization server that issued the credentials (see **[Writing OAuth clients](client/oauth-clients.md#machine-to-machine)**). Without it the MCP server decides which authorization server receives them; 3.0 makes the keyword required. | ## Recap @@ -144,7 +146,7 @@ These are not spec changes, only SDK internals with a better replacement. They w * Deprecated is advisory: no wire changes, everything keeps working against pre-2026 sessions, and you get a visible `MCPDeprecationWarning` (a `UserWarning`, so it is on by default). * Sampling and roots additionally need a back-channel that a 2026-07-28 session does not have. On a modern connection they warn and then they raise. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silences the whole category; `"error::mcp.MCPDeprecationWarning"` in pytest turns it into a test failure. -* One SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, is deprecated separately for removal in 3.0. +* The [SDK-level deprecations](#deprecated-sdk-helpers) follow the same rule: they warn now, and 3.0 drops the old form. * New code should not be built on any of these. Every other page in these docs teaches the current API. diff --git a/docs/get-started/first-steps.md b/docs/get-started/first-steps.md index ad2f4c63..a5d8b4ff 100644 --- a/docs/get-started/first-steps.md +++ b/docs/get-started/first-steps.md @@ -12,7 +12,7 @@ Three words you'll see on every page from here on: * A **client** lives inside the host and speaks MCP. The host runs one client per server it's connected to. * A **server** is what you build with this SDK. It exposes things to clients. It never talks to the model directly. -You write the server. Hosts are someone else's product. The SDK also gives you a `Client`. You'll use it to test your servers, and it shows up later on this page. +You write the server. Hosts are someone else's product. The SDK also gives you a `Client`, the same class a host would use to reach a server by URL or launch it as a subprocess. It shows up later on this page, and it is also how you'll test your servers. ## The three primitives @@ -78,22 +78,20 @@ You saw three tabs in the Inspector. How did it know there were three? When a client connects, the server declares its **capabilities**: which families of requests it will answer. The client uses that declaration to decide what to even ask for. You never wrote it; `MCPServer` declares it for you. -Look at it yourself. The SDK's `Client` accepts the server object directly and connects to it **in memory** (no subprocess, no port): +Look at it yourself. Leave `server.py` running over HTTP in one terminal: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +and point a client at it from another: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -113,8 +111,9 @@ That dictionary is your server's declared **capabilities**. It's the first thing Notice what isn't there. `completions` (argument autocomplete for resource templates and prompts) needs a handler you write, this server doesn't have one, so the capability is absent and a well-behaved client won't ask. That's the rule for everything optional: register the thing and the capability appears; **[Completions](../servers/completions.md)** proves it. !!! info - `Client(mcp)` is the same in-memory client every example in these docs is tested with, and - it's how you'll test yours. It gets a whole page: **[Testing](testing.md)**. + That `client.py` is a complete MCP client, and **[The Client](../client/index.md)** is its page. + In a test you skip the terminal and the port and hand `Client` the server object itself, + `Client(mcp)`. That gets a whole page too: **[Testing](testing.md)**. ## What you did not write @@ -123,7 +122,7 @@ Look back over this page. You wrote three small Python functions. You did **not* * A JSON Schema. `a: int, b: int` *is* the schema for `add`. * A request handler. `tools/list`, `resources/read`, `prompts/get`: all served for you. * A capability declaration. `MCPServer` made it for you. -* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `Client(mcp)`, and you never saw it. +* A line of protocol. The version negotiation, the JSON-RPC framing, the capability exchange: all of it happened inside `mcp dev` and `client.py`, and you never saw it. That ratio is the whole point of the SDK. @@ -134,6 +133,6 @@ That ratio is the whole point of the SDK. * One decorator per primitive: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, description, and schema come from the function. * A URI with a `{param}` makes a resource **template**, listed separately from concrete resources. * The server's **capabilities** are declared for you, and a client only asks for what a server declares. -* `Client(mcp)` connects to the server object in memory: your test harness from day one. +* `Client("http://localhost:8000/mcp")` talks to your running server. Hand it the server object instead, `Client(mcp)`, and it is your test harness from day one. Next up is **[Connect to a real host](real-host.md)**: this server inside Claude Desktop or an IDE, for real. Then **[Testing](testing.md)**: one page, one in-memory client, and you're never guessing whether it works. After that, each primitive gets its own page, starting with the one the model drives: **[Tools](../servers/tools.md)**. diff --git a/docs/get-started/testing.md b/docs/get-started/testing.md index e45d13b4..98c67173 100644 --- a/docs/get-started/testing.md +++ b/docs/get-started/testing.md @@ -1,8 +1,8 @@ # Testing -The Python SDK ships a `Client` class with an **in-memory transport**: pass it your server object and it connects to it directly. +The SDK's `Client` class, the same one that connects to a URL or launches a subprocess, also connects **in memory**: pass it your server object and it talks to it directly. -No subprocess. No port. No transport at all. It's the same idea as FastAPI's `TestClient`. +No subprocess. No port. Nothing on a wire. It's the same idea as FastAPI's `TestClient`. ## Basic usage @@ -91,7 +91,7 @@ instead of the sanitised one. Leave it on in tests. It has no meaning in production code. -## In-process by default +## Era-neutral by default !!! note `Client(mcp)` connects in-process and is **era-neutral** by default: it probes the server and diff --git a/docs/handlers/multi-round-trip.md b/docs/handlers/multi-round-trip.md index 1d5b9f52..c76069db 100644 --- a/docs/handlers/multi-round-trip.md +++ b/docs/handlers/multi-round-trip.md @@ -159,7 +159,7 @@ The low-level `Server` is the no-batteries tier: unlike `MCPServer`, nothing is ## A 2026-07-28 result -`InputRequiredResult` only exists at protocol version **2026-07-28**. The in-memory `Client(server)` negotiates it for you; over the wire, `mode="auto"` discovers it. After connecting, `client.protocol_version` tells you what you got. +`InputRequiredResult` only exists at protocol version **2026-07-28**. `Client`'s default `mode="auto"` discovers it on any connection. After connecting, `client.protocol_version` tells you what you got. !!! warning A pre-2026 session has nowhere to put an `InputRequiredResult`. Return one from your handler on a diff --git a/docs/handlers/progress.md b/docs/handlers/progress.md index 57bbb59e..d48b4c6a 100644 --- a/docs/handlers/progress.md +++ b/docs/handlers/progress.md @@ -24,19 +24,17 @@ Three arguments, and you decide what they mean: The client opts in **per call**, by passing `progress_callback=` to `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -51,28 +49,31 @@ anyio.run(main) The callback is an `async` function taking exactly what the server reported: `progress`, `total`, `message`. !!! info - `Client(mcp)` connects straight to the server object, in memory, the same client the **[Testing](../get-started/testing.md)** - page is built on. `progress_callback` is the same parameter whatever transport the `Client` - uses; the *timing* you are about to see is the in-memory connection's. It runs your callback - inline, so every report lands before `call_tool` returns. Over a real transport the - notifications race the result, and a slow callback can still be running after `call_tool` has - returned. + `progress_callback` is the same parameter whatever you handed `Client`: a URL as here, a + `StdioServerParameters`, or the server object in a test. Mind the timing over a real + transport, though. Each notification is delivered on its own, beside the response, so a slow + callback can still be running after `call_tool` has returned. Only the in-process test + connection runs the callback inline and guarantees every report lands first. ### Try it -Put `client.py` next to `server.py` and run it: +Serve `server.py` over HTTP, then run the client from a second terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Every `await ctx.report_progress(...)` on the server became one call to `show` on the client, in order, and both lines printed **before** `call_tool` returned. Progress is not bundled into the result; it streams while the tool is still working. +Every `await ctx.report_progress(...)` on the server became one call to `show` on the client, in order. Progress is not bundled into the result. It streams while the tool is still working. !!! warning `progress_callback` belongs to the **call**, not the `Client`. There is no constructor argument diff --git a/docs/migration.md b/docs/migration.md index 25e366d8..03f04675 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -17,7 +17,7 @@ Every section heading below names the API it affects, so searching this page for | Change | First symptom | Section | |---|---|---| -| `FastMCP` renamed to `MCPServer` | `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` | [`FastMCP` renamed](#fastmcp-renamed-to-mcpserver) | +| `FastMCP` renamed to `MCPServer` | `ModuleNotFoundError: No module named 'mcp.server.fastmcp'` (newer 2.x releases follow it with a pointer to this guide) | [`FastMCP` renamed](#fastmcp-renamed-to-mcpserver) | | Fields renamed from camelCase to snake_case | `AttributeError: 'Tool' object has no attribute 'inputSchema'` | [snake_case fields](#field-names-changed-from-camelcase-to-snake_case) | | `mcp.types` names removed | `ImportError: cannot import name 'Content' from 'mcp.types'` | [Removed types](#removed-type-aliases-and-classes) | | `McpError` renamed to `MCPError` | `ImportError: cannot import name 'McpError' from 'mcp'` | [`McpError` renamed](#mcperror-renamed-to-mcperror) | @@ -119,7 +119,7 @@ them: ```python import httpx -http_client = httpx.AsyncClient(follow_redirects=True) +http_client = httpx.AsyncClient(timeout=httpx.Timeout(30, read=300)) ``` **After (v2):** @@ -127,7 +127,7 @@ http_client = httpx.AsyncClient(follow_redirects=True) ```python import httpx2 -http_client = httpx2.AsyncClient(follow_redirects=True) +http_client = httpx2.AsyncClient(timeout=httpx2.Timeout(30, read=300)) ``` `httpx2` is API-compatible with `httpx`, so usually only the import name @@ -672,6 +672,8 @@ All submodules under `mcp.server.fastmcp.*` are now under `mcp.server.mcpserver. - `ToolError`, `ResourceError` — from `mcp.server.mcpserver.exceptions` - `MCPServerError` (renamed from `FastMCPError`) — from `mcp.server.mcpserver.exceptions` +Importing `mcp.server.fastmcp`, or anything below it, raises `ModuleNotFoundError` (newer 2.x releases include a link to this section in its message), so existing `except ImportError` or `except ModuleNotFoundError` fallbacks around the v1 import keep working. + ### What is unchanged on `MCPServer` Beyond the changes covered in this section, the everyday `FastMCP` surface carries over to `MCPServer` as-is: @@ -2090,7 +2092,6 @@ http_client = httpx2.AsyncClient( headers={"Authorization": "Bearer token"}, timeout=httpx2.Timeout(30, read=300), auth=my_auth, - follow_redirects=True, ) async with http_client: @@ -2101,11 +2102,11 @@ async with http_client: ... ``` -v1's internal client set `follow_redirects=True`; set it explicitly when supplying your own `httpx2.AsyncClient` to preserve that behavior. +v1's internal client set `follow_redirects=True`. You don't need it on your own client: the transport follows a method-preserving redirect within the endpoint's origin (a trailing-slash 307/308, say) itself, and does not follow one anywhere else, whatever the client is configured to do. `streamable_http_client` itself keeps a small signature — `streamable_http_client(url, *, http_client=None, terminate_on_close=True)` — and now yields a 2-tuple (next section). The removed function's other parameters map onto the client you build: -- `headers`, `timeout`, `sse_read_timeout`, `auth`: set them on the `httpx2.AsyncClient` as above. `streamablehttp_client` defaulted to `httpx.Timeout(30, read=300)`; a bare `httpx2.AsyncClient()` falls back to httpx2's flat 5-second timeout, too short for the long-lived GET stream, so set `timeout=httpx2.Timeout(30, read=300)` (as shown) to keep v1's values. Omitting `http_client` still gives you a default client with those timeouts and `follow_redirects=True`. +- `headers`, `timeout`, `sse_read_timeout`, `auth`: set them on the `httpx2.AsyncClient` as above. `streamablehttp_client` defaulted to `httpx.Timeout(30, read=300)`; a bare `httpx2.AsyncClient()` falls back to httpx2's flat 5-second timeout, too short for the long-lived GET stream, so set `timeout=httpx2.Timeout(30, read=300)` (as shown) to keep v1's values. Omitting `http_client` still gives you a default client with those timeouts. - `httpx_client_factory`: gone with no replacement — call your factory yourself and pass the result as `http_client`. - `terminate_on_close`: unchanged (default `True`). @@ -2149,10 +2150,7 @@ async def capture_session_id(response: httpx2.Response) -> None: if session_id: captured_session_ids.append(session_id) -http_client = httpx2.AsyncClient( - event_hooks={"response": [capture_session_id]}, - follow_redirects=True, -) +http_client = httpx2.AsyncClient(event_hooks={"response": [capture_session_id]}) async with http_client: async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream): @@ -2497,7 +2495,9 @@ metadata's `issuer` exactly matches the authorization server URL advertised in t resource metadata, as required by [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) section 3.3 ([SEP-2468](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468)). The comparison is a simple string comparison ([RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) -section 6.2.1), so even a trailing-slash disagreement counts as a mismatch. v1 accepted the +section 6.2.1), so even a trailing-slash disagreement counts as a mismatch. (For an older server +that publishes no protected resource metadata the expected value is the MCP server's own origin, +and there a root issuer with a trailing slash is accepted too.) v1 accepted the metadata without checking, so a server pairing whose two values disagree authenticated fine under v1 and now fails the entire flow. For example, when the MCP server's protected resource metadata advertises @@ -2515,7 +2515,7 @@ OAuthFlowError: Authorization server metadata issuer mismatch: https://as.exampl There is no client-side override. Fix the deployment instead: make the authorization server's `issuer` string-equal the URL in the protected resource metadata's `authorization_servers` -list. See [OAuth metadata URLs no longer gain a trailing slash](#oauth-metadata-urls-no-longer-gain-a-trailing-slash) +list (or the MCP server's origin, without protected resource metadata). See [OAuth metadata URLs no longer gain a trailing slash](#oauth-metadata-urls-no-longer-gain-a-trailing-slash) for how v2 preserves the exact string form of these URLs. ### OAuth client requests `offline_access` and adds `prompt=consent` when the authorization server supports it ([SEP-2207](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2207)) diff --git a/docs/protocol-versions.md b/docs/protocol-versions.md index 9ef19a7c..ab40f24a 100644 --- a/docs/protocol-versions.md +++ b/docs/protocol-versions.md @@ -6,9 +6,17 @@ Servers released before 2026-07-28 open every connection with the **`initialize` You almost never have to care, because `Client` negotiates for you. This page is about the one constructor argument that controls it, `mode=`, and the three times you change it. +Every snippet on this page is a `client.py` that talks to the Bookshop `server.py` from **[The Client](client/index.md)**. Start that server in one terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Then run each snippet in a second terminal with `python client.py`. + ## `mode="auto"` -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -26,13 +34,14 @@ Either way you come out connected, and `client.protocol_version` tells you which That is the whole feature. One `Client`, any era of server, no branching in your code. !!! info - `MCPServer` answers `server/discover` on every transport — in-memory, stdio, streamable - HTTP — so against your own server `auto` always lands on `2026-07-28`. The fallback only - ever fires against a real pre-2026 server, which is exactly when you want it to. + `MCPServer` answers `server/discover` on every transport — Streamable HTTP, stdio, and the + in-process connection your tests use — so against your own server `auto` always lands on + `2026-07-28`. The fallback only ever fires against a real pre-2026 server, which is exactly + when you want it to. ## `mode="legacy"` -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -56,7 +65,7 @@ At 2026-07-28 it is gone. The server *returns* its questions and you retry the c `mode` also accepts a modern protocol version string. Today that set is exactly `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -89,7 +98,7 @@ The probe is cheap, but it is still a round trip you pay on every reconnect, and So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `instructions`, and the identity the server stamped into the result's `_meta`. Hand it back as `prior_discover=` the next time: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/docs/run/asgi.md b/docs/run/asgi.md index 2eca9273..78e97888 100644 --- a/docs/run/asgi.md +++ b/docs/run/asgi.md @@ -94,7 +94,7 @@ That trailing `/mcp` is `streamable_http_path`. Set it to `"/"` and the mount pr --8<-- "docs_src/asgi/tutorial004.py" ``` -Now clients connect to `/notes`, not `/notes/mcp`. +Now clients connect to `/notes/`, not `/notes/mcp`. ## CORS for browser clients @@ -137,4 +137,4 @@ A browser-based client needs two permissions from you: to **send** its MCP reque * Browser clients need CORS: `allow_headers` for the `Mcp-*` request headers, `expose_headers=["Mcp-Session-Id"]` for the response. * `@mcp.custom_route()` adds plain, unauthenticated HTTP endpoints next to `/mcp`. -Once the server is reachable at a real URL, **[The Client](../client/index.md)** connects to it with that URL instead of a server object. +Once the server is reachable at a real URL, **[The Client](../client/index.md)** connects to it with that URL. diff --git a/docs/run/authorization.md b/docs/run/authorization.md index b7d731b1..fefd0ed3 100644 --- a/docs/run/authorization.md +++ b/docs/run/authorization.md @@ -18,12 +18,12 @@ That's the whole triangle. Everything on this page is the middle bullet. The SDK has no opinion about what a valid token looks like. You tell it, by implementing **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` is a protocol with one async method. `verify_token` gets the raw token from the `Authorization` header and returns an **`AccessToken`** if it's valid, `None` if it isn't. There is nothing else to implement. -* This one looks the token up in a table. A real one verifies a JWT signature or calls the authorization server's token-introspection endpoint. That code is yours; the SDK only calls it. +* This one looks the token up in a table; each entry records the resource it was issued for. A real one verifies a JWT signature or calls the authorization server's token-introspection endpoint, and reports who the token was issued for (its `aud`) in `AccessToken.resource`. That code is yours; the SDK only calls it. * `token_verifier=` and `auth=` always travel together. Pass one without the other and `MCPServer(...)` raises a `ValueError` before it ever serves a request. `AuthSettings` is the public face of your resource server: @@ -31,6 +31,10 @@ The SDK has no opinion about what a valid token looks like. You tell it, by impl * `issuer_url`: the authorization server that issues your tokens. * `resource_server_url`: the public URL of this MCP endpoint. It names *which* resource a token is for, and it's where the discovery document lives. * `required_scopes`: every token must carry all of them. +* `validate_token_resource`: refuse any token whose `AccessToken.resource` is not `resource_server_url`. Leaving it unset while `resource_server_url` is set warns (`MCPDeprecationWarning`) and behaves as `False`; 3.0 makes `True` the default for resource servers. + * Turn it on when your authorization server binds tokens to the `resource` the client requested, which MCP clients always send. Keep `resource_server_url` the exact URL clients connect to. + * Leave it off when your authorization server uses its own audience identifiers (an Auth0 API identifier, an Entra application ID) and check `aud` in your verifier instead, returning `None` for a token that isn't for this server. + * If `aud` is a list, put the entry that equals `resource_server_url` in `resource`. !!! tip `examples/servers/simple-auth/` in the SDK repository has an `IntrospectionTokenVerifier` that calls @@ -86,7 +90,7 @@ This document is how a client that has never heard of your server finds its way Inside any handler, **`get_access_token()`** is the `AccessToken` your verifier returned for the current request: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -120,6 +124,6 @@ An authorization server can also accept an enterprise identity provider's signed * `token_verifier=` and `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` always travel together. * The SDK publishes [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata at `/.well-known/oauth-protected-resource/...` and answers unauthenticated requests with a 401 whose `WWW-Authenticate` header points at it. That is the entire discovery story. * `get_access_token()` in any handler is who's calling. -* Authorization is an HTTP concern. `stdio` and the in-memory client never see it. +* Authorization is an HTTP concern. `stdio` and the in-memory test client never see it. The client half (discovering your authorization server and fetching the token for you) is **[OAuth clients](../client/oauth-clients.md)**. And a client that *asserts* an identity instead of asking a user for one is **[Identity assertion](../client/identity-assertion.md)**. diff --git a/docs/run/deploy.md b/docs/run/deploy.md index 24f25c20..c08bc158 100644 --- a/docs/run/deploy.md +++ b/docs/run/deploy.md @@ -42,6 +42,22 @@ Deployed behind a real hostname, that same default rejects **every request** unt deployed server that refuses every connection is a Host allowlist until proven otherwise. **[Troubleshooting](../troubleshooting.md)** starts here too. +## Behind a TLS-terminating proxy + +If TLS ends at a proxy (an ingress, a load balancer, Caddy, nginx) and uvicorn serves plain HTTP behind it, tell uvicorn to trust the proxy's `X-Forwarded-*` headers: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Without that, the app believes it is being served over `http://`, and any redirect it issues (the usual one is `/mcp` → `/mcp/`) points at `http://…`. The Python client refuses to follow an HTTPS endpoint to plain HTTP and says so: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +The client-side stopgap is to configure the exact URL the server serves (`https://mcp.example.com/mcp/`, slash included) so no redirect happens. The fix is the flag above. `FORWARDED_ALLOW_IPS` is the environment-variable spelling; `*` trusts every hop, which is only right when nothing but the proxy can reach uvicorn. + ## Workers, and who has to be sticky Once the hostname answers, put more than one worker behind it. There is no SDK knob for that; you scale a Starlette app the way you scale any ASGI app, by handing the object to something that knows how to fork: @@ -165,6 +181,7 @@ An `MCPServer` is a protocol implementation, not an application server. The depl ## Recap * Out of the box the app answers only requests addressed to localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` is the go-live gate: until you pass it, every request behind a real hostname is a `421` and the reason is only in the server's log. +* Behind a TLS-terminating proxy, run uvicorn with `--proxy-headers --forwarded-allow-ips=...`, or its redirects point at `http://` and the client refuses them. * On 2026-07-28 there is no session and nothing for a load balancer to be sticky on. `stateless_http=True` is a legacy-only knob because a modern request is routed and answered before that flag is ever read. * The default `requestState` key is `os.urandom(32)`, minted per process. A multi-round-trip retry that reaches a different worker fails with `-32602` *"Invalid or expired requestState"*. * The fix is `RequestStateSecurity(keys=[...])` **and** the same server name on every instance. The name is the token's default audience claim. Same keys, same name. diff --git a/docs/run/index.md b/docs/run/index.md index da54dc31..fb23b4bb 100644 --- a/docs/run/index.md +++ b/docs/run/index.md @@ -70,6 +70,11 @@ Each transport has its own keyword arguments, all on `run()`: * `max_request_body_size`: largest accepted request body in bytes. Defaults to 4 MiB; larger requests receive HTTP 413 before parsing or session creation. Raise it only when legitimate MCP messages exceed that size. +* `session_idle_timeout`: seconds a legacy session may sit with nothing in flight before the + server closes it. Default 1800. `None` disables it. See + [Session lifetime and limits](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: how many legacy sessions one process holds at once. Default 10 000. `None` + removes the limit. Covered in the same section. * `event_store`, `retry_interval`, `transport_security`: resumability and DNS-rebinding protection. They can wait, until you deploy somewhere other than localhost; **[Deploy & scale](deploy.md)** covers `transport_security`. !!! warning diff --git a/docs/run/legacy-clients.md b/docs/run/legacy-clients.md index a1c0f760..15809fd6 100644 --- a/docs/run/legacy-clients.md +++ b/docs/run/legacy-clients.md @@ -16,15 +16,25 @@ So a legacy client is not something you build *for*. It is something that connec ## One handler, both eras -Here is a tool that has to ask the user something, and both eras of client calling it: +Here is a tool that has to ask the user something: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` needs one thing the model didn't supply: how many copies. `Annotated[..., Resolve(ask_quantity)]` is how a tool declares that (**[Dependencies](../handlers/dependencies.md)** is that whole story). Nothing in `reserve` names a version, checks a capability, or branches. -The two clients are open **at the same time**, on the same `mcp` object. `mode="legacy"` runs the `initialize` handshake: the exact connection a pre-2026 client opens. The other one takes the default and lands on `2026-07-28`. +Serve it over HTTP, and here are both eras of client calling it: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +The two clients are open **at the same time**, against the same running server. `mode="legacy"` runs the `initialize` handshake: the exact connection a pre-2026 client opens. The other one takes the default and lands on `2026-07-28`. Run `python client.py` from a second terminal: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -56,6 +66,40 @@ On one worker that is invisible. On two, it is the whole problem: a request that events to a client reconnecting to the *same* session), not a session store. It never makes a session reachable from another process. +## Session lifetime and limits + +A legacy session does not live forever, and one process does not hold an unlimited number of +them. Two settings control this. Both are keyword arguments on `run()`, `streamable_http_app()` +and `Server.streamable_http_app()`. Modern (`2026-07-28`) connections and `stateless_http=True` +have no sessions, so neither setting applies to them. + +| Setting | Default | What it does | What the client sees | Turn it off | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Closes a session that has had nothing in flight for that long. | `404 Session not found`. It has to `initialize` again. | `None` | +| `max_sessions` | `10_000` | Refuses to open a session beyond that many. Existing sessions are untouched and nothing is evicted. | `503 Too many open sessions` with JSON-RPC code `-32603`. | `None` | + +What counts as "in flight": + +* An open `GET` stream. The SDK clients keep one open, so a connected client's session never + expires. +* A request that is still being answered. A tool call that runs longer than the timeout is not + interrupted, and the countdown only starts once it finishes. +* Nothing else. Between requests the clock runs. Any request on the session restarts it, + `ping` included. Once a session has expired, nothing revives it. + +A client that ends its session with `DELETE` frees it immediately. So does a client whose +opening request was refused. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Both events show up in the server log. An expiry is `Session idle timeout` at `INFO`. A +refused open is `Refusing to open a new session: sessions are already open` at `WARNING`. + +The limits are per process. With four workers the ceiling is four times `max_sessions`, and each +worker expires its own sessions. + ## The one knob: `stateless_http` If stickiness is a cost you refuse to pay, there is exactly one thing you can change. @@ -81,7 +125,7 @@ Two things about it matter more than what it does. !!! check Do the wrong thing. `reserve` is the exact tool that just served both clients. Deploy it with - `stateless_http=True`, connect the same two clients over HTTP, and call it from each. + `stateless_http=True`, connect the same two clients, and call it from each. The modern client still gets `Reserved 2 of 'Dune'.` The modern leg didn't change. diff --git a/docs/servers/structured-output.md b/docs/servers/structured-output.md index 792bcb0c..fbe09ba8 100644 --- a/docs/servers/structured-output.md +++ b/docs/servers/structured-output.md @@ -169,6 +169,8 @@ result.structured_content # {"London": 16.2, "Reykjavik": 4.4} The keys must be `str`. A `dict[int, float]` can't be a JSON object, so it falls back to the `{"result": ...}` wrapper. +Dictionary results use Pydantic's `TypeAdapter` for validation and serialization. If you inspect a tool's `FuncMetadata.output_model`, it holds the dictionary type annotation with its schema title. + ## Validation `output_schema` is not documentation. Whatever your function returns is **validated against it** before it leaves the server. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index b0e365ca..bb7dfda4 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -8,6 +8,12 @@ Several entries run against this one server. One tool and one templated resource --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Those entries reach it at `http://localhost:8000/mcp`, so leave it running over HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + The errors this page quotes are real: the SDK's own test suite reproduces every one of them. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` @@ -18,7 +24,7 @@ This is not an MCP error. It is anyio noise, and your real error is the **last l ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -44,7 +50,7 @@ Two things to do with that: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -62,7 +68,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -70,7 +76,7 @@ Enter it. `__aenter__` is the connection: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -246,7 +252,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` -The server does not recognise the `Mcp-Session-Id` your client sent, almost always because the server **restarted** (or you were routed to a different instance). Sessions live in that one process's memory. +The server does not recognise the `Mcp-Session-Id` your client sent. Either the server **restarted** (or you were routed to a different instance), or the session **expired** because nothing was in flight for `session_idle_timeout`, which is 30 minutes by default. See [Session lifetime and limits](run/legacy-clients.md#session-lifetime-and-limits). Sessions live in that one process's memory. There is no server bug to find. The HTTP response is a `404` whose body *is* JSON-RPC, so, unlike the `421` above, the python `Client` shows you this one verbatim: @@ -256,9 +262,9 @@ There is no server bug to find. The HTTP response is a `404` whose body *is* JSO The fix is to reconnect: leave the `async with Client(...)` block and enter a new one, which negotiates a fresh session. For a long-lived client, that means catching `MCPError` around your calls and reconnecting on this message rather than retrying inside a dead session. -If it happens *without* a restart, you are running more than one worker without sticky sessions: each worker holds its own session table, so a request routed to the wrong one lands here. **[Deploy & scale](run/deploy.md)** and **[Serving legacy clients](run/legacy-clients.md)** own that story and its two fixes (sticky routing, or `stateless_http=True`). +If it happens *without* a restart and without the client having gone quiet that long, you are running more than one worker without sticky sessions: each worker holds its own session table, so a request routed to the wrong one lands here. **[Deploy & scale](run/deploy.md)** and **[Serving legacy clients](run/legacy-clients.md)** own that story and its two fixes (sticky routing, or `stateless_http=True`). -For the server operator, the matching log line is `Rejected request with unknown or expired session ID: `. It is logged at `INFO`, so it is invisible at the usual `WARNING` threshold. Seeing it in bursts right after a deploy is normal; every connected client is reconnecting. +For the server operator, the matching log line is `Rejected request with unknown or expired session ID: `. It is logged at `INFO`, so it is invisible at the usual `WARNING` threshold. Seeing it in bursts right after a deploy is normal; every connected client is reconnecting. When the session expired instead, that line is preceded by `Session idle timeout`, also at `INFO`. ## `MCPError: Method not found` @@ -270,7 +276,13 @@ One thing does **not** produce this error, despite being a request the modern pr Your server wants to ask the user something, and this client never said it can be asked. -An elicitation resolver refuses up front when the connected client did not declare form elicitation, and `e.error.data` names exactly what is missing: +This Bistro asks before it books, through a resolver: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Serve it in place of the Weather server and call `book_table` from a client that passed no `elicitation_callback`. The resolver refuses up front, because the connected client never declared form elicitation, and `e.error.data` names exactly what is missing: ```json { @@ -284,7 +296,7 @@ Pass `elicitation_callback=` to `Client(...)`. Registering the callback *is* the ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -309,14 +321,14 @@ You see this one from `ctx.elicit()` on a legacy connection, and on any connecti Your handler tried to reach the client mid-request, on a connection whose call has no channel that can carry a request from the server. There are three server configurations that put a call there. -**A `2026-07-28` connection: any transport, always.** The modern protocol has no server-initiated requests at all, so the server refuses before anything is sent. `ctx.elicit()` inside a tool is the classic way to meet this (on the very first in-memory test, since `Client(server)` negotiates `2026-07-28` without being asked), and passing `elicitation_callback=` changes nothing, because no request ever reaches the client for it to answer: +**A `2026-07-28` connection: any transport, always.** The modern protocol has no server-initiated requests at all, so the server refuses before anything is sent. `ctx.elicit()` inside a tool is the classic way to meet this, usually in that tool's very first in-memory **[test](get-started/testing.md)**, since `Client(mcp)` negotiates `2026-07-28` without being asked. Passing `elicitation_callback=` changes nothing, because no request ever reaches the client for it to answer: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -358,7 +370,7 @@ The server could not verify the `requestState` token your client echoed back, so ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -411,7 +423,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` in the server log is the only sign that two same-named tools collapsed into one. * One 421, three spellings: `Server returned an error response` (the python `Client`), `421 Misdirected Request` / `Invalid Host header` (everything else), `Invalid Host header: ` (the server log). Fix: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> a mounted app whose host lifespan never entered `mcp.session_manager.run()`. -* `Session not found` -> the server restarted; reconnect. +* `Session not found` -> the server restarted or the session expired (`session_idle_timeout`); reconnect. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` needs a server-to-client channel: a `2026-07-28` connection never has one, `stateless_http=True` takes away the legacy one, and `json_response=True` takes away the request-scoped one. Use a resolver (a legacy client also needs a server that keeps the channel). Its neighbour `Method not found` is a request for a method the other side's protocol revision doesn't have. * `Client did not declare the form elicitation capability ...` and `Elicitation not supported` -> the client is missing `elicitation_callback=`. * `Invalid or expired requestState` never says why on the wire. The server log does; `unknown key` means share `RequestStateSecurity(keys=[...])` across workers. diff --git a/docs/whats-new.md b/docs/whats-new.md index 068efb26..6627f794 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -37,11 +37,11 @@ Not everything a tool needs should come from the model. New in v2, a tool parame v1 handed you three nested layers: a transport context manager yielding raw streams, a `ClientSession` wrapped around them, and a hand-called `await session.initialize()`. v2 has one object: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` takes a server object (in memory, no transport: the testing story), a URL (Streamable HTTP), a `StdioServerParameters` (a stdio subprocess), or any other transport context manager such as `sse_client(...)`. Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_capabilities` and `client.protocol_version` are simply there afterwards, and `client.server_info` is too when the server identifies itself (it is `Implementation | None` now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down. +`Client` takes a URL (Streamable HTTP), a `StdioServerParameters` (a stdio subprocess), any other transport context manager such as `sse_client(...)`, or, in tests, the server object itself (in memory, no transport). Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_capabilities` and `client.protocol_version` are simply there afterwards, and `client.server_info` is too when the server identifies itself (it is `Implementation | None` now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down. **[The Client](client/index.md)** introduces it, **[Client transports](client/transports.md)** covers the four connection forms, **[Client callbacks](client/callbacks.md)** covers the callbacks themselves, and **[Testing](get-started/testing.md)** shows the in-memory pattern that replaces v1's `create_connected_server_and_client_session()` helper. @@ -166,11 +166,15 @@ Every server-initiated request is gone at 2026-07-28: push elicitation, sampling The replacement turns the call around. A tool that needs something from the user *returns* the question (`InputRequiredResult`), the client answers it with the same callbacks it always had, and the call is retried with the answers attached. `Client` drives that loop for you. On the server you rarely build the result yourself, because a **[dependency](handlers/dependencies.md)** does it: annotate a parameter with `Resolve(ask_quantity)`, where `ask_quantity` is an ordinary function you write, and the SDK asks over whichever mechanism the connection supports, a live elicitation request on a legacy session or a multi-round-trip on 2026. One tool body, both eras: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -That file is the pitch in one place: one server, one `Resolve`-backed tool, and a legacy client plus a modern client both getting their answer, in memory. **[Multi-round-trip requests](handlers/multi-round-trip.md)** explains the mechanism (including `request_state`, which the SDK seals and verifies for you); **[Elicitation](handlers/elicitation.md)** covers the asking. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Those two files are the whole pitch: one server, one `Resolve`-backed tool, and a legacy client plus a modern client both getting their answer from the same running server (**[Serving legacy clients](run/legacy-clients.md)** walks through them). **[Multi-round-trip requests](handlers/multi-round-trip.md)** explains the mechanism (including `request_state`, which the SDK seals and verifies for you); **[Elicitation](handlers/elicitation.md)** covers the asking. !!! warning "This is the one place a ported v1 server changes behavior" Your own tests hit it first: `Client(mcp)` negotiates 2026-07-28 against your v2 server by diff --git a/docs_src/apps/tutorial001.py b/docs_src/apps/tutorial001.py index 27d9eded..6cbefdff 100644 --- a/docs_src/apps/tutorial001.py +++ b/docs_src/apps/tutorial001.py @@ -1,6 +1,4 @@ -from mcp import Client -from mcp.client import advertise -from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID, Apps, client_supports_apps +from mcp.server.apps import Apps, client_supports_apps from mcp.server.mcpserver import MCPServer from mcp.server.mcpserver.context import Context @@ -30,10 +28,3 @@ def get_time(ctx: Context) -> str: apps.add_html_resource("ui://clock/app.html", CLOCK_HTML, title="Clock") mcp = MCPServer("clock", extensions=[apps]) - - -async def main() -> None: - async with Client(mcp, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client: - result = await client.call_tool("get_time", {}) - print(result.content) - # [TextContent(text='2026-06-26T12:00:00Z')] diff --git a/docs_src/apps/tutorial001_client.py b/docs_src/apps/tutorial001_client.py new file mode 100644 index 00000000..dae52b4f --- /dev/null +++ b/docs_src/apps/tutorial001_client.py @@ -0,0 +1,20 @@ +import anyio + +from mcp import Client +from mcp.client import advertise +from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID +from mcp.types import TextContent + +APPS_SUPPORT = advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]}) + + +async def main() -> None: + async with Client("http://localhost:8000/mcp", extensions=[APPS_SUPPORT]) as client: + result = await client.call_tool("get_time", {}) + for block in result.content: + if isinstance(block, TextContent): + print(block.text) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/authorization/tutorial001.py b/docs_src/authorization/tutorial001.py index f15f54fd..d1a9db87 100644 --- a/docs_src/authorization/tutorial001.py +++ b/docs_src/authorization/tutorial001.py @@ -4,8 +4,10 @@ from mcp.server import MCPServer from mcp.server.auth.provider import AccessToken, TokenVerifier from mcp.server.auth.settings import AuthSettings +RESOURCE = "http://127.0.0.1:8000/mcp" + KNOWN_TOKENS = { - "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]), + "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"], resource=RESOURCE), } @@ -19,8 +21,9 @@ mcp = MCPServer( token_verifier=StaticTokenVerifier(), auth=AuthSettings( issuer_url=AnyHttpUrl("https://auth.example.com"), - resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"), + resource_server_url=AnyHttpUrl(RESOURCE), required_scopes=["notes:read"], + validate_token_resource=True, ), ) diff --git a/docs_src/authorization/tutorial002.py b/docs_src/authorization/tutorial002.py index 55b024f2..c088442a 100644 --- a/docs_src/authorization/tutorial002.py +++ b/docs_src/authorization/tutorial002.py @@ -5,8 +5,10 @@ from mcp.server.auth.middleware.auth_context import get_access_token from mcp.server.auth.provider import AccessToken, TokenVerifier from mcp.server.auth.settings import AuthSettings +RESOURCE = "http://127.0.0.1:8000/mcp" + KNOWN_TOKENS = { - "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"]), + "alice-token": AccessToken(token="alice-token", client_id="alice", scopes=["notes:read"], resource=RESOURCE), } @@ -20,8 +22,9 @@ mcp = MCPServer( token_verifier=StaticTokenVerifier(), auth=AuthSettings( issuer_url=AnyHttpUrl("https://auth.example.com"), - resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"), + resource_server_url=AnyHttpUrl(RESOURCE), required_scopes=["notes:read"], + validate_token_resource=True, ), ) diff --git a/docs_src/caching/tutorial002.py b/docs_src/caching/tutorial002.py index e1722e81..acd95f2d 100644 --- a/docs_src/caching/tutorial002.py +++ b/docs_src/caching/tutorial002.py @@ -7,6 +7,7 @@ TOOLS = [Tool(name="forecast", input_schema={"type": "object"})] async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult: + print("tools/list served") return ListToolsResult(tools=TOOLS, ttl_ms=1_000) @@ -15,3 +16,4 @@ server = Server( on_list_tools=list_tools, cache_hints={"tools/list": CacheHint(ttl_ms=60_000, scope="public")}, ) +app = server.streamable_http_app() diff --git a/docs_src/caching/tutorial003.py b/docs_src/caching/tutorial003.py index 9ff3c361..f2495bab 100644 --- a/docs_src/caching/tutorial003.py +++ b/docs_src/caching/tutorial003.py @@ -1,39 +1,34 @@ from dataclasses import dataclass -from typing import Any + +import anyio from mcp import Client from mcp.client import CacheConfig -from mcp.server import CacheHint, Server, ServerRequestContext -from mcp.types import ListToolsResult, PaginatedRequestParams, Tool +from mcp.types import ListToolsResult @dataclass -class DemoState: - fetches: int = 0 - now: float = 1_000_000.0 +class Clock: + now: float = 0.0 -state = DemoState() +clock = Clock() # advanced by hand below, so the TTL runs out without sleeping -async def list_tools(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListToolsResult: - state.fetches += 1 - return ListToolsResult(tools=[Tool(name="forecast", input_schema={"type": "object"})]) - - -server = Server( - "Weather", - on_list_tools=list_tools, - cache_hints={"tools/list": CacheHint(ttl_ms=60_000, scope="public")}, -) +async def run(client: Client) -> ListToolsResult: + tools = await client.list_tools() # fetch 1 + await client.list_tools() # still fresh: served from the cache + clock.now += 2 + await client.list_tools() # past the one-second TTL: fetch 2 + await client.list_tools(cache_mode="refresh") # skip the cache read: fetch 3 + return tools async def main() -> None: - start = state.fetches - async with Client(server, cache=CacheConfig(clock=lambda: state.now)) as client: - await client.list_tools() # fetch 1 - await client.list_tools() # fresh for 60s: served from the cache - state.now += 60.0 - await client.list_tools() # the TTL ran out: fetch 2 - await client.list_tools(cache_mode="refresh") # skip the cache read: fetch 3 - print(f"4 calls, {state.fetches - start} fetches") + async with Client("http://localhost:8000/mcp", cache=CacheConfig(clock=lambda: clock.now)) as client: + tools = await run(client) + print(tools.ttl_ms, tools.cache_scope) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial001.py b/docs_src/client/tutorial001.py index b020926d..e7645509 100644 --- a/docs_src/client/tutorial001.py +++ b/docs_src/client/tutorial001.py @@ -1,18 +1,56 @@ -from mcp import Client +from pydantic import BaseModel + from mcp.server import MCPServer +from mcp.server.mcpserver.exceptions import ToolError +from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.") +GENRES = ["fiction", "non-fiction", "poetry"] + + +class Book(BaseModel): + title: str + author: str + year: int + + +@mcp.tool(title="Search the catalog") +def search_books(query: str, limit: int = 10) -> str: + """Search the catalog by title or author.""" + return f"Found 3 books matching {query!r} (showing up to {limit})." + @mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." +def lookup_book(title: str) -> Book: + """Look up a book by its exact title.""" + if title != "Dune": + raise ToolError(f"No book titled {title!r} in the catalog.") + return Book(title="Dune", author="Frank Herbert", year=1965) -async def main() -> None: - async with Client(mcp) as client: - print(client.server_info) - print(client.server_capabilities) - print(client.protocol_version) - print(client.instructions) +@mcp.resource("catalog://genres") +def genres() -> list[str]: + """The genres the catalog is organised by.""" + return GENRES + + +@mcp.resource("catalog://genres/{genre}") +def books_in_genre(genre: str) -> str: + """Every title we stock in one genre.""" + return f"3 books filed under {genre}." + + +@mcp.prompt(title="Recommend a book") +def recommend(genre: str) -> str: + """Ask for a recommendation in a genre.""" + return f"Recommend one {genre} book from the catalog and say why." + + +@mcp.completion() +async def complete_genre( + ref: PromptReference | ResourceTemplateReference, + argument: CompletionArgument, + context: CompletionContext | None, +) -> Completion | None: + return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)]) diff --git a/docs_src/client/tutorial001_client.py b/docs_src/client/tutorial001_client.py new file mode 100644 index 00000000..faf06506 --- /dev/null +++ b/docs_src/client/tutorial001_client.py @@ -0,0 +1,15 @@ +import anyio + +from mcp import Client + + +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + print(client.server_info) + print(client.server_capabilities) + print(client.protocol_version) + print(client.instructions) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial002.py b/docs_src/client/tutorial002.py index a3e379ab..68fc7a6b 100644 --- a/docs_src/client/tutorial002.py +++ b/docs_src/client/tutorial002.py @@ -1,20 +1,17 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.tool(title="Search the catalog") -def search_books(query: str, limit: int = 10) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r} (showing up to {limit})." async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.list_tools() for tool in result.tools: print(tool.name) print(tool.title) print(tool.description) print(tool.input_schema) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial003.py b/docs_src/client/tutorial003.py index 0831f5f7..f104205a 100644 --- a/docs_src/client/tutorial003.py +++ b/docs_src/client/tutorial003.py @@ -1,29 +1,11 @@ -from pydantic import BaseModel +import anyio from mcp import Client -from mcp.server import MCPServer -from mcp.server.mcpserver.exceptions import ToolError from mcp.types import TextContent -mcp = MCPServer("Bookshop") - - -class Book(BaseModel): - title: str - author: str - year: int - - -@mcp.tool() -def lookup_book(title: str) -> Book: - """Look up a book by its exact title.""" - if title != "Dune": - raise ToolError(f"No book titled {title!r} in the catalog.") - return Book(title="Dune", author="Frank Herbert", year=1965) - async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("lookup_book", {"title": "Dune"}) for block in result.content: @@ -32,3 +14,7 @@ async def main() -> None: print(result.structured_content) print(result.is_error) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial004.py b/docs_src/client/tutorial004.py index b0d62a77..35acc3d0 100644 --- a/docs_src/client/tutorial004.py +++ b/docs_src/client/tutorial004.py @@ -1,24 +1,11 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer from mcp.types import TextResourceContents -mcp = MCPServer("Bookshop") - - -@mcp.resource("catalog://genres") -def genres() -> list[str]: - """The genres the catalog is organised by.""" - return ["fiction", "non-fiction", "poetry"] - - -@mcp.resource("catalog://genres/{genre}") -def books_in_genre(genre: str) -> str: - """Every title we stock in one genre.""" - return f"3 books filed under {genre}." - async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: listed = await client.list_resources() print([resource.uri for resource in listed.resources]) @@ -29,3 +16,7 @@ async def main() -> None: for contents in result.contents: if isinstance(contents, TextResourceContents): print(contents.text) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial005.py b/docs_src/client/tutorial005.py index ce4d1647..18e4d498 100644 --- a/docs_src/client/tutorial005.py +++ b/docs_src/client/tutorial005.py @@ -1,20 +1,17 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.prompt(title="Recommend a book") -def recommend(genre: str) -> str: - """Ask for a recommendation in a genre.""" - return f"Recommend one {genre} book from the catalog and say why." async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: listed = await client.list_prompts() print(listed.prompts) result = await client.get_prompt("recommend", {"genre": "poetry"}) for message in result.messages: print(message.role, message.content) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial006.py b/docs_src/client/tutorial006.py index 370e0b79..5410e3b1 100644 --- a/docs_src/client/tutorial006.py +++ b/docs_src/client/tutorial006.py @@ -1,31 +1,17 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer -from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference - -mcp = MCPServer("Bookshop") - -GENRES = ["fiction", "non-fiction", "poetry"] - - -@mcp.prompt() -def recommend(genre: str) -> str: - """Ask for a recommendation in a genre.""" - return f"Recommend one {genre} book from the catalog and say why." - - -@mcp.completion() -async def complete_genre( - ref: PromptReference | ResourceTemplateReference, - argument: CompletionArgument, - context: CompletionContext | None, -) -> Completion | None: - return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)]) +from mcp.types import PromptReference async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.complete( ref=PromptReference(type="ref/prompt", name="recommend"), argument={"name": "genre", "value": "p"}, ) print(result.completion.values) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client/tutorial007.py b/docs_src/client/tutorial007.py index c5c918bc..85580114 100644 --- a/docs_src/client/tutorial007.py +++ b/docs_src/client/tutorial007.py @@ -1,30 +1,25 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer from mcp.types import Tool -mcp = MCPServer("Bookshop") - -@mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." - - -@mcp.tool() -def reserve_book(title: str) -> str: - """Put a book on hold.""" - return f"Reserved {title!r}." +async def list_all_tools(client: Client) -> list[Tool]: + tools: list[Tool] = [] + cursor: str | None = None + while True: + page = await client.list_tools(cursor=cursor) + tools.extend(page.tools) + if page.next_cursor is None: + return tools + cursor = page.next_cursor async def main() -> None: - async with Client(mcp) as client: - tools: list[Tool] = [] - cursor: str | None = None - while True: - page = await client.list_tools(cursor=cursor) - tools.extend(page.tools) - if page.next_cursor is None: - break - cursor = page.next_cursor + async with Client("http://localhost:8000/mcp") as client: + tools = await list_all_tools(client) print([tool.name for tool in tools]) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/client_transports/tutorial003.py b/docs_src/client_transports/tutorial003.py index 4df055e2..488737dc 100644 --- a/docs_src/client_transports/tutorial003.py +++ b/docs_src/client_transports/tutorial003.py @@ -8,7 +8,6 @@ async def main() -> None: async with httpx2.AsyncClient( headers={"Authorization": "Bearer ..."}, timeout=httpx2.Timeout(30.0, read=300.0), - follow_redirects=True, ) as http_client: transport = streamable_http_client("http://localhost:8000/mcp", http_client=http_client) async with Client(transport) as client: diff --git a/docs_src/extensions/tutorial003.py b/docs_src/extensions/tutorial003.py index 312371be..882e5e73 100644 --- a/docs_src/extensions/tutorial003.py +++ b/docs_src/extensions/tutorial003.py @@ -1,7 +1,6 @@ from collections.abc import Sequence from typing import Any -from mcp import Client from mcp.server.extension import Extension, ToolBinding from mcp.server.mcpserver import MCPServer @@ -24,12 +23,3 @@ class Stamps(Extension): mcp = MCPServer("post-office", extensions=[Stamps()]) - - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.extensions) - # {'com.example/stamps': {'sealed': True}} - result = await client.call_tool("stamp", {"text": "hello"}) - print(result.content) - # [TextContent(text='[stamped] hello')] diff --git a/docs_src/extensions/tutorial003_client.py b/docs_src/extensions/tutorial003_client.py new file mode 100644 index 00000000..3d1b47b8 --- /dev/null +++ b/docs_src/extensions/tutorial003_client.py @@ -0,0 +1,16 @@ +import anyio + +from mcp import Client + + +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + print(client.server_capabilities.extensions) + # {'com.example/stamps': {'sealed': True}} + result = await client.call_tool("stamp", {"text": "hello"}) + print(result.content) + # [TextContent(type='text', text='[stamped] hello', annotations=None, meta=None)] + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/extensions/tutorial004.py b/docs_src/extensions/tutorial004.py index d3e2ef3b..01cb3d8e 100644 --- a/docs_src/extensions/tutorial004.py +++ b/docs_src/extensions/tutorial004.py @@ -1,11 +1,9 @@ from collections.abc import Sequence -from typing import Any, Literal +from typing import Any from pydantic import Field import mcp.types as types -from mcp import Client -from mcp.client import advertise from mcp.server.context import ServerRequestContext from mcp.server.extension import Extension, MethodBinding from mcp.server.mcpserver import MCPServer, require_client_extension @@ -22,11 +20,6 @@ class SearchResult(types.Result): items: list[str] -class SearchRequest(types.Request[SearchParams, Literal["com.example/search"]]): - method: Literal["com.example/search"] = "com.example/search" - params: SearchParams - - async def search(ctx: ServerRequestContext[Any, Any], params: SearchParams) -> SearchResult: require_client_extension(ctx, EXTENSION_ID) return SearchResult(items=[f"{params.query}-{n}" for n in range(params.limit)]) @@ -49,11 +42,3 @@ class Search(Extension): mcp = MCPServer("catalog", extensions=[Search()]) - - -async def main() -> None: - async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client: - request = SearchRequest(params=SearchParams(query="mcp", limit=3)) - result = await client.session.send_request(request, SearchResult) - print(result.items) - # ['mcp-0', 'mcp-1', 'mcp-2'] diff --git a/docs_src/extensions/tutorial004_client.py b/docs_src/extensions/tutorial004_client.py new file mode 100644 index 00000000..fdfcddc8 --- /dev/null +++ b/docs_src/extensions/tutorial004_client.py @@ -0,0 +1,35 @@ +from typing import Literal + +import anyio + +import mcp.types as types +from mcp import Client +from mcp.client import advertise + +EXTENSION_ID = "com.example/search" + + +class SearchParams(types.RequestParams): + query: str + limit: int = 10 + + +class SearchResult(types.Result): + items: list[str] + + +class SearchRequest(types.Request[SearchParams, Literal["com.example/search"]]): + method: Literal["com.example/search"] = "com.example/search" + params: SearchParams + + +async def main() -> None: + async with Client("http://localhost:8000/mcp", extensions=[advertise(EXTENSION_ID)]) as client: + request = SearchRequest(params=SearchParams(query="mcp", limit=3)) + result = await client.session.send_request(request, SearchResult) + print(result.items) + # ['mcp-0', 'mcp-1', 'mcp-2'] + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/extensions/tutorial006.py b/docs_src/extensions/tutorial006.py index 88592af9..9d9f9966 100644 --- a/docs_src/extensions/tutorial006.py +++ b/docs_src/extensions/tutorial006.py @@ -1,9 +1,6 @@ -from collections.abc import Sequence -from typing import Any, Literal +from typing import Any import mcp.types as types -from mcp import Client -from mcp.client import ClaimContext, ClientExtension, ResultClaim from mcp.server.context import CallNext, HandlerResult, ServerRequestContext from mcp.server.extension import Extension from mcp.server.mcpserver import MCPServer, require_client_extension @@ -11,13 +8,6 @@ from mcp.server.mcpserver import MCPServer, require_client_extension EXTENSION_ID = "com.example/receipts" -class ReceiptResult(types.Result): - """The claimed result shape; `result_type` pins the wire tag.""" - - result_type: Literal["receipt"] = "receipt" - receipt_token: str - - class ReceiptIssuer(Extension): """Server half: answers `buy` with a receipt instead of a final result.""" @@ -35,18 +25,6 @@ class ReceiptIssuer(Extension): return {"resultType": "receipt", "receiptToken": "r-117"} -class Receipts(ClientExtension): - """Client half: claims the `receipt` shape and supplies the code that finishes it.""" - - identifier = EXTENSION_ID - - def claims(self) -> Sequence[ResultClaim[Any]]: - return [ResultClaim(result_type="receipt", model=ReceiptResult, resolve=self._redeem)] - - async def _redeem(self, claimed: ReceiptResult, ctx: ClaimContext) -> types.CallToolResult: - return await ctx.session.call_tool("redeem", {"token": claimed.receipt_token}) - - mcp = MCPServer("shop", extensions=[ReceiptIssuer()]) @@ -60,10 +38,3 @@ def buy(item: str) -> types.CallToolResult: def redeem(token: str) -> str: """Exchange a receipt token for the goods.""" return f"goods for {token}" - - -async def main() -> None: - async with Client(mcp, extensions=[Receipts()]) as client: - result = await client.call_tool("buy", {"item": "lamp"}) - print(result.content) - # [TextContent(text='goods for r-117')] diff --git a/docs_src/extensions/tutorial006_client.py b/docs_src/extensions/tutorial006_client.py new file mode 100644 index 00000000..11c655ca --- /dev/null +++ b/docs_src/extensions/tutorial006_client.py @@ -0,0 +1,40 @@ +from collections.abc import Sequence +from typing import Any, Literal + +import anyio + +import mcp.types as types +from mcp import Client +from mcp.client import ClaimContext, ClientExtension, ResultClaim + +EXTENSION_ID = "com.example/receipts" + + +class ReceiptResult(types.Result): + """The claimed result shape; `result_type` pins the wire tag.""" + + result_type: Literal["receipt"] = "receipt" + receipt_token: str + + +class Receipts(ClientExtension): + """Client half: claims the `receipt` shape and supplies the code that finishes it.""" + + identifier = EXTENSION_ID + + def claims(self) -> Sequence[ResultClaim[Any]]: + return [ResultClaim(result_type="receipt", model=ReceiptResult, resolve=self._redeem)] + + async def _redeem(self, claimed: ReceiptResult, ctx: ClaimContext) -> types.CallToolResult: + return await ctx.session.call_tool("redeem", {"token": claimed.receipt_token}) + + +async def main() -> None: + async with Client("http://localhost:8000/mcp", extensions=[Receipts()]) as client: + result = await client.call_tool("buy", {"item": "lamp"}) + print(result.content) + # [TextContent(type='text', text='goods for r-117', annotations=None, meta=None)] + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/extensions/tutorial007.py b/docs_src/extensions/tutorial007.py index 182fc8f6..659c728f 100644 --- a/docs_src/extensions/tutorial007.py +++ b/docs_src/extensions/tutorial007.py @@ -1,9 +1,7 @@ from collections.abc import Sequence -from typing import Any, Literal +from typing import Any import mcp.types as types -from mcp import Client -from mcp.client import advertise from mcp.server.context import ServerRequestContext from mcp.server.extension import Extension, MethodBinding from mcp.server.mcpserver import MCPServer @@ -19,12 +17,6 @@ class JobStatus(types.Result): status: str -class JobStatusRequest(types.Request[JobParams, Literal["com.example/jobs.status"]]): - method: Literal["com.example/jobs.status"] = "com.example/jobs.status" - params: JobParams - name_param = "jobId" # params["jobId"] rides the Mcp-Name header - - async def job_status(ctx: ServerRequestContext[Any, Any], params: JobParams) -> JobStatus: return JobStatus(status=f"{params.job_id} is running") @@ -39,11 +31,3 @@ class Jobs(Extension): mcp = MCPServer("worker", extensions=[Jobs()]) - - -async def main() -> None: - async with Client(mcp, extensions=[advertise(EXTENSION_ID)]) as client: - request = JobStatusRequest(params=JobParams(job_id="job-7")) - result = await client.session.send_request(request, JobStatus) - print(result.status) - # job-7 is running diff --git a/docs_src/extensions/tutorial007_client.py b/docs_src/extensions/tutorial007_client.py new file mode 100644 index 00000000..0f7d7961 --- /dev/null +++ b/docs_src/extensions/tutorial007_client.py @@ -0,0 +1,35 @@ +from typing import Literal + +import anyio + +import mcp.types as types +from mcp import Client +from mcp.client import advertise + +EXTENSION_ID = "com.example/jobs" + + +class JobParams(types.RequestParams): + job_id: str + + +class JobStatus(types.Result): + status: str + + +class JobStatusRequest(types.Request[JobParams, Literal["com.example/jobs.status"]]): + method: Literal["com.example/jobs.status"] = "com.example/jobs.status" + params: JobParams + name_param = "jobId" # params["jobId"] rides the Mcp-Name header + + +async def main() -> None: + async with Client("http://localhost:8000/mcp", extensions=[advertise(EXTENSION_ID)]) as client: + request = JobStatusRequest(params=JobParams(job_id="job-7")) + result = await client.session.send_request(request, JobStatus) + print(result.status) + # job-7 is running + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/first_steps/tutorial001_client.py b/docs_src/first_steps/tutorial001_client.py new file mode 100644 index 00000000..f0ba92ff --- /dev/null +++ b/docs_src/first_steps/tutorial001_client.py @@ -0,0 +1,12 @@ +import anyio + +from mcp import Client + + +async def main() -> None: + async with Client("http://localhost:8000/mcp") as client: + print(client.server_capabilities.model_dump(exclude_none=True)) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/identity_assertion/tutorial001.py b/docs_src/identity_assertion/tutorial001.py index afcd5378..24bc2657 100644 --- a/docs_src/identity_assertion/tutorial001.py +++ b/docs_src/identity_assertion/tutorial001.py @@ -62,7 +62,7 @@ oauth = IdentityAssertionOAuthProvider( async def main() -> None: - async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client: + async with httpx2.AsyncClient(auth=oauth) as http_client: transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client) async with Client(transport) as client: result = await client.list_tools() diff --git a/docs_src/legacy_clients/tutorial001.py b/docs_src/legacy_clients/tutorial001.py index 2090201f..998444de 100644 --- a/docs_src/legacy_clients/tutorial001.py +++ b/docs_src/legacy_clients/tutorial001.py @@ -2,11 +2,8 @@ from typing import Annotated from pydantic import BaseModel -from mcp import Client -from mcp.client import ClientRequestContext from mcp.server import MCPServer from mcp.server.mcpserver import AcceptedElicitation, Elicit, ElicitationResult, Resolve -from mcp.types import ElicitRequestParams, ElicitResult mcp = MCPServer("Bookshop") @@ -26,17 +23,3 @@ async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], R if isinstance(quantity, AcceptedElicitation): return f"Reserved {quantity.data.copies} of {title!r}." return "Nothing reserved." - - -async def answer(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: - return ElicitResult(action="accept", content={"copies": 2}) - - -async def main() -> None: - async with ( - Client(mcp, mode="legacy", elicitation_callback=answer) as legacy, - Client(mcp, elicitation_callback=answer) as modern, - ): - for client in (legacy, modern): - result = await client.call_tool("reserve", {"title": "Dune"}) - print(client.protocol_version, result.structured_content) diff --git a/docs_src/legacy_clients/tutorial001_client.py b/docs_src/legacy_clients/tutorial001_client.py new file mode 100644 index 00000000..afe0981e --- /dev/null +++ b/docs_src/legacy_clients/tutorial001_client.py @@ -0,0 +1,23 @@ +import anyio + +from mcp import Client +from mcp.client import ClientRequestContext +from mcp.types import ElicitRequestParams, ElicitResult + + +async def answer(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult: + return ElicitResult(action="accept", content={"copies": 2}) + + +async def main() -> None: + async with ( + Client("http://localhost:8000/mcp", mode="legacy", elicitation_callback=answer) as legacy, + Client("http://localhost:8000/mcp", elicitation_callback=answer) as modern, + ): + for client in (legacy, modern): + result = await client.call_tool("reserve", {"title": "Dune"}) + print(client.protocol_version, result.structured_content) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/lowlevel/tutorial001.py b/docs_src/lowlevel/tutorial001.py index 3b96aa2a..ac705a2e 100644 --- a/docs_src/lowlevel/tutorial001.py +++ b/docs_src/lowlevel/tutorial001.py @@ -30,3 +30,4 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool) +app = server.streamable_http_app() diff --git a/docs_src/oauth_clients/tutorial001.py b/docs_src/oauth_clients/tutorial001.py index d150dc5d..6e01553d 100644 --- a/docs_src/oauth_clients/tutorial001.py +++ b/docs_src/oauth_clients/tutorial001.py @@ -55,7 +55,7 @@ oauth = OAuthClientProvider( async def main() -> None: - async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client: + async with httpx2.AsyncClient(auth=oauth) as http_client: transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client) async with Client(transport) as client: result = await client.list_tools() diff --git a/docs_src/oauth_clients/tutorial002.py b/docs_src/oauth_clients/tutorial002.py index dd4105f9..507ee4bf 100644 --- a/docs_src/oauth_clients/tutorial002.py +++ b/docs_src/oauth_clients/tutorial002.py @@ -30,11 +30,12 @@ oauth = ClientCredentialsOAuthProvider( client_id="reporting-agent", client_secret="...", scope="user", + issuer="http://localhost:9000", ) async def main() -> None: - async with httpx2.AsyncClient(auth=oauth, follow_redirects=True) as http_client: + async with httpx2.AsyncClient(auth=oauth) as http_client: transport = streamable_http_client("http://localhost:8001/mcp", http_client=http_client) async with Client(transport) as client: result = await client.list_tools() diff --git a/docs_src/pagination/tutorial001.py b/docs_src/pagination/tutorial001.py index 3bc97540..d1dc7c2a 100644 --- a/docs_src/pagination/tutorial001.py +++ b/docs_src/pagination/tutorial001.py @@ -17,3 +17,4 @@ async def list_books(ctx: ServerRequestContext[Any], params: PaginatedRequestPar server = Server("Bookshop", on_list_resources=list_books) +app = server.streamable_http_app() diff --git a/docs_src/pagination/tutorial002.py b/docs_src/pagination/tutorial002.py index f7284777..61e40bc7 100644 --- a/docs_src/pagination/tutorial002.py +++ b/docs_src/pagination/tutorial002.py @@ -1,33 +1,26 @@ -from typing import Any +import anyio from mcp import Client -from mcp.server import Server, ServerRequestContext -from mcp.types import ListResourcesResult, PaginatedRequestParams, Resource - -BOOKS = [f"book-{n}" for n in range(1, 101)] - -PAGE_SIZE = 10 +from mcp.types import Resource -async def list_books(ctx: ServerRequestContext[Any], params: PaginatedRequestParams | None) -> ListResourcesResult: - start = 0 if params is None or params.cursor is None else int(params.cursor) - end = start + PAGE_SIZE - page = [Resource(uri=f"books://catalog/{name}", name=name) for name in BOOKS[start:end]] - next_cursor = str(end) if end < len(BOOKS) else None - return ListResourcesResult(resources=page, next_cursor=next_cursor) - - -server = Server("Bookshop", on_list_resources=list_books) +async def list_all_resources(client: Client) -> list[Resource]: + resources: list[Resource] = [] + cursor: str | None = None + while True: + page = await client.list_resources(cursor=cursor) + resources.extend(page.resources) + if page.next_cursor is None: + break + cursor = page.next_cursor + return resources async def main() -> None: - async with Client(server) as client: - resources: list[Resource] = [] - cursor: str | None = None - while True: - page = await client.list_resources(cursor=cursor) - resources.extend(page.resources) - if page.next_cursor is None: - break - cursor = page.next_cursor + async with Client("http://localhost:8000/mcp") as client: + resources = await list_all_resources(client) print(f"{len(resources)} resources") + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/protocol_versions/tutorial001.py b/docs_src/protocol_versions/tutorial001.py index 23570e3b..1da11030 100644 --- a/docs_src/protocol_versions/tutorial001.py +++ b/docs_src/protocol_versions/tutorial001.py @@ -1,15 +1,12 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: print(client.protocol_version) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/protocol_versions/tutorial002.py b/docs_src/protocol_versions/tutorial002.py index 5c00c8b4..d43ebb7b 100644 --- a/docs_src/protocol_versions/tutorial002.py +++ b/docs_src/protocol_versions/tutorial002.py @@ -1,15 +1,12 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." async def main() -> None: - async with Client(mcp, mode="legacy") as client: + async with Client("http://localhost:8000/mcp", mode="legacy") as client: print(client.protocol_version) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/protocol_versions/tutorial003.py b/docs_src/protocol_versions/tutorial003.py index 5fd32ac1..e5ca3ca4 100644 --- a/docs_src/protocol_versions/tutorial003.py +++ b/docs_src/protocol_versions/tutorial003.py @@ -1,15 +1,12 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." async def main() -> None: - async with Client(mcp, mode="2026-07-28") as client: + async with Client("http://localhost:8000/mcp", mode="2026-07-28") as client: print(client.protocol_version) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/docs_src/protocol_versions/tutorial004.py b/docs_src/protocol_versions/tutorial004.py index dd0443b9..36087769 100644 --- a/docs_src/protocol_versions/tutorial004.py +++ b/docs_src/protocol_versions/tutorial004.py @@ -1,20 +1,17 @@ +import anyio + from mcp import Client -from mcp.server import MCPServer - -mcp = MCPServer("Bookshop") - - -@mcp.tool() -def search_books(query: str) -> str: - """Search the catalog by title or author.""" - return f"Found 3 books matching {query!r}." async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: saved = client.session.discover_result - async with Client(mcp, mode="2026-07-28", prior_discover=saved) as client: + async with Client("http://localhost:8000/mcp", mode="2026-07-28", prior_discover=saved) as client: print(client.protocol_version) if client.server_info is not None: print(client.server_info.name) + + +if __name__ == "__main__": + anyio.run(main) diff --git a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py index a190b899..b04e6fb5 100644 --- a/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py +++ b/examples/clients/simple-auth-client/mcp_simple_auth_client/main.py @@ -233,7 +233,7 @@ class SimpleAuthClient: await self._run_session(read_stream, write_stream) else: print("šŸ“” Opening StreamableHTTP transport connection with auth...") - async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client: + async with httpx2.AsyncClient(auth=oauth_auth) as custom_client: async with streamable_http_client(url=self.server_url, http_client=custom_client) as ( read_stream, write_stream, diff --git a/examples/clients/simple-chatbot/README.MD b/examples/clients/simple-chatbot/README.md similarity index 100% rename from examples/clients/simple-chatbot/README.MD rename to examples/clients/simple-chatbot/README.md diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py index 5f219461..836032a1 100644 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ b/examples/servers/everything-server/mcp_everything_server/server.py @@ -12,7 +12,7 @@ from typing import Annotated, Any import click from mcp.server import ServerRequestContext -from mcp.server.mcpserver import Context, MCPServer, RequestStateSecurity +from mcp.server.mcpserver import Context, Elicit, ElicitationResult, MCPServer, RequestStateSecurity, Resolve from mcp.server.mcpserver.exceptions import ToolError from mcp.server.mcpserver.prompts.base import Prompt, UserMessage from mcp.server.streamable_http import EventCallback, EventMessage, EventStore @@ -350,6 +350,9 @@ def test_x_mcp_header( return f"region={region}" +# SEP-2575 server-stateless diagnostics (the conformance scenario probes these tools by name) + + @mcp.tool() async def test_missing_capability(ctx: Context) -> str: """Tests that a handler-raised MISSING_REQUIRED_CLIENT_CAPABILITY surfaces as a top-level JSON-RPC error. @@ -370,6 +373,33 @@ async def test_missing_capability(ctx: Context) -> str: return "Client declared sampling capability; proceeding." +def _ask_stream_probe() -> Elicit[UserResponse]: + return Elicit("The stateless streaming probe asks for a word", UserResponse) + + +@mcp.tool() +async def test_streaming_elicitation( + answer: Annotated[ElicitationResult[UserResponse], Resolve(_ask_stream_probe)], +) -> str: + """A tool that needs elicitation, asked through a resolver (SEP-2575 / SEP-2322). + + On 2026-07-28 the question is returned as an InputRequiredResult rather than sent + on the response stream; on earlier versions it is a mid-call elicitation request. + """ + return f"elicitation {answer.action}" + + +@mcp.tool() +async def test_logging_tool(ctx: Context) -> str: + """Logs once on the request-scoped channel (SEP-2575). + + On 2026-07-28 the message is delivered only when the request's `_meta` sets + `io.modelcontextprotocol/logLevel`. + """ + await ctx.info("test_logging_tool ran") # pyright: ignore[reportDeprecated] + return "logged through the request-scoped, logLevel-gated channel" + + # SEP-2322 InputRequiredResult fixtures (multi-round-trip / ephemeral workflow) NAME_SCHEMA = {"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]} diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index 0320871b..c53df6a5 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -71,6 +71,7 @@ def create_resource_server(settings: ResourceServerSettings) -> MCPServer: issuer_url=settings.auth_server_url, required_scopes=[settings.mcp_scope], resource_server_url=settings.server_url, + validate_token_resource=True, # tokens must be reported as issued for server_url ), ) # Store settings for later use in run() diff --git a/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py b/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py index 933935d6..687196d1 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py +++ b/examples/servers/simple-auth/mcp_simple_auth/token_verifier.py @@ -69,12 +69,19 @@ class IntrospectionTokenVerifier(TokenVerifier): logger.warning(f"Token resource validation failed. Expected: {self.resource_url}") return None + # `aud` may be a string or a list; report the entry naming this server when there is + # one, otherwise what the token was issued for, so the server can compare it. + aud: str | list[str] | None = data.get("aud") + audiences = aud if isinstance(aud, list) else [aud] if aud else [] + own = self.resource_url.rstrip("/") + resource = next((a for a in audiences if a.rstrip("/") == own), audiences[0] if audiences else None) + return AccessToken( token=token, client_id=data.get("client_id", "unknown"), scopes=data.get("scope", "").split() if data.get("scope") else [], expires_at=data.get("exp"), - resource=data.get("aud"), # Include resource in token + resource=resource, subject=data.get("sub"), # RFC 7662 subject (resource owner) claims=data, ) diff --git a/examples/servers/simple-tool/mcp_simple_tool/server.py b/examples/servers/simple-tool/mcp_simple_tool/server.py index a43dd0f7..ac99110b 100644 --- a/examples/servers/simple-tool/mcp_simple_tool/server.py +++ b/examples/servers/simple-tool/mcp_simple_tool/server.py @@ -1,15 +1,16 @@ import anyio import click +import httpx2 import mcp.types as types from mcp.server import Server, ServerRequestContext -from mcp.shared._httpx_utils import create_mcp_http_client async def fetch_website( url: str, ) -> list[types.ContentBlock]: headers = {"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"} - async with create_mcp_http_client(headers=headers) as client: + timeout = httpx2.Timeout(30, read=300) + async with httpx2.AsyncClient(headers=headers, timeout=timeout, follow_redirects=True) as client: response = await client.get(url) response.raise_for_status() return [types.TextContent(type="text", text=response.text)] diff --git a/examples/snippets/clients/identity_assertion_client.py b/examples/snippets/clients/identity_assertion_client.py index 19cde274..8c80f289 100644 --- a/examples/snippets/clients/identity_assertion_client.py +++ b/examples/snippets/clients/identity_assertion_client.py @@ -66,7 +66,7 @@ async def main() -> None: scope="user", ) - async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client: + async with httpx2.AsyncClient(auth=oauth_auth) as http_client: async with streamable_http_client("http://localhost:8001/mcp", http_client=http_client) as (read, write): async with ClientSession(read, write) as session: await session.initialize() diff --git a/examples/snippets/clients/oauth_client.py b/examples/snippets/clients/oauth_client.py index 58c542ea..11e0f5f9 100644 --- a/examples/snippets/clients/oauth_client.py +++ b/examples/snippets/clients/oauth_client.py @@ -72,7 +72,7 @@ async def main(): callback_handler=handle_callback, ) - async with httpx2.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client: + async with httpx2.AsyncClient(auth=oauth_auth) as custom_client: async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write): async with ClientSession(read, write) as session: await session.initialize() diff --git a/examples/snippets/servers/oauth_server.py b/examples/snippets/servers/oauth_server.py index 962ef061..204ede4b 100644 --- a/examples/snippets/servers/oauth_server.py +++ b/examples/snippets/servers/oauth_server.py @@ -24,8 +24,9 @@ mcp = MCPServer( # Auth settings for RFC 9728 Protected Resource Metadata auth=AuthSettings( issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL - resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL + resource_server_url=AnyHttpUrl("http://127.0.0.1:8000/mcp"), # This server's URL (mcp.run() default) required_scopes=["user"], + validate_token_resource=True, ), ) diff --git a/examples/stories/_shared/auth.py b/examples/stories/_shared/auth.py index 35e99724..cdbf3fd6 100644 --- a/examples/stories/_shared/auth.py +++ b/examples/stories/_shared/auth.py @@ -169,4 +169,5 @@ def auth_settings( required_scopes=scopes, client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=scopes, default_scopes=scopes), identity_assertion_enabled=identity_assertion_enabled, + validate_token_resource=True, ) diff --git a/examples/stories/bearer_auth/README.md b/examples/stories/bearer_auth/README.md index 9e809fad..530f048a 100644 --- a/examples/stories/bearer_auth/README.md +++ b/examples/stories/bearer_auth/README.md @@ -78,7 +78,7 @@ kill "$SERVER_PID" - `RESOURCE_URL` is hard-coded to port 8000 (the harness's in-process origin). If you change `--port`, edit `RESOURCE_URL` to match or the PRM document's `resource` field will be wrong. -- Auth is HTTP-only; over stdio or the in-memory transport `get_access_token()` +- Auth is HTTP-only; over stdio (or the in-memory test transport) `get_access_token()` returns `None` and there is no gate. - The 401/403 status codes and `WWW-Authenticate` header are HTTP-level and `Client` cannot observe them; they are pinned by diff --git a/examples/stories/bearer_auth/server.py b/examples/stories/bearer_auth/server.py index 45c9872c..31a0f77b 100644 --- a/examples/stories/bearer_auth/server.py +++ b/examples/stories/bearer_auth/server.py @@ -28,6 +28,7 @@ class StaticTokenVerifier(TokenVerifier): client_id="demo-client", scopes=[REQUIRED_SCOPE], expires_at=int(time.time()) + 3600, + resource=RESOURCE_URL, subject="demo-user", ) @@ -40,6 +41,7 @@ def build_app() -> Starlette: issuer_url=AnyHttpUrl(ISSUER), resource_server_url=AnyHttpUrl(RESOURCE_URL), required_scopes=[REQUIRED_SCOPE], + validate_token_resource=True, ), ) diff --git a/examples/stories/bearer_auth/server_lowlevel.py b/examples/stories/bearer_auth/server_lowlevel.py index e03cb26d..dd15fa88 100644 --- a/examples/stories/bearer_auth/server_lowlevel.py +++ b/examples/stories/bearer_auth/server_lowlevel.py @@ -46,6 +46,7 @@ def build_app() -> Starlette: issuer_url=AnyHttpUrl(ISSUER), resource_server_url=AnyHttpUrl(RESOURCE_URL), required_scopes=[REQUIRED_SCOPE], + validate_token_resource=True, ), token_verifier=StaticTokenVerifier(), transport_security=NO_DNS_REBIND, diff --git a/examples/stories/identity_assertion/README.md b/examples/stories/identity_assertion/README.md index 1002a5f7..68b986df 100644 --- a/examples/stories/identity_assertion/README.md +++ b/examples/stories/identity_assertion/README.md @@ -87,7 +87,7 @@ kill "$SERVER_PID" `exp` has passed and expires tokens out of its own store. - `transport_security=NO_DNS_REBIND` is harness-only; drop it for a real deployment. -- Auth is HTTP-only; over stdio or the in-memory transport there is no gate. +- Auth is HTTP-only; over stdio (or the in-memory test transport) there is no gate. ## Spec diff --git a/examples/stories/oauth_client_credentials/README.md b/examples/stories/oauth_client_credentials/README.md index d58b004c..82bc3c42 100644 --- a/examples/stories/oauth_client_credentials/README.md +++ b/examples/stories/oauth_client_credentials/README.md @@ -33,8 +33,8 @@ the client and server side. - `client.py` `main` — opens with `async with Client(target, mode=mode) as client:` and that's the whole program. `target` is a transport that already carries the OAuth `httpx2.Auth`; the body never touches a token. -- `client.py` `build_auth` — five lines of `ClientCredentialsOAuthProvider` - config is all the caller writes; the SDK does RFC 9728 PRM → +- `client.py` `build_auth` — a few lines of `ClientCredentialsOAuthProvider` + config (credentials plus `issuer=`) is all the caller writes; the SDK does RFC 9728 PRM → RFC 8414 AS-metadata discovery and token exchange on the first 401. - `server.py` `token_endpoint` — the *entire* AS for this grant: validate HTTP-Basic `client_id:client_secret`, mint a token, return RFC 6749 JSON. diff --git a/examples/stories/oauth_client_credentials/client.py b/examples/stories/oauth_client_credentials/client.py index 78dc7c7c..72e10868 100644 --- a/examples/stories/oauth_client_credentials/client.py +++ b/examples/stories/oauth_client_credentials/client.py @@ -8,13 +8,13 @@ from stories._harness import Target, run_client # MCP_URL pins the resource to :8000, and the server side builds its PRM/AS metadata from # the same constant — run the server on 8000 or the discovery chain points at the wrong origin. -from stories._shared.auth import MCP_URL, InMemoryTokenStorage +from stories._shared.auth import BASE_URL, MCP_URL, InMemoryTokenStorage from .server import DEMO_CLIENT_ID, DEMO_CLIENT_SECRET, DEMO_SCOPE def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth: - """The ``httpx2.Auth`` for the ``client_credentials`` grant — five lines of provider config. + """The ``httpx2.Auth`` for the ``client_credentials`` grant — a few lines of provider config. The SDK then handles 401 → RFC 9728 PRM → RFC 8414 AS-metadata discovery → token POST → Bearer attachment automatically. ``Client(url)`` has no ``auth=`` passthrough yet, so the @@ -27,6 +27,7 @@ def build_auth(_http: httpx2.AsyncClient) -> httpx2.Auth: client_id=DEMO_CLIENT_ID, client_secret=DEMO_CLIENT_SECRET, scope=DEMO_SCOPE, + issuer=BASE_URL, ) diff --git a/examples/stories/oauth_client_credentials/server.py b/examples/stories/oauth_client_credentials/server.py index 7e3d910e..0ccc3e32 100644 --- a/examples/stories/oauth_client_credentials/server.py +++ b/examples/stories/oauth_client_credentials/server.py @@ -66,7 +66,14 @@ def build_app() -> Starlette: if creds != f"{DEMO_CLIENT_ID}:{DEMO_CLIENT_SECRET}": return JSONResponse({"error": "invalid_client"}, status_code=401) access = f"access_{secrets.token_hex(16)}" - issued[access] = AccessToken(token=access, client_id=DEMO_CLIENT_ID, scopes=[DEMO_SCOPE], expires_at=None) + resource = form.get("resource") # RFC 8707: bind the token to the resource the client asked for + issued[access] = AccessToken( + token=access, + client_id=DEMO_CLIENT_ID, + scopes=[DEMO_SCOPE], + expires_at=None, + resource=resource if isinstance(resource, str) else None, + ) body = OAuthToken(access_token=access, token_type="Bearer", expires_in=3600, scope=DEMO_SCOPE) return JSONResponse(body.model_dump(exclude_none=True), headers={"cache-control": "no-store"}) diff --git a/examples/stories/oauth_client_credentials/server_lowlevel.py b/examples/stories/oauth_client_credentials/server_lowlevel.py index cde947e9..5879bb82 100644 --- a/examples/stories/oauth_client_credentials/server_lowlevel.py +++ b/examples/stories/oauth_client_credentials/server_lowlevel.py @@ -63,7 +63,14 @@ def build_app() -> Starlette: if creds != f"{DEMO_CLIENT_ID}:{DEMO_CLIENT_SECRET}": return JSONResponse({"error": "invalid_client"}, status_code=401) access = f"access_{secrets.token_hex(16)}" - issued[access] = AccessToken(token=access, client_id=DEMO_CLIENT_ID, scopes=[DEMO_SCOPE], expires_at=None) + resource = form.get("resource") # RFC 8707: bind the token to the resource the client asked for + issued[access] = AccessToken( + token=access, + client_id=DEMO_CLIENT_ID, + scopes=[DEMO_SCOPE], + expires_at=None, + resource=resource if isinstance(resource, str) else None, + ) body = OAuthToken(access_token=access, token_type="Bearer", expires_in=3600, scope=DEMO_SCOPE) return JSONResponse(body.model_dump(exclude_none=True), headers={"cache-control": "no-store"}) diff --git a/examples/stories/pagination/README.md b/examples/stories/pagination/README.md index f7113d4c..19fd7f0d 100644 --- a/examples/stories/pagination/README.md +++ b/examples/stories/pagination/README.md @@ -22,8 +22,8 @@ Drop `--server server_lowlevel` (on either transport) to run against the - `client.py` `main` — `async with Client(target, mode=mode) as client:` is the whole connection. The story owns the construction; `target` is whatever - `Client()` accepts (an in-process server, a transport, or an HTTP URL) and - the entry point picks it. + `Client()` accepts (an HTTP URL, `StdioServerParameters`, a transport, or, + in tests, an in-process server) and the entry point picks it. - `client.py` — `if page.next_cursor is None: break`. Termination is key-absent, not falsy; `while cursor:` would be a spec bug. - `server_lowlevel.py` — the handler owns the cursor encoding (here: an diff --git a/examples/stories/prompts/README.md b/examples/stories/prompts/README.md index 3bce94b9..7d184768 100644 --- a/examples/stories/prompts/README.md +++ b/examples/stories/prompts/README.md @@ -21,8 +21,9 @@ uv run python -m stories.prompts.client --http --server server_lowlevel ## What to look at - `client.py` `main` — the body opens with `async with Client(target, - mode=mode) as client:`; `target` is anything `Client(...)` accepts (an - in-process server, a `Transport`, or an HTTP URL). + mode=mode) as client:`; `target` is anything `Client(...)` accepts (an HTTP + URL, `StdioServerParameters`, a `Transport`, or, in tests, an in-process + server). - `server.py` `greet` vs `code_review` — return a bare `str` (wrapped as one user message) or a `list[Message]` for a multi-turn seed conversation. - `server.py` `complete()` — one global handler dispatches on `ref` + diff --git a/examples/stories/reconnect/README.md b/examples/stories/reconnect/README.md index a5d3d8f5..f07befd8 100644 --- a/examples/stories/reconnect/README.md +++ b/examples/stories/reconnect/README.md @@ -9,7 +9,7 @@ traffic and has `server_info` / `server_capabilities` available immediately. ## Run it ```bash -# over HTTP — Streamable HTTP only; in-memory has no "round-trip" to skip. +# HTTP only: the point of this story is skipping a network round trip. # The client self-hosts the server on a free port, runs, then tears it down. uv run python -m stories.reconnect.client --http # same, against the lowlevel-API server variant diff --git a/examples/stories/resources/README.md b/examples/stories/resources/README.md index 10b210fe..0436c033 100644 --- a/examples/stories/resources/README.md +++ b/examples/stories/resources/README.md @@ -21,8 +21,9 @@ uv run python -m stories.resources.client --http --server server_lowlevel - `client.py` `async with Client(target, mode=mode) as client:` — the one line every client example exists to teach. `target` is anything `Client()` - accepts (an in-process server, a transport, or an HTTP URL) and `mode=` is - always explicit; the rest of the story is the body of that `async with`. + accepts (an HTTP URL, `StdioServerParameters`, a transport, or, in tests, an + in-process server) and `mode=` is always explicit; the rest of the story is + the body of that `async with`. - `server.py` `app_config` vs `greeting` — a URI with no `{}` registers a static resource (appears in `resources/list`); a URI with `{name}` registers a template (appears only in `resources/templates/list`) and the placeholder diff --git a/examples/stories/schema_validators/README.md b/examples/stories/schema_validators/README.md index 984f1595..62b3d098 100644 --- a/examples/stories/schema_validators/README.md +++ b/examples/stories/schema_validators/README.md @@ -20,8 +20,9 @@ uv run python -m stories.schema_validators.client --http --server server_lowleve ## What to look at - `client.py` `main` — the body opens with `async with Client(target, mode=mode) - as client:`. `target` is anything `Client` accepts (an in-process server, a - transport, or an HTTP URL); the entry point picks it, the story constructs it. + as client:`. `target` is anything `Client` accepts (an HTTP URL, + `StdioServerParameters`, a transport, or, in tests, an in-process server); + the entry point picks it, the story constructs it. - `server.py` — `who.name` vs `who["name"]`: pydantic and dataclass parameters arrive as **instances** (attribute access); TypedDict and `dict[str, Any]` arrive as plain dicts. diff --git a/i18n/README.md b/i18n/README.md index 126f8bee..948e95cc 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -11,10 +11,10 @@ The English pages under `docs/` are the source. This directory holds what steers ```text uv run --frozen python scripts/docs/translations.py status [--lang CODE] -uv run --frozen --group translate python scripts/docs/translations.py translate --lang CODE [--pages PATH ...] +uv run --frozen --group translate python scripts/docs/translations.py translate [--lang CODE ...] [--pages PATH ...] [--jobs N] uv run --frozen python scripts/docs/translations.py stage [--lang CODE] ``` -`status` is offline: per language it lists missing, outdated (with the sections that changed), current and removable pages (translations whose English page is gone — `git rm` them). `translate` calls the Claude API (`ANTHROPIC_API_KEY` in the environment; the registry's model, or `DOCS_TRANSLATE_MODEL` to trial another) for the missing and outdated pages, retranslating only the English sections that changed and keeping the rest byte for byte; `--pages` instead re-translates exactly the named pages from scratch, which is also how a glossary or instructions change reaches existing pages (each generated page records the English section hashes it reflects, so editing those inputs invalidates nothing). `stage` assembles the tree each language site is built from (every language's, or one with `--lang`): each generated page exactly as it was generated, under an "outdated" notice linking the current English page when the English has changed since, and the English page where nothing was generated yet; `scripts/docs/build.sh` runs it before building them. Commit the generated pages in an ordinary pull request. +`status` is offline: per language it lists missing, outdated (with the sections that changed), current and removable pages (translations whose English page is gone — `git rm` them). `translate` calls the Claude API (`ANTHROPIC_API_KEY` in the environment; the registry's model, or `DOCS_TRANSLATE_MODEL` to trial another) for the missing and outdated pages of every language (or just the `--lang` ones), several pages at a time (`--jobs`, default 8; each page is its own request, so this changes how long the run takes, not what the model sees), retranslating only the English sections that changed and keeping the rest byte for byte; `--pages` instead re-translates exactly the named pages from scratch (in every language unless `--lang` narrows it), which is also how a glossary or instructions change reaches existing pages (each generated page records the English section hashes it reflects, so editing those inputs invalidates nothing). `stage` assembles the tree each language site is built from (every language's, or one with `--lang`): each generated page exactly as it was generated, under an "outdated" notice linking the current English page when the English has changed since, and the English page where nothing was generated yet; `scripts/docs/build.sh` runs it before building them. Commit the generated pages in an ordinary pull request. To add a language, add an entry to `languages.yml`, write `/instructions.md` (the sections the `pt` file has) and `/glossary.json`, then run `translate --lang `. diff --git a/i18n/de/pages/advanced/apps.md b/i18n/de/pages/advanced/apps.md index 2df9303c..266de049 100644 --- a/i18n/de/pages/advanced/apps.md +++ b/i18n/de/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ Das SDK liefert das als eingebaute Extension `Apps` (`io.modelcontextprotocol/ui ## Eine Uhr mit Gesicht {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ Nicht jeder Client rendert Apps. Die Spezifikation sagt unverblümt, was das fü Das Modell liest `content`; der iframe ist für Menschen. Ein UI-fƤhiger Host füttert das Modell trotzdem mit dem Textergebnis, und ein reiner Text-Client bekommt *nur* das. Das kanonische Muster ist also: ein Tool, zwei Antworten. Sieh dir `get_time` noch einmal an: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` ist nur dann `True`, wenn der Client die Extension `io.modelcontextprotocol/ui` deklariert **und** `text/html;profile=mcp-app` in seinen `mimeTypes`-Einstellungen aufgeführt hat. Das Feld ist Pflicht, ein Client, der es weglƤsst, zƤhlt also nicht. Genau das deklariert `main()` in derselben Datei: die Client-HƤlfte der Aushandlung – und die reichhaltige Antwort kommt zurück. +`client_supports_apps(ctx)` ist nur dann `True`, wenn der Client die Extension `io.modelcontextprotocol/ui` deklariert **und** `text/html;profile=mcp-app` in seinen `mimeTypes`-Einstellungen aufgeführt hat. Das Feld ist Pflicht, ein Client, der es weglƤsst, zƤhlt also nicht. Hier ist die Client-HƤlfte der Aushandlung: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Stelle `server.py` über HTTP bereit und starte dann den Client in einem zweiten Terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Die reichhaltige Antwort kam zurück. Lass `extensions=[APPS_SUPPORT]` im `Client`-Aufruf weg, und dasselbe Programm gibt stattdessen `The time is 2026-06-26T12:00:00Z.` aus – das ist alles, was ein reiner Text-Client je zu sehen bekommt. !!! warning Gib niemals einen Platzhalter wie `"[Rendered UI]"` als einzigen Inhalt zurück. Wenn der Fallback-Text nutzlos ist, ist das Tool für jeden reinen Text-Client und für das Modell selbst nutzlos. Schreib den Satz. diff --git a/i18n/de/pages/advanced/extensions.md b/i18n/de/pages/advanced/extensions.md index 79745141..c47c7ae3 100644 --- a/i18n/de/pages/advanced/extensions.md +++ b/i18n/de/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensions {#extensions} @@ -49,7 +49,7 @@ Nimm als PrƤfix eine Domain, die du kontrollierst. `io.modelcontextprotocol/*` Die kleinste nützliche Extension besteht aus einem Tool und einer Settings-Map: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ Die kleinste nützliche Extension besteht aus einem Tool und einer Settings-Map: * `settings()` ist der Wert, der unter `capabilities.extensions["com.example/stamps"]` angekündigt wird. Gib `{}` zurück (den Standardwert), um die Extension ohne Settings anzukündigen. * Die Extension bekommt den Server nie in die Hand. Sie deklariert ihre BeitrƤge als Daten; `MCPServer` verarbeitet sie. Es gibt kein `self.server`, das sie verƤndern kƶnnte. -Und `main()` ist der Beweis, ein In-Memory-Client direkt gegen `mcp`: +Stelle sie über HTTP bereit, und ein Client liefert den Beweis: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Jede `server.py` auf dieser Seite wird mit diesem Befehl bereitgestellt, und jede `client.py` lƤuft daneben mit `python client.py` aus einem zweiten Terminal. + ### Eigene Methoden bedienen {#serving-your-own-methods} Eine Extension kann **neue Request-Methoden** registrieren: eigene Verben, bedient neben denen der Spezifikation: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ Methoden sind **strikt additiv**. Das SDK erzwingt das bei der Konstruktion, nic ### Die Client-Seite {#the-client-side} -Das `main()` derselben Datei ist die ganze Client-Geschichte, beide HƤlften davon: +Der Client ist ein eigenes Programm und trƤgt beide HƤlften der Client-Geschichte: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` deklariert die Extension. Die Deklarationen werden zu `ClientCapabilities.extensions`: Auf einer 2026-07-28-Verbindung reist die Map im `_meta`-Umschlag jedes einzelnen Requests, der Server sieht sie also bei **jedem** Request; auf einer Legacy-Verbindung reist sie mit dem `initialize`-Handshake. Dem Server-Code ist das egal: `require_client_extension(ctx, ...)` und `ctx.session.check_client_capability(...)` lesen auf beiden Pfaden die richtige Quelle. * Vendor-Methoden steigen eine Schicht tiefer zu `client.session.send_request(...)` hinab; `Client` bekommt nur für Verben der Spezifikation eigene Methoden. `send_request` akzeptiert jede `Request`-Unterklasse, der Vendor-Request geht also unverƤndert durch. +* `SearchRequest` und die beiden Models, die er trƤgt, sind der Vertrag der Extension auf der Leitung, also deklariert der Client sie für sich selbst. Eine verƶffentlichte Extension würde sie in einem Paket ausliefern, das beide Seiten importieren. ### `tools/call` abfangen {#intercepting-toolscall} @@ -109,12 +116,18 @@ Der Hook umhüllt `tools/call` und sonst nichts. Für alles, was jede Nachricht ## Eine Client-Extension verwenden {#using-a-client-extension} -Eine **Client-Extension** ist derselbe Vertrag von der konsumierenden Seite: ein Bündel clientseitigen Verhaltens hinter einem einzigen Identifier. Übergib Instanzen an `Client(extensions=[...])` und rufe Tools ganz normal auf: +Eine **Client-Extension** ist derselbe Vertrag von der konsumierenden Seite: ein Bündel clientseitigen Verhaltens hinter einem einzigen Identifier. Der Server hier beantwortet `buy` mit einem einzulƶsenden Beleg statt mit der Ware, und das nur für einen Client, der die Extension deklariert hat: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +Übergib auf dem Client Instanzen an `Client(extensions=[...])` und rufe Tools ganz normal auf: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` gibt ein gewƶhnliches `CallToolResult` zurück, wie jeder andere Aufruf. Was die Extension geƤndert hat: Der Server darf `buy` jetzt mit einer `receipt`-**Ergebnisform** statt mit einem endgültigen Ergebnis beantworten, und `Receipts` bringt sie zu Ende (hier, indem sie den Beleg mit einem Folgeaufruf einlƶst), bevor `call_tool` zurückkehrt. An der Aufrufstelle bewegt sich nichts. Lass die Extension weg, und nichts davon existiert: Die Schranke des Servers weist einen Client ab, der sie nicht deklariert hat (Fehler -32021), und eine beanspruchte Form von einem Server, der die Schranke überspringt, fƤllt durch die Validierung, genau wie die Spezifikation es für einen unbekannten `resultType` verlangt. Standardmäßig aus, an beiden Enden der Leitung. @@ -124,15 +137,15 @@ Um einen Identifier **ohne** clientseitiges Verhalten anzukündigen (der Server ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## Eine Client-Extension schreiben {#writing-a-client-extension} Leite von `ClientExtension` ab und überschreibe nur, was du brauchst. Drei Arten von BeitrƤgen, jede mit einer Standardimplementierung: `settings()`, `claims()` und `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * Der Identifier folgt derselben Grammatik wie auf dem Server und wird validiert, wenn die Klasse definiert wird. @@ -153,12 +166,18 @@ Zwei stille Regeln. Claims sind nur auf 2026-07-28-Verbindungen aktiv, und die C ### Extension-Verben {#extension-verbs} -Die eigenen Request-Methoden einer Extension brauchen keine clientseitige Registrierung. Ein Vendor-Request-Typ leitet von `mcp.types.Request` ab und geht durch `client.session.send_request`, wie in [Eigene Methoden bedienen](#serving-your-own-methods). Eine ErgƤnzung: Wenn ein Params-Schlüssel im `Mcp-Name`-Header mitreisen muss (Extension-Spezifikationen wie Tasks verlangen das für ihre Verben), deklariert der Request-Typ `name_param`: +Die eigenen Request-Methoden einer Extension brauchen keine clientseitige Registrierung. Ein Vendor-Request-Typ leitet von `mcp.types.Request` ab und geht durch `client.session.send_request`, wie in [Eigene Methoden bedienen](#serving-your-own-methods). Nimm einen Server, dessen Extension ein einziges Verb zu einem benannten Job bedient: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +Eine ErgƤnzung auf dem Client: Wenn ein Params-Schlüssel im `Mcp-Name`-Header mitreisen muss (Extension-Spezifikationen wie Tasks verlangen das für ihre Verben), deklariert der Request-Typ `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Die Session spiegelt `params["jobId"]` auf jedem Sendepfad in `Mcp-Name`, und ein fehlender Wert scheitert laut, statt einen erforderlichen Header stillschweigend wegzulassen. ## Was eine Extension nicht kann {#what-an-extension-cannot-do} diff --git a/i18n/de/pages/advanced/low-level-server.md b/i18n/de/pages/advanced/low-level-server.md index a84bc12e..0158d393 100644 --- a/i18n/de/pages/advanced/low-level-server.md +++ b/i18n/de/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # Der Low-Level-Server {#the-low-level-server} @@ -36,18 +36,22 @@ Drei Dinge haben sich geƤndert, und sie sind die ganze Low-Level-API: ### Ausprobieren {#try-it} -Hierfür gibt es keinen Inspector: `mcp dev` und `mcp run` akzeptieren nur einen `MCPServer`. Dem In-Memory-`Client` ist das egal; er nimmt einen Low-Level-`Server` genauso wie einen `MCPServer`: +`mcp dev` und `mcp run` akzeptieren nur einen `MCPServer`, also betreibst du diesen hier selbst. Die letzte Zeile von `server.py` baut daraus eine gewƶhnliche ASGI-App, und uvicorn führt sie aus: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Richte den Inspector oder einen beliebigen Client auf `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ Derselbe Text, den die `@mcp.tool()`-Version erzeugt hat. Zwei ehrliche Untersch * `result.structured_content` ist `None`. Der High-Level-Server verpackt ein `-> str` für dich in `{"result": ...}`; hier baut niemand, was du nicht gebaut hast. * `list_tools` gibt das Schema zurück, das **du** getippt hast, Zeichen für Zeichen. Die High-Level-Version hatte `"title": "Query"` auf jeder Property und ein `"title": "search_booksArguments"` an der Wurzel: Pydantic-Artefakte. Hier unten gilt: Was auf der Leitung ist, hast du dort hingelegt. +In einem Test sparst du dir uvicorn und den Port: `Client(server)` nimmt einen Low-Level-`Server` im selben Prozess genauso entgegen wie einen `MCPServer`, und **[Testen](../get-started/testing.md)** ist genau dieses Muster. + ## Nichts wird für dich geprüft {#nothing-is-checked-for-you} `MCPServer` weist ein fehlerhaftes Argument ab, bevor deine Funktion überhaupt lƤuft, indem er den Aufruf gegen das generierte Schema validiert (**[Tools](../servers/tools.md)**). @@ -215,4 +221,4 @@ Jeder davon ist eine Idee, für die du jetzt das Vokabular hast; jeder hat seine * `add_request_handler(method, params_type, handler)` bedient jede Methode. `initialize` ist reserviert. * Die Capabilities, die ein `Server` ankündigt, leiten sich davon ab, welche Handler du registriert hast. -`Client(server)` hat beide Server identisch behandelt, weil sie dasselbe Protokoll *sind* – und genau darum geht es. Die nƤchste Schicht darunter ist gar keine Klasse: Es ist **[Middleware](middleware.md)**. +Der Client hat beide Server identisch behandelt, weil sie dasselbe Protokoll *sind* – und genau darum geht es. Die nƤchste Schicht darunter ist gar keine Klasse: Es ist **[Middleware](middleware.md)**. diff --git a/i18n/de/pages/advanced/pagination.md b/i18n/de/pages/advanced/pagination.md index a082b294..8b84af10 100644 --- a/i18n/de/pages/advanced/pagination.md +++ b/i18n/de/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Paginierung {#pagination} @@ -19,21 +19,25 @@ Paginierung ist für den Server gedacht, dessen Ressourcenliste in Wahrheit eine --8<-- "docs_src/pagination/tutorial001.py" ``` -* Auf einem Low-Level-`Server` sind Handler Konstruktorargumente, keine Dekoratoren. `on_list_resources` beantwortet jeden `resources/list`-Request; mehr Verkabelung gibt es nicht. -* Jeder paginierte Handler ist als `params: PaginatedRequestParams | None` typisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK jedoch nie `None` (ein Request ohne `params`-Member erreicht den Handler als Modell mit seinen Standardwerten). Das Signal, auf das es ankommt, ist daher `params.cursor is None`: **von vorne beginnen**. +* Auf einem Low-Level-`Server` sind Handler Konstruktorargumente, keine Dekoratoren. `on_list_resources` beantwortet jeden `resources/list`-Request; das ist schon die ganze Anbindung. +* Jeder paginierte Handler ist als `params: PaginatedRequestParams | None` typisiert, und das Beispiel akzeptiert beides. Über eine Verbindung übergibt dir das SDK allerdings nie `None` (ein Request ohne `params`-Member erreicht den Handler als Modell mit seinen Standardwerten). Das Signal, auf das es ankommt, ist daher `params.cursor is None`: **von vorne beginnen**. * Du entscheidest, was ein Cursor *ist*. Hier ist es ein Offset, als String dargestellt. Ein Zeitstempel, ein PrimƤrschlüssel, ein Base64-Blob: alles, was du beim Herausgeben erzeugen und beim Zurückkommen wiedererkennen kannst. * Mit `next_cursor=None` sagst du ā€ždas war die letzte Seiteā€œ. Es gibt keine Anzahl, keine Gesamtsumme, kein `has_more`. `None` ist das ganze Signal. !!! tip Eine `PAGE_SIZE` von 10 macht das Beispiel lesbar. WƤhle deine pro Endpunkt: Eine Liste - einzeiliger Ressourcen vertrƤgt eine Seite mit 500 EintrƤgen; eine Liste fetter Prompt-Templates nicht. + einzeiliger Ressourcen vertrƤgt eine Seite mit 500 EintrƤgen; eine Liste üppiger Prompt-Templates nicht. Der Client hat dabei nichts mitzureden, und das ist Absicht. ### Ausprobieren {#try-it} -`Client(server)` verbindet sich im Speicher mit einem Low-Level-`Server` genau so, wie er sich mit einem `MCPServer` verbindet. +`mcp run` akzeptiert nur einen `MCPServer`, diesen hier stellst du also selbst bereit. Die letzte Zeile von `server.py` baut aus dem `Server` eine gewƶhnliche ASGI-App, und uvicorn führt sie aus: -Rufe `list_resources()` ohne Argumente auf. Du bekommst zehn Ressourcen, `book-1` bis `book-10`, und `next_cursor` ist der String `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Richte einen beliebigen Client (**[Der Client](../client/index.md)** oder den Inspector) auf `http://localhost:8000/mcp` und rufe `list_resources()` ohne Argumente auf. Du bekommst zehn Ressourcen, `book-1` bis `book-10`, und `next_cursor` ist der String `"10"`. Gib ihn mit `list_resources(cursor="10")` zurück, und die erste Ressource ist `book-11`, der neue `next_cursor` ist `"20"`. @@ -43,7 +47,7 @@ Die zehnte Seite kommt mit `next_cursor` auf `None` zurück. Fertig. Jede `list_*`-Methode auf `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) nimmt ein Keyword-Argument `cursor=`. Eine paginierte Liste leerzulesen ist ein einziges `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Jede `list_*`-Methode auf `Client` (`list_tools`, `list_resources`, `list_resour * Erweitere die Liste, **bevor** du auf `next_cursor` schaust: Auch die letzte Seite enthƤlt Ressourcen. * `next_cursor is None` ist der Ausstieg. Alles andere geht unverƤndert direkt zurück in `cursor=`. -Führe sein `main()` aus, und es gibt `100 resources` aus: zehn Seiten zu je zehn, zusammengefügt von einer Schleife, die nie wusste, dass es zehn Seiten waren. +WƤhrend uvicorn weiterhin `server.py` ausliefert, starte in einem zweiten Terminal `python client.py`. Es gibt `100 resources` aus: zehn Seiten zu je zehn, zusammengefügt von einer Schleife, die nie wusste, dass es zehn Seiten waren. Das ist dieselbe Schleife, die **[Der Client](../client/index.md)** für jedes `list_*`-Verb zeigt, und sie kostet nichts gegenüber einem Server, der nicht paginiert: `next_cursor` ist schon in der ersten Response `None`, und die Schleife lƤuft genau einmal. diff --git a/i18n/de/pages/client/caching.md b/i18n/de/pages/client/caching.md index 88602435..43ce2df4 100644 --- a/i18n/de/pages/client/caching.md +++ b/i18n/de/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Caching-Hinweise {#caching-hints} @@ -31,7 +31,7 @@ Ohne weitere Konfiguration sagt jedes Ergebnis `ttlMs: 0, cacheScope: "private"` Auf dem Low-Level-`Server` bauen Handler ihre Ergebnisse von Hand, und `ttl_ms` / `cache_scope` sind einfach Felder der Ergebnismodelle. Ein Handler, der sie explizit setzt, gewinnt immer gegen die Map aus dem Konstruktor, Feld für Feld: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -45,10 +45,24 @@ Ein Vorbehalt bei paginierten Listen: Das Protokoll verlangt **denselben `cacheS In einer 2026-07-28-Session beachtet `Client` die Hinweise für dich: Er hat einen eingebauten Response-Cache, der standardmäßig aktiv ist. Ein Ergebnis, das mit einem `ttlMs` ankommt, wird gespeichert, und ein identischer Aufruf innerhalb dieser TTL wird ohne Roundtrip aus dem Cache bedient. Ein Ergebnis, das *keinen* Hinweis trƤgt, wird nicht gecacht: Ergebnisse ohne Hinweis bekommen `CacheConfig.default_ttl_ms`, dessen Standardwert `0` ist (sofort abgelaufen), sodass ein Server, der nichts deklariert, Aufruf für Aufruf genau denselben Verkehr sieht wie schon immer. -```python title="client.py" hl_lines="33 35 38" +Um das zu beobachten, stelle die `server.py` aus dem vorigen Abschnitt mit uvicorn bereit (ihre letzte Zeile baut die ASGI-App). Der Handler gibt jedes Mal eine Zeile aus, wenn er tatsƤchlich lƤuft: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Führe `python client.py` in einem zweiten Terminal aus. Es gibt die Hinweise aus, die das erste Ergebnis trug: das `ttlMs` des Handlers neben dem `cacheScope` der Map: + +```text +1000 public +``` + +Das Terminal des Servers erzƤhlt den Rest: Zwischen den Request-Logs von uvicorn erscheint `tools/list served` dreimal. + Vier Aufrufe, drei Abrufe. Der zweite Aufruf fand einen frischen Eintrag und erreichte den Server nie; die (injizierte) Uhr über die TTL hinaus vorzustellen ließ den dritten wieder abrufen; der vierte gab `cache_mode="refresh"` an. Dieses Keyword-Argument gibt es auf den fünf cachenden Verben (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (der Standardwert) liefert einen frischen Eintrag, wenn es einen gibt, und speichert andernfalls den Abruf. @@ -57,7 +71,7 @@ Vier Aufrufe, drei Abrufe. Der zweite Aufruf fand einen frischen Eintrag und err Eine Regel steht über `"use"`: **Aufrufe mit `meta` erreichen immer den Server.** Ein Request mit gesetztem `meta` (ein Progress-Token, Tracing-Felder) erwartet einen Request auf der Leitung, deshalb wird er unter `cache_mode="use"` wie `"refresh"` behandelt: Das Lesen aus dem Cache entfƤllt, und das abgerufene Ergebnis ersetzt trotzdem den gecachten Eintrag. `"bypass"` und ein explizites `"refresh"` verhalten sich wie immer. -Um das Caching ganz abzuschalten, konstruiere mit `Client(server, cache=None)`: Jeder Aufruf ist wieder ein Roundtrip, und `cache_mode` wird zwar weiter akzeptiert, bewirkt aber nichts. +Um das Caching ganz abzuschalten, übergib `cache=None` bei der Konstruktion des `Client`: Jeder Aufruf ist wieder ein Roundtrip, und `cache_mode` wird zwar weiter akzeptiert, bewirkt aber nichts. Auch der Scope wird automatisch beachtet: `"private"`-EintrƤge sind an die *Partition* des Caches gebunden (siehe unten), wƤhrend `"public"`-EintrƤge sich für breiteres Teilen entscheiden kƶnnen. Und **Benachrichtigungen schlagen die TTL** für genau die EintrƤge, die sie benennen: Eine `list_changed`-Benachrichtigung verdrƤngt die passende gecachte Liste, und `resources/updated` verdrƤngt den gecachten Lesevorgang, der unter exakt ihrem URI gespeichert ist – egal, wie frisch sie waren. Auf einer 2026-07-28-Verbindung kommen diese Benachrichtigungen auf einem `subscriptions/listen`-Stream an, den du mit `client.listen(...)` ƶffnest, und die VerdrƤngung ist abgeschlossen, bevor dein Watcher das Ereignis sieht; alles dazu steht in **[Abonnements](subscriptions.md)**. diff --git a/i18n/de/pages/client/callbacks.md b/i18n/de/pages/client/callbacks.md index ba7103a3..57d3216d 100644 --- a/i18n/de/pages/client/callbacks.md +++ b/i18n/de/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Client-Callbacks {#client-callbacks} @@ -63,7 +63,7 @@ Ein `tools/call` von dir, ein `elicitation/create` zurück vom Server, beantwort `mode="legacy"` im `Client(...)`-Aufruf leistet echte Arbeit. Standardmäßig handelt `Client(...)` den modernen Protokollpfad aus, und dieser Pfad hat keinen Rückkanal (back-channel) für Requests vom Server an den Client: `ctx.elicit` schlƤgt fehl, bevor dein Callback überhaupt lƤuft. Das entscheidet nicht der Transport, sondern das ausgehandelte - Protokoll – in-memory genauso wie über eine URL. Setze `mode="legacy"` fest, wann immer dein Client + Protokoll. Setze `mode="legacy"` fest, wann immer dein Client einen solchen Request beantworten muss; jeder Test hinter dieser Seite tut das. Alles Weitere steht in **[Protokollversionen](../protocol-versions.md)**. In einer 2026-07-28-Session ist der Callback nicht tot, er wird nur anders gespeist: Gibt ein Tool ein @@ -151,4 +151,4 @@ Zwei weitere. Keiner deklariert etwas. * `sampling_callback` und `list_roots_callback` funktionieren genauso, bedienen aber veraltete Features; moderne Server verwenden stattdessen Multi-Roundtrip-Requests. * `logging_callback` und `message_handler` empfangen Benachrichtigungen. Sie deklarieren nichts. -Das erste Argument von `Client(...)` ist ein Transport-Objekt. **[Client-Transporte](transports.md)** behandelt jede Art davon. +Das erste Argument von `Client(...)` wƤhlt den Transport. **[Client-Transporte](transports.md)** behandelt jede Art davon. diff --git a/i18n/de/pages/client/index.md b/i18n/de/pages/client/index.md index 4a5a9361..a3ce8240 100644 --- a/i18n/de/pages/client/index.md +++ b/i18n/de/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # Der Client {#the-client} @@ -11,13 +11,23 @@ Er ist ein einziges Objekt mit einem einzigen Lebenszyklus: erzeugen, `async wit ## Der erste Client {#your-first-client} -```python title="client.py" hl_lines="14-18" +Ein Client braucht einen Server, mit dem er sprechen kann. Mit diesem Bookshop verbindet sich jedes Beispiel auf dieser Seite. Speichere ihn als `server.py` und lass ihn über HTTP laufen: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst. Der Client sind die fünf hervorgehobenen Zeilen. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` bekommt das **Server-Objekt selbst**. Das ist der In-Memory-Transport: kein Subprozess, kein Port, kein HTTP. So verbindet sich jedes Beispiel auf dieser Seite und jeder Test, den du schreibst. +Damit lƤuft er unter `http://localhost:8000/mcp`. Der Client ist ein eigenes Programm. Speichere ihn als `client.py` und führe in einem zweiten Terminal `python client.py` aus: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` bekommt eine **URL** und verbindet sich deshalb über Streamable HTTP mit dem Server, den du gerade gestartet hast. * `async with` ist der **Lebenszyklus**. Beim Betreten wird verbunden und ausgehandelt, beim Verlassen getrennt. Es gibt kein `connect()`/`close()`-Paar, und ein `Client` lƤsst sich nach dem Ende des Blocks nicht wiederverwenden. * Innerhalb des Blocks liegen die Fakten zur Verbindung bereits als einfache Properties vor. @@ -25,10 +35,10 @@ Der Server oben ist nur da, damit du etwas hast, womit du dich verbinden kannst. `Client` nimmt ein positionelles Argument und leitet den Transport aus dessen Typ ab: -* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**. -* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Weg für die Produktion. -* Ein `StdioServerParameters`: der Befehl, der als **Subprozess** gestartet wird und mit dem über dessen stdin und stdout gesprochen wird. +* Ein URL-String (`Client("http://localhost:8000/mcp")`): Streamable HTTP, der Transport, hinter dem du deinen Server bereitstellst. +* Ein `StdioServerParameters`: der Befehl, der als lokaler **Subprozess** gestartet wird und mit dem über dessen stdin und stdout gesprochen wird. * Ein **Transport**: alles, was sich mit `async with ... as (read, write)` verwenden lƤsst, etwa `streamable_http_client(url, http_client=...)` um deinen eigenen HTTP-Client herum. +* Eine Instanz von `MCPServer` (oder des Low-Level-`Server`): Verbindung **im selben Prozess**, ohne Subprozess und ohne Port. Das ist die Variante für Tests, und **[Testen](../get-started/testing.md)** baut darauf auf. Alles Übrige auf dieser Seite ist in allen vier FƤllen identisch. Header, Subprozesse, Timeouts und das `Transport`-Protokoll haben ihre eigene Seite: **[Client-Transporte](transports.md)**. @@ -49,11 +59,11 @@ Eine Protokollversion hast du nie ausgewƤhlt. Standardmäßig sondiert der `Cli ## Tools auflisten {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` gibt ein `ListToolsResult` zurück; die Tools stehen in `.tools`. Jedes davon ist die vollstƤndige Definition, die ein Host einem Modell übergeben würde: +`list_tools()` gibt ein `ListToolsResult` zurück; die Tools stehen in `.tools`. Jedes davon ist die vollstƤndige Definition, die ein Host einem Modell übergeben würde. Hier ist das erste: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ und `tool.input_schema` ist das JSON-Schema, das der Server aus den Type Hints d Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern, und alles, was ein Modell braucht, um gültige Argumente zu erzeugen. +Das zweite Tool, `lookup_book`, wurde ohne `title=` registriert, sein `tool.title` ist also `None`. + !!! tip `title` ist optional, also muss sich eine UI, die einem Menschen Tools anzeigt, entscheiden: den `title`, wenn es einen gibt, sonst den `name`. `from mcp.shared.metadata_utils import get_display_name` macht genau das – @@ -86,7 +98,7 @@ Dieses Schema ist alles, was eine UI braucht, um ein Argumentformular zu rendern `call_tool(name, arguments)` führt das Tool aus und gibt dir ein `CallToolResult` zurück. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ Ein Tool, das eine Exception auslƶst, lƶst in deinem Client **keine** aus. Es Die Ressourcen-Verben kommen paarweise: zwei Wege zum Auflisten, einer zum Lesen. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ Ein Client kann sich auch mitteilen lassen, wann sich eine Ressource Ƥndert. Au ## Prompts {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ Ein Host reicht diese Nachrichten direkt an das Modell weiter. Das ist das ganze Ein Server mit einem Handler für VervollstƤndigungen kann Argumente von Prompts und Ressourcen-Templates automatisch vervollstƤndigen, wƤhrend die Person tippt. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ Die Antwort steht in `result.completion.values`. Tippe `"p"`, und der Server lie Jede `list_*`-Methode nimmt ein Keyword-Argument `cursor=`, und jedes Ergebnis trƤgt einen `next_cursor`. Wenn `next_cursor` `None` ist, hast du alles. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Diese Schleife ist gegenüber jedem Server korrekt. `MCPServer` gibt alles auf einer Seite zurück, also ist `next_cursor` `None` und die Schleife lƤuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in **[Paginierung](../advanced/pagination.md)**. +`list_all_tools` ist gegenüber jedem Server korrekt. `MCPServer` gibt alles auf einer Seite zurück, also ist `next_cursor` `None` und die Schleife lƤuft einmal – deshalb schreibt der meiste Code sie nie. Server, die wirklich paginieren, und die Regeln, denen Cursor gehorchen, stehen in **[Paginierung](../advanced/pagination.md)**. ## In Tests {#in-tests} -`Client(mcp)` ohne Prozess und ohne Port ist bereits ein Test-Harness für deinen Server. +Jede `client.py` auf dieser Seite hat `server.py` über HTTP erreicht. In einem Test überspringst du das Netzwerk und übergibst `Client` das Server-Objekt selbst: `from server import mcp`, dann `Client(mcp)`. Kein Prozess, kein Port, und jede Methode von oben funktioniert genauso. -Dafür gibt es ein eigenes Konstruktor-Flag: `Client(mcp, raise_exceptions=True)`. Es wirkt nur auf In-Memory-Verbindungen, und **[Testen](../get-started/testing.md)** ist die Seite, die es erklƤrt und das ganze Muster darum herum aufbaut. +Dafür gibt es ein eigenes Konstruktor-Flag: `Client(mcp, raise_exceptions=True)`. Es wirkt nur auf Verbindungen im selben Prozess, und **[Testen](../get-started/testing.md)** ist die Seite, die es erklƤrt und das ganze Muster darum herum aufbaut. ## Zusammenfassung {#recap} -* `Client(x)` verbindet sich in-memory mit einem Server-Objekt, über Streamable HTTP mit einem URL-String und über alles andere per Transport. +* `Client(x)` verbindet sich über Streamable HTTP mit einem URL-String, startet für ein `StdioServerParameters` einen Subprozess, betritt einen Transport direkt und nimmt in Tests das Server-Objekt selbst. * `async with` ist der ganze Lebenszyklus. Darin sind `server_capabilities` und `protocol_version` bereits gefüllt; `server_info` und `instructions` ebenfalls, wenn der Server sie liefert. * `list_tools()` gibt dir für jedes Tool `name`, `title`, `description` und `input_schema`. * `call_tool()` gibt `content` für das Modell, `structured_content` für deinen Code und `is_error` zurück. Ein Tool, das eine Exception auslƶst, ist ein Ergebnis, keine Exception. diff --git a/i18n/de/pages/client/oauth-clients.md b/i18n/de/pages/client/oauth-clients.md index bc2a86dc..d0b32ce8 100644 --- a/i18n/de/pages/client/oauth-clients.md +++ b/i18n/de/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth-Clients {#oauth-clients} @@ -81,18 +81,20 @@ Sieh dir `main()` an. Der Provider kommt an den **httpx2-Client**, der httpx2-Cl Wenn `Client` zum ersten Mal einen Request schickt, antwortet der Server mit `401`. Der Provider übernimmt: -1. **Discovery.** Er liest den `WWW-Authenticate`-Header, holt die Protected Resource Metadata des Servers von `/.well-known/oauth-protected-resource`, erfƤhrt, welcher Autorisierungsserver diese Ressource schützt, und holt die Metadaten *dieses* Servers. +1. **Discovery.** Er liest den `WWW-Authenticate`-Header, holt die Protected Resource Metadata des Servers von `/.well-known/oauth-protected-resource`, erfƤhrt, welcher Autorisierungsserver diese Ressource schützt, und holt die Metadaten *dieses* Servers. (Ein Ƥlterer Server, der keine Ressourcen-Metadaten verƶffentlicht, wird stattdessen an seinem eigenen Origin nach Autorisierungsserver-Metadaten gefragt.) So oder so müssen die Metadaten als `issuer` den Server nennen, für den sie geholt wurden; alles andere wird abgelehnt. 2. **Registrierung.** Nichts im Speicher? Er registriert dich dynamisch mit deiner `OAuthClientMetadata` und speichert das Ergebnis. 3. **Autorisierung.** Er generiert das PKCE-Paar und einen `state`, baut die Autorisierungs-URL, awaited deinen `redirect_handler` und awaited dann deinen `callback_handler` für den Code. 4. **Austausch.** Er tauscht den Code gegen ein `OAuthToken`, speichert es und wiederholt deinen ursprünglichen Request mit `Authorization: Bearer ...`. Danach ist Ruhe. Tokens kommen aus dem Speicher, ein abgelaufenes Access-Token wird mit dem Refresh-Token erneuert, und erst wenn nichts davon klappt, führt er den Flow erneut aus. +Eine Transportregel gilt für alle diese Requests: Wie der MCP-Request, in dem sie laufen, folgen sie einem Redirect nur, wenn er auf demselben Origin bleibt und die Methode beibehƤlt (etwa ein 307/308 wegen eines SchrƤgstrichs am Ende), und behandeln jeden anderen Redirect so, als würde diese URL nicht antworten. + Nichts davon hast du geschrieben. Zwei Keyword-Argumente bleiben übrig (`client_metadata_url` und `validate_resource_url`), und diese Datei braucht keines davon. `client_metadata_url` ist dasjenige, das man kennen sollte; es bekommt unten einen eigenen Abschnitt. ### Ausprobieren {#try-it} -Die meisten Beispiele in dieser Dokumentation kannst du mit einem In-Memory-`Client(server)` prüfen. Dieses nicht: Der ganze Sinn des Flows ist ein HTTP-`401`, und zwischen einem In-Memory-Client und seinem Server gibt es kein HTTP. +Der In-Memory-`Client(server)`, den deine Tests verwenden, hilft hier nicht: Der ganze Sinn des Flows ist ein HTTP-`401`, und zwischen einem In-Memory-Client und seinem Server gibt es kein HTTP. Das Repository liefert die Live-Variante mit. `examples/servers/simple-auth/` betreibt einen eigenstƤndigen Autorisierungsserver und einen geschützten MCP-Server; `examples/clients/simple-auth-client/` ist der Client dieser Seite, ausgebaut zu einem kleinen CLI. Sein README enthƤlt die beiden Befehle: Starte die Server, lass den Client gegen sie laufen, und du siehst die vier Schritte vorbeiziehen. @@ -110,13 +112,14 @@ Ein nƤchtlicher Job, ein CI-Schritt, ein anderer Dienst. Es gibt keinen Browser `ClientCredentialsOAuthProvider` ist dasselbe `httpx2.Auth`, ohne den Menschen: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` Was sich geƤndert hat: * Keine `OAuthClientMetadata`, keine Handler. Du übergibst `client_id` und `client_secret`; der Provider baut eine minimale `client_credentials`-Registrierung darum herum und überspringt die dynamische Registrierung komplett. +* `issuer` benennt den Autorisierungsserver, der diese Zugangsdaten ausgestellt hat; verwende den `issuer`-Wert, den sein Dokument unter `/.well-known/oauth-authorization-server` zurückgibt. Die Discovery lƤuft weiterhin wie oben, aber Token-Requests werden ausschließlich aus Metadaten für *diesen* Issuer gebaut; zeigt der MCP-Server irgendwo anders hin, bricht der Flow stattdessen mit einem `OAuthFlowError` ab. Ihn wegzulassen ist veraltet, und ab 3.0 ist er erforderlich (siehe **[Veraltete Features](../deprecated.md#deprecated-sdk-helpers)**); bis dahin warnt der Provider und verwendet den Autorisierungsserver, den die Discovery findet. * `scope` ist ein durch Leerzeichen getrennter String, das OAuth-Format auf der Leitung. * Alles danach ist identisch: dasselbe `TokenStorage`, derselbe `httpx2.AsyncClient(auth=...)`, derselbe `streamable_http_client`. @@ -129,7 +132,7 @@ Standardmäßig reist das Secret als HTTP Basic Auth im Token-Request (`client_s Ein weiterer Provider liegt in `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, für Clients, die sich mit einem JWT statt einem gemeinsamen Secret authentifizieren (`private_key_jwt`, die Variante mit Schlüsselpaar und Workload-IdentitƤt). Er folgt - demselben Muster: einen erzeugen, auf `auth=` setzen. Dasselbe Modul liefert + demselben Muster: einen erzeugen (er nimmt denselben optionalen `issuer` entgegen), auf `auth=` setzen. Dasselbe Modul liefert `SignedJWTParameters` und `static_assertion_provider`, zwei Helfer, die seine Assertion bauen. Es gibt noch eine Situation ohne Menschen: Der Client gehƶrt zu einem Unternehmen, dessen Identity Provider – nicht die Person am Host – entscheidet, welche MCP-Server er erreichen darf. Das ist ein anderer Grant mit eigenem Vertrauensmodell und eigener Seite: **[Identity Assertion](identity-assertion.md)**. diff --git a/i18n/de/pages/client/session-groups.md b/i18n/de/pages/client/session-groups.md index e94089b2..0f34e596 100644 --- a/i18n/de/pages/client/session-groups.md +++ b/i18n/de/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Session-Gruppen {#session-groups} @@ -78,7 +78,7 @@ HƤltst du bereits eine verbundene `ClientSession` (`Client.session` ist eine), ## Zusammenfassung {#recap} * `ClientSessionGroup` hƤlt viele Server-Verbindungen und führt deren Tools, Ressourcen und Prompts in je ein `dict` zusammen. -* `connect_to_server(params)` pro Server. Es nimmt Transport-Parameter entgegen, nie das Server-Objekt oder die URL, die ein `Client` entgegennimmt. +* `connect_to_server(params)` pro Server. Es nimmt Transport-Parameter entgegen, nie die URL oder den `Transport`, die ein `Client` entgegennimmt. * `group.call_tool(name, arguments)` leitet den Aufruf für dich an den zustƤndigen Server weiter. * Namen müssen in der gesamten Gruppe eindeutig sein; zwei Server mit einem `search`-Tool kƶnnen nicht ohne Weiteres nebeneinander bestehen. * `component_name_hook=` schreibt jeden registrierten Namen um. Der Dict-Schlüssel Ƥndert sich, der Name auf der Leitung nicht. diff --git a/i18n/de/pages/client/transports.md b/i18n/de/pages/client/transports.md index b605938f..75de6f81 100644 --- a/i18n/de/pages/client/transports.md +++ b/i18n/de/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Client-Transporte {#client-transports} @@ -11,30 +11,15 @@ Du konfigurierst nie einen separat. `Client` nimmt ein einziges positionales Arg Die *Server*-Seite jedes Transports (was `mcp.run()` tut und was du bereitstellst) steht in **[Den Server betreiben](../run/index.md)**. -## Im Speicher {#in-memory} - -Übergib das Server-Objekt selbst: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Kein Subprozess, kein Port, keine Bytes auf einer Leitung. Client und Server sind zwei Objekte im selben Prozess, und der Aufruf lƤuft trotzdem durch die echte Protokollschicht: `search_books` wird genau so aufgelistet, validiert und aufgerufen, wie es über HTTP geschƤhe. - -Damit ist es zwei Dinge zugleich: - -* **Eine Testumgebung.** Jedes Beispiel in dieser Dokumentation wird so ausgeführt, und die Seite **[Testen](../get-started/testing.md)** baut das ganze Muster darauf auf. -* **Eine Embedding-API.** Eine Anwendung, die den Server selbst erzeugt, braucht keinen Netzwerk-Hop, um dessen Tools aufzurufen. - ## Streamable HTTP {#streamable-http} -Übergib einen URL-String und du bekommst **Streamable HTTP**, den Transport, hinter dem du bereitstellst: +Übergib einen URL-String und du bekommst **Streamable HTTP** – den Transport, hinter dem du bereitstellst und zu dem du zuerst greifen solltest: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `streamable_http_client(...)`, auf Basis eines `httpx2.AsyncClient`, der so konfiguriert ist, wie MCP es braucht: `follow_redirects=True`, ein Timeout von 30 Sekunden für connect/write/pool und ein Read-Timeout von 300 Sekunden, weil der Server einen Response-Stream offen halten kann. +Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `streamable_http_client(...)`, auf Basis eines `httpx2.AsyncClient`, der so konfiguriert ist, wie MCP es braucht: ein Timeout von 30 Sekunden für connect/write/pool und ein Read-Timeout von 300 Sekunden, weil der Server einen Response-Stream offen halten kann. !!! check Ein `Client`, den du erzeugt hast, ist **nicht** verbunden. Das Erzeugen wƤhlt nur den Transport; @@ -50,7 +35,7 @@ Das ist der ganze Produktions-Client. `Client` packt die URL für dich in `strea Sobald du einen `Authorization`-Header, ein Cookie, einen Proxy, mTLS oder ein anderes Timeout brauchst, baust du den `httpx2.AsyncClient` selbst und übergibst ihn an `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ oder übergibst deinem `httpx2.AsyncClient` ein explizites `verify=ssl_context` !!! info `httpx2` behƤlt die vertraute `httpx`-API bei. Wenn du `httpx` kennst, weißt du hier also bereits, wie Auth, Proxys, Event-Hooks, Retries und Verbindungslimits gehen. Das SDK fügt nichts hinzu und nimmt - nichts weg. Hier dockt auch OAuth an: + nichts weg – außer bei der [Behandlung von Redirects](#redirects). Hier dockt auch OAuth an: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Der ganze Ablauf steht in **[OAuth-Clients](oauth-clients.md)**. +### Redirects {#redirects} + +Der Transport verbindet sich mit der URL, die du ihm gegeben hast, und nur mit diesem Origin. + +* Einem `307`/`308`-Redirect, der auf demselben Schema, Host und Port bleibt, wird gefolgt, ebenso `http://` → `https://` auf demselben Host. Das deckt den üblichen Trailing-Slash-Redirect `/mcp` → `/mcp/` ab. +* Einem Redirect irgendwo anders hin wird **nicht** gefolgt. Der Aufruf schlƤgt fehl mit: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Ist diese URL der Server, den du meintest, trag sie in deine Konfiguration ein. Wenn nicht, ist der Server oder ein Proxy davor falsch konfiguriert. + +Das gilt für jeden `httpx2.AsyncClient`, den du übergibst: Seine Einstellung `follow_redirects` wird für MCP-Requests nicht herangezogen, in keine der beiden Richtungen. Die OAuth-Provider des SDK wenden dieselbe Regel auf ihre eigenen Requests an. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` bedeutet, dass der + Server hinter einem TLS-terminierenden Proxy sitzt, von dem er nichts weiß, und `http://`-Redirects ausgibt. + Das behebst du auf dem Server (**[Bereitstellen und skalieren](../run/deploy.md#behind-a-tls-terminating-proxy)**) + oder indem du genau die `https://…/`-URL verwendest, die die Meldung vorschlƤgt. + ## stdio {#stdio} Ein **stdio**-Server ist ein Subprozess. Der Client startet ihn, schreibt JSON-RPC in seine stdin und liest JSON-RPC aus seiner stdout. So betreibt ein Desktop-Host einen Server auf deinem Rechner: Ein Host *ist* dieser Code plus eine UI, und **[Mit einem echten Host verbinden](../get-started/real-host.md)** zeigt dieselbe Beziehung von der Seite des Hosts, als Konfigurationsdatei. @@ -105,6 +111,18 @@ Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, bau Ein Server, der einen API-Key braucht, findet ihn dort nicht. Übergib ihn explizit mit `env=`; diese Variablen werden über die Allow-List gelegt. Genau das tut `BOOKSHOP_API_KEY` oben. +## Im Speicher {#in-memory} + +In einem Test gibt es nichts bereitzustellen und nichts zu starten. Übergib das Server-Objekt selbst: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Kein Subprozess, kein Port, keine Bytes auf einer Leitung. Client und Server sind zwei Objekte im selben Prozess, und der Aufruf lƤuft trotzdem durch die echte Protokollschicht: `search_books` wird genau so aufgelistet, validiert und aufgerufen, wie es über HTTP geschƤhe. **[Testen](../get-started/testing.md)** baut das ganze Muster darauf auf. + +Dieselbe Form dient zugleich als Embedding-API: Eine Anwendung, die den Server selbst erzeugt, kann dessen Tools ohne Netzwerk-Hop aufrufen. + ## SSE {#sse} `sse_client(url)` aus `mcp.client.sse` ist der HTTP-Transport, den Streamable HTTP abgelƶst hat. Pack ihn genauso ein, `Client(sse_client("http://localhost:8000/sse"))`, um mit einem Server zu sprechen, der ihn noch verwendet – und bau nichts Neues darauf. @@ -113,15 +131,16 @@ Die stderr des Kindprozesses landet in deiner. Um sie woandershin zu leiten, bau Für `Client` ist alles oben Genannte dasselbe. -Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` lƶst sein Argument nach Typ auf: Ein Server-Objekt verbindet im Prozess, ein `str` wird zu `streamable_http_client(url)`, ein `StdioServerParameters` wird zu `stdio_client(params)`, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst. +Ein **Transport** ist ein beliebiger asynchroner Kontextmanager, der ein `(read, write)`-Paar von Nachrichten-Streams liefert: formal das `Transport`-Protokoll in `mcp.client`. `Client` lƶst sein Argument nach Typ auf: Ein `str` wird zu `streamable_http_client(url)`, ein `StdioServerParameters` wird zu `stdio_client(params)`, ein Server-Objekt verbindet im Prozess, und alles andere wird direkt als Transport betreten. Diese letzte Regel ist der Grund, warum `stdio_client(...)`, `streamable_http_client(...)` und `sse_client(...)` alle in denselben Platz passen – und warum du deinen eigenen schreiben kannst. ## Zusammenfassung {#recap} -* `Client(mcp)` (das Server-Objekt) verbindet im Speicher. Nutze es für Tests und zum Einbetten. * `Client("http://.../mcp")` (eine URL) verbindet über Streamable HTTP, den Produktions-Transport. * Header, Auth, Proxys und Timeouts gehƶren auf einen `httpx2.AsyncClient`, den du an `streamable_http_client(url, http_client=...)` übergibst. Es gibt kein Keyword `headers=`. +* Redirects wird nur innerhalb des eigenen Origins der URL gefolgt (ein Trailing-Slash-`307`/`308`), plus `http`→`https` auf demselben Host. Alles andere schlƤgt mit `Redirect to … not followed` fehl; konfiguriere die endgültige URL. * stdio ist `Client(StdioServerParameters(...))`. Pack es nur dann selbst in `stdio_client(...)` ein, wenn du die stderr des Kindprozesses umleiten willst. * Der Subprozess bekommt eine Umgebung per Allow-List, nicht deine; `env=` ergƤnzt sie. +* `Client(mcp)` (das Server-Objekt) verbindet im Speicher. Nutze es in Tests oder um einen Server in die Anwendung einzubetten, die ihn gebaut hat. * Ein Transport ist alles, womit du `async with x as (read, write)` schreiben kannst. Alles, was weder Server-Objekt noch URL noch `StdioServerParameters` ist, reicht `Client` direkt an dieses Protokoll weiter. * Das Erzeugen eines `Client` wƤhlt den Transport. `async with` ƶffnet ihn. diff --git a/i18n/de/pages/deprecated.md b/i18n/de/pages/deprecated.md index 0887f2a8..0655c7ec 100644 --- a/i18n/de/pages/deprecated.md +++ b/i18n/de/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Veraltete Features {#deprecated-features} -Die Spec 2026-07-28 mustert fünf Dinge aus. Das SDK implementiert jedes davon weiterhin, und jedes davon trƤgt jetzt eine **Deprecation-Warnung**. Ein SDK-Helfer ist unabhƤngig davon veraltet und steht [am Ende](#deprecated-sdk-helpers). +Die Spec 2026-07-28 mustert fünf Dinge aus. Das SDK implementiert jedes davon weiterhin, und jedes davon trƤgt jetzt eine **Deprecation-Warnung**. Ein paar Dinge sind unabhƤngig davon nur auf SDK-Ebene veraltet; sie stehen [am Ende](#deprecated-sdk-helpers). Die Tabelle unten nennt jedes veraltete Feature, den Grund, warum es verschwindet, und den Ersatz, auf dem du aufbauen solltest. @@ -138,11 +138,13 @@ Das ist die ganze API. Es gibt keinen Schalter pro Methode, und du willst auch k ## Veraltete SDK-Helfer {#deprecated-sdk-helpers} -Das sind keine Spec-Ƅnderungen, sondern nur SDK-Interna mit einem besseren Ersatz. Sie warnen mit derselben `MCPDeprecationWarning` und werden in 3.0 entfernt. +Das sind keine Spec-Ƅnderungen, sondern nur Verwendungsweisen des SDK mit einem besseren Ersatz. Sie warnen mit derselben `MCPDeprecationWarning`, und 3.0 entfernt die alte Form. | Veraltet | Was du stattdessen tust | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` und danach `FuncMetadata.call_fn()`. Aufgerufen hat es ohnehin nur Code, der `FuncMetadata` direkt ansteuert (etwa eine eigene `Tool`-Unterklasse). | +| `AuthSettings(resource_server_url=...)` ohne `validate_token_resource=` | Setze es: Mit `True` lehnt der Server Bearer-Tokens ab, die dein Verifier nicht als für `resource_server_url` ausgestellt meldet; `False` besagt, dass dein Verifier die Audience des Tokens selbst prüft (siehe **[Autorisierung](run/authorization.md#a-token-verifier)**). Nicht gesetzt verhƤlt es sich wie `False`; 3.0 macht `True` zum Standardwert, sobald `resource_server_url` gesetzt ist. | +| `ClientCredentialsOAuthProvider(...)` oder `PrivateKeyJWTOAuthProvider(...)` ohne `issuer=` | Übergib `issuer=` und benenne damit den Autorisierungsserver, der die Credentials ausgestellt hat (siehe **[OAuth-Clients schreiben](client/oauth-clients.md#machine-to-machine)**). Ohne ihn entscheidet der MCP-Server, welcher Autorisierungsserver sie erhƤlt; 3.0 macht das Keyword-Argument verpflichtend. | ## Zusammenfassung {#recap} @@ -151,7 +153,7 @@ Das sind keine Spec-Ƅnderungen, sondern nur SDK-Interna mit einem besseren Ersa * Veraltet ist ein Hinweis: keine Ƅnderungen auf der Leitung, alles funktioniert weiterhin gegen Sessions von vor 2026, und du bekommst eine sichtbare `MCPDeprecationWarning` (eine `UserWarning`, also standardmäßig aktiv). * Sampling und Roots brauchen zusƤtzlich einen Rückkanal, den eine 2026-07-28-Session nicht hat. Auf einer modernen Verbindung warnen sie und werfen dann eine Exception. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` bringt die ganze Kategorie zum Schweigen; `"error::mcp.MCPDeprecationWarning"` in pytest macht daraus einen fehlschlagenden Test. -* Ein SDK-Helfer, `FuncMetadata.call_fn_with_arg_validation()`, ist separat veraltet und wird in 3.0 entfernt. +* Für die [veralteten SDK-Helfer](#deprecated-sdk-helpers) gilt dieselbe Regel: Sie warnen jetzt, und 3.0 entfernt die alte Form. * Neuer Code sollte auf nichts davon aufbauen. Jede andere Seite dieser Dokumentation vermittelt die aktuelle API. diff --git a/i18n/de/pages/get-started/first-steps.md b/i18n/de/pages/get-started/first-steps.md index e841e02b..d9e2ba0d 100644 --- a/i18n/de/pages/get-started/first-steps.md +++ b/i18n/de/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Erste Schritte {#first-steps} @@ -17,7 +17,7 @@ Drei Wƶrter, die dir ab hier auf jeder Seite begegnen: * Ein **Client** lebt im Host und spricht MCP. Der Host betreibt einen Client pro Server, mit dem er verbunden ist. * Ein **Server** ist das, was du mit diesem SDK baust. Er stellt Clients Dinge bereit. Mit dem Modell spricht er nie direkt. -Du schreibst den Server. Hosts sind das Produkt anderer. Das SDK gibt dir außerdem einen `Client`. Mit ihm testest du deine Server, und er taucht weiter unten auf dieser Seite auf. +Du schreibst den Server. Hosts sind das Produkt anderer. Das SDK gibt dir außerdem einen `Client` – dieselbe Klasse, mit der ein Host einen Server per URL erreichen oder als Subprozess starten würde. Er taucht weiter unten auf dieser Seite auf, und mit ihm testest du auch deine Server. ## Die drei Primitive {#the-three-primitives} @@ -83,22 +83,20 @@ Du hast im Inspector drei Tabs gesehen. Woher wusste er, dass es drei sind? Wenn sich ein Client verbindet, deklariert der Server seine **Capabilities**: welche Familien von Requests er beantwortet. Der Client entscheidet anhand dieser Deklaration, wonach er überhaupt fragt. Du hast sie nie geschrieben; `MCPServer` deklariert sie für dich. -Sieh es dir selbst an. Der `Client` des SDK nimmt das Server-Objekt direkt entgegen und verbindet sich **im Speicher** damit (kein Subprozess, kein Port): +Sieh es dir selbst an. Lass `server.py` in einem Terminal über HTTP laufen: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +und richte aus einem anderen Terminal einen Client darauf: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -115,11 +113,12 @@ Dieses Dictionary sind die deklarierten **Capabilities** deines Servers. Es ist `MCPServer` bedient alle drei Primitive, also werden immer alle drei deklariert. -Achte darauf, was fehlt. `completions` (die automatische VervollstƤndigung von Argumenten für Ressourcen-Templates und Prompts) braucht einen Handler, den du schreibst. Dieser Server hat keinen, also fehlt die Capability, und ein wohlerzogener Client fragt gar nicht erst. Das ist die Regel für alles Optionale: Registriere das Ding, und die Capability erscheint; **[VervollstƤndigungen](../servers/completions.md)** zeigt es. +Achte darauf, was fehlt. `completions` (die automatische VervollstƤndigung von Argumenten für Ressourcen-Templates und Prompts) braucht einen Handler, den du schreibst. Dieser Server hat keinen, also fehlt die Capability, und ein Client, der sich korrekt verhƤlt, fragt gar nicht erst. Das ist die Regel für alles Optionale: Registriere das Ding, und die Capability erscheint; **[VervollstƤndigungen](../servers/completions.md)** zeigt es. !!! info - `Client(mcp)` ist derselbe In-Memory-Client, mit dem jedes Beispiel in dieser Dokumentation - getestet wird, und so testest du auch deine. Er bekommt eine ganze Seite: **[Testen](testing.md)**. + Dieses `client.py` ist ein vollstƤndiger MCP-Client, und **[Der Client](../client/index.md)** ist seine Seite. + In einem Test sparst du dir das Terminal und den Port und übergibst `Client` das Server-Objekt + selbst: `Client(mcp)`. Auch das bekommt eine ganze Seite: **[Testen](testing.md)**. ## Was du nicht geschrieben hast {#what-you-did-not-write} @@ -128,7 +127,7 @@ Blick auf diese Seite zurück. Du hast drei kleine Python-Funktionen geschrieben * Ein JSON-Schema. `a: int, b: int` *ist* das Schema für `add`. * Einen Request-Handler. `tools/list`, `resources/read`, `prompts/get`: alles für dich bedient. * Eine Capability-Deklaration. `MCPServer` hat sie für dich erstellt. -* Eine Zeile Protokoll. Die Versionsaushandlung, das JSON-RPC-Framing, der Austausch der Capabilities: Das alles passierte in `mcp dev` und `Client(mcp)`, und du hast es nie gesehen. +* Eine Zeile Protokoll. Die Versionsaushandlung, das JSON-RPC-Framing, der Austausch der Capabilities: Das alles passierte in `mcp dev` und `client.py`, und du hast es nie gesehen. Dieses VerhƤltnis ist der ganze Sinn des SDK. @@ -139,6 +138,6 @@ Dieses VerhƤltnis ist der ganze Sinn des SDK. * Ein Dekorator pro Primitiv: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Name, Beschreibung und Schema kommen aus der Funktion. * Ein URI mit einem `{param}` ergibt ein Ressourcen-**Template**, das getrennt von konkreten Ressourcen aufgelistet wird. * Die **Capabilities** des Servers werden für dich deklariert, und ein Client fragt nur nach dem, was ein Server deklariert. -* `Client(mcp)` verbindet sich im Speicher mit dem Server-Objekt: deine Testumgebung vom ersten Tag an. +* `Client("http://localhost:8000/mcp")` spricht mit deinem laufenden Server. Übergib ihm stattdessen das Server-Objekt, `Client(mcp)`, und er ist deine Testumgebung vom ersten Tag an. Als NƤchstes kommt **[Mit einem echten Host verbinden](real-host.md)**: dieser Server in Claude Desktop oder einer IDE, in echt. Danach **[Testen](testing.md)**: eine Seite, ein In-Memory-Client, und du musst nie raten, ob es funktioniert. Danach bekommt jedes Primitiv seine eigene Seite, angefangen mit dem, das das Modell steuert: **[Tools](../servers/tools.md)**. diff --git a/i18n/de/pages/get-started/testing.md b/i18n/de/pages/get-started/testing.md index f5501402..7accf716 100644 --- a/i18n/de/pages/get-started/testing.md +++ b/i18n/de/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Testen {#testing} -Das Python SDK bringt eine Klasse `Client` mit einem **In-Memory-Transport** mit: Übergib ihr dein Server-Objekt, und sie verbindet sich direkt damit. +Die Klasse `Client` des SDK – dieselbe, die sich mit einer URL verbindet oder einen Subprozess startet – verbindet sich auch **In-Memory**: Übergib ihr dein Server-Objekt, und sie spricht direkt mit ihm. -Kein Subprozess. Kein Port. Überhaupt kein Transport. Die Idee ist dieselbe wie bei FastAPIs `TestClient`. +Kein Subprozess. Kein Port. Nichts auf der Leitung. Die Idee ist dieselbe wie bei FastAPIs `TestClient`. ## Grundlegende Verwendung {#basic-usage} @@ -97,12 +97,12 @@ statt der bereinigten. Lass es in Tests eingeschaltet. In Produktionscode hat es keine Bedeutung. -## Standardmäßig im selben Prozess {#in-process-by-default} +## Standardmäßig generationsneutral {#era-neutral-by-default} !!! note `Client(mcp)` verbindet sich im selben Prozess und ist standardmäßig **generationsneutral** (era-neutral): Er prüft den Server und wƤhlt den passenden Protokollpfad. Lege `mode="legacy"` fest, wenn dein Test Legacy-spezifische - Semantik prüft (Sampling- oder Elicitation-Push – Elicitation ist die Rückfrage bei der Person am Host –, `message_handler`), und lass `raise_exceptions=True` + Semantik prüft (Push von Sampling oder Elicitation – Rückfrage bei der Person am Host –, `message_handler`), und lass `raise_exceptions=True` dort weg: Eine Legacy-Verbindung bereinigt von vornherein nie, und das Flag lƶst den Fehler erneut in der Server-Task aus statt in deinem Test. diff --git a/i18n/de/pages/handlers/multi-round-trip.md b/i18n/de/pages/handlers/multi-round-trip.md index 37737b09..159fd896 100644 --- a/i18n/de/pages/handlers/multi-round-trip.md +++ b/i18n/de/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # Multi-Roundtrip-Requests {#multi-round-trip-requests} @@ -164,7 +164,7 @@ Der Low-Level-`Server` ist die Stufe ohne Extras: Anders als bei `MCPServer` wir ## Ein Ergebnis für 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` gibt es nur bei Protokollversion **2026-07-28**. Der In-Memory-`Client(server)` handelt sie für dich aus; über die Leitung entdeckt `mode="auto"` sie. Nach dem Verbinden sagt dir `client.protocol_version`, was du bekommen hast. +`InputRequiredResult` gibt es nur bei Protokollversion **2026-07-28**. `mode="auto"`, der Standardwert von `Client`, entdeckt sie auf jeder Verbindung. Nach dem Verbinden sagt dir `client.protocol_version`, was du bekommen hast. !!! warning Eine Session vor 2026 hat keinen Platz für ein `InputRequiredResult`. Gibst du eines aus deinem Handler auf einer diff --git a/i18n/de/pages/handlers/progress.md b/i18n/de/pages/handlers/progress.md index cdefdfab..77e947a2 100644 --- a/i18n/de/pages/handlers/progress.md +++ b/i18n/de/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Fortschritt {#progress} @@ -29,19 +29,17 @@ Drei Argumente, und du bestimmst, was sie bedeuten: Der Client meldet sich **pro Aufruf** an, indem er `progress_callback=` an `call_tool` übergibt: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,28 +54,31 @@ anyio.run(main) Der Callback ist eine `async`-Funktion, die genau das entgegennimmt, was der Server gemeldet hat: `progress`, `total`, `message`. !!! info - `Client(mcp)` verbindet sich direkt mit dem Server-Objekt, im Speicher – derselbe Client, auf dem die Seite - **[Testen](../get-started/testing.md)** aufbaut. `progress_callback` ist derselbe Parameter, egal welchen - Transport der `Client` nutzt; das *Timing*, das du gleich siehst, ist das der In-Memory-Verbindung. Sie führt - deinen Callback inline aus, sodass jede Meldung eintrifft, bevor `call_tool` zurückkehrt. Über einen echten - Transport liefern sich die Benachrichtigungen ein Rennen mit dem Ergebnis, und ein langsamer Callback kann noch - laufen, nachdem `call_tool` bereits zurückgekehrt ist. + `progress_callback` ist derselbe Parameter, egal was du `Client` übergeben hast: eine URL wie hier, ein + `StdioServerParameters`-Objekt oder das Server-Objekt in einem Test. Achte über einen echten Transport + allerdings auf das Timing. Jede Benachrichtigung wird für sich zugestellt, neben der Response, sodass ein + langsamer Callback noch laufen kann, nachdem `call_tool` bereits zurückgekehrt ist. Nur die + In-Process-Testverbindung führt den Callback inline aus und garantiert, dass jede Meldung vorher eintrifft. ### Ausprobieren {#try-it} -Lege `client.py` neben `server.py` und starte es: +Stelle `server.py` über HTTP bereit und starte dann den Client aus einem zweiten Terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Jedes `await ctx.report_progress(...)` auf dem Server wurde zu einem Aufruf von `show` auf dem Client, in derselben Reihenfolge, und beide Zeilen wurden ausgegeben, **bevor** `call_tool` zurückkehrte. Fortschritt wird nicht ins Ergebnis gepackt; er streamt, wƤhrend das Tool noch arbeitet. +Jedes `await ctx.report_progress(...)` auf dem Server wurde zu einem Aufruf von `show` auf dem Client, in derselben Reihenfolge. Fortschritt wird nicht ins Ergebnis gepackt. Er streamt, wƤhrend das Tool noch arbeitet. !!! warning `progress_callback` gehƶrt zum **Aufruf**, nicht zum `Client`. Es gibt kein Konstruktorargument dafür, diff --git a/i18n/de/pages/protocol-versions.md b/i18n/de/pages/protocol-versions.md index 887ed4ab..b4250730 100644 --- a/i18n/de/pages/protocol-versions.md +++ b/i18n/de/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Protokollversionen {#protocol-versions} @@ -11,9 +11,17 @@ Server, die vor 2026-07-28 verƶffentlicht wurden, erƶffnen jede Verbindung mit Darum musst du dich fast nie kümmern, denn `Client` handelt das für dich aus. Diese Seite behandelt das eine Konstruktorargument, das es steuert, `mode=`, und die drei FƤlle, in denen du es Ƥnderst. +Jedes Snippet auf dieser Seite ist eine `client.py`, die mit der Bookshop-`server.py` aus **[Der Client](client/index.md)** spricht. Starte diesen Server in einem Terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Führe dann jedes Snippet in einem zweiten Terminal mit `python client.py` aus. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ So oder so bist du am Ende verbunden, und `client.protocol_version` sagt dir, we Das ist das ganze Feature. Ein `Client`, Server jeder Generation, keine Verzweigung in deinem Code. !!! info - `MCPServer` beantwortet `server/discover` auf jedem Transport – In-Memory, stdio, Streamable - HTTP –, sodass `auto` gegen deinen eigenen Server immer bei `2026-07-28` landet. Der Fallback - greift nur gegen einen echten Server von vor 2026, und genau dann willst du ihn auch. + `MCPServer` beantwortet `server/discover` auf jedem Transport – Streamable HTTP, stdio und die + In-Process-Verbindung, die deine Tests verwenden –, sodass `auto` gegen deinen eigenen Server + immer bei `2026-07-28` landet. Der Fallback greift nur gegen einen echten Server von vor 2026, + und genau dann willst du ihn auch. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ Bei 2026-07-28 ist er weg. Der Server *gibt* seine Fragen *zurück*, und du wied `mode` akzeptiert auch den String einer modernen Protokollversion. Heute ist diese Menge genau `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ Die Sondierung ist billig, aber sie bleibt ein Roundtrip, den du bei jedem Neuve Also heb sie auf. Nach einer `auto`-Verbindung enthƤlt `client.session.discover_result` genau das `DiscoverResult`, das der Server gesendet hat: seine `supported_versions`, seine `capabilities`, seine `instructions` und die IdentitƤt, die der Server in das `_meta` des Ergebnisses gestempelt hat. Gib es beim nƤchsten Mal als `prior_discover=` zurück: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/de/pages/run/asgi.md b/i18n/de/pages/run/asgi.md index 23750a5c..ab304496 100644 --- a/i18n/de/pages/run/asgi.md +++ b/i18n/de/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # In eine bestehende App einbinden {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Das abschließende `/mcp` ist `streamable_http_path`. Setze es auf `"/"`, und da --8<-- "docs_src/asgi/tutorial004.py" ``` -Jetzt verbinden sich Clients mit `/notes`, nicht mit `/notes/mcp`. +Jetzt verbinden sich Clients mit `/notes/`, nicht mit `/notes/mcp`. ## CORS für Browser-Clients {#cors-for-browser-clients} @@ -142,4 +142,4 @@ Ein browserbasierter Client braucht zwei Erlaubnisse von dir: seine MCP-Request- * Browser-Clients brauchen CORS: `allow_headers` für die `Mcp-*`-Request-Header, `expose_headers=["Mcp-Session-Id"]` für die Response. * `@mcp.custom_route()` fügt einfache, nicht authentifizierte HTTP-Endpunkte neben `/mcp` hinzu. -Sobald der Server unter einer echten URL erreichbar ist, verbindet sich **[Der Client](../client/index.md)** über diese URL mit ihm statt über ein Server-Objekt. +Sobald der Server unter einer echten URL erreichbar ist, verbindet sich **[Der Client](../client/index.md)** über diese URL mit ihm. diff --git a/i18n/de/pages/run/authorization.md b/i18n/de/pages/run/authorization.md index 3cd5d35a..d717103c 100644 --- a/i18n/de/pages/run/authorization.md +++ b/i18n/de/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorisierung {#authorization} @@ -23,12 +23,12 @@ Das ist das ganze Dreieck. Alles auf dieser Seite betrifft den mittleren Punkt. Das SDK hat keine Meinung dazu, wie ein gültiges Token aussieht. Das sagst du ihm, indem du **`TokenVerifier`** implementierst: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` ist ein Protokoll mit einer einzigen asynchronen Methode. `verify_token` bekommt das rohe Token aus dem `Authorization`-Header und gibt ein **`AccessToken`** zurück, wenn es gültig ist, und `None`, wenn nicht. Mehr gibt es nicht zu implementieren. -* Dieser hier schlƤgt das Token in einer Tabelle nach. Ein echter prüft eine JWT-Signatur oder ruft den Token-Introspection-Endpunkt des Authorization Servers auf. Dieser Code gehƶrt dir; das SDK ruft ihn nur auf. +* Dieser hier schlƤgt das Token in einer Tabelle nach; jeder Eintrag hƤlt fest, für welche Ressource es ausgestellt wurde. Ein echter prüft eine JWT-Signatur oder ruft den Token-Introspection-Endpunkt des Authorization Servers auf und meldet in `AccessToken.resource`, für wen das Token ausgestellt wurde (sein `aud`). Dieser Code gehƶrt dir; das SDK ruft ihn nur auf. * `token_verifier=` und `auth=` treten immer gemeinsam auf. Übergibst du das eine ohne das andere, lƶst `MCPServer(...)` einen `ValueError` aus, bevor auch nur ein Request bedient wird. `AuthSettings` ist das ƶffentliche Gesicht deines Resource Servers: @@ -36,6 +36,10 @@ Das SDK hat keine Meinung dazu, wie ein gültiges Token aussieht. Das sagst du i * `issuer_url`: der Authorization Server, der deine Tokens ausstellt. * `resource_server_url`: die ƶffentliche URL dieses MCP-Endpunkts. Sie benennt, für *welche* Ressource ein Token gilt, und unter ihr liegt das Discovery-Dokument. * `required_scopes`: jedes Token muss alle davon tragen. +* `validate_token_resource`: lehnt jedes Token ab, dessen `AccessToken.resource` nicht `resource_server_url` ist. LƤsst du es ungesetzt, wƤhrend `resource_server_url` gesetzt ist, gibt es eine Warnung (`MCPDeprecationWarning`) und es verhƤlt sich wie `False`; ab 3.0 ist `True` der Standardwert für Resource Server. + * Schalte es ein, wenn dein Authorization Server Tokens an die `resource` bindet, die der Client angefordert hat – MCP-Clients senden sie immer. Halte `resource_server_url` exakt auf der URL, mit der sich Clients verbinden. + * Lass es aus, wenn dein Authorization Server eigene Audience-Bezeichner verwendet (einen Auth0-API-Identifier, eine Entra-Application-ID), und prüfe `aud` stattdessen in deinem Verifier: Gib für ein Token, das nicht für diesen Server bestimmt ist, `None` zurück. + * Ist `aud` eine Liste, trage in `resource` den Eintrag ein, der `resource_server_url` entspricht. !!! tip `examples/servers/simple-auth/` im SDK-Repository enthƤlt einen `IntrospectionTokenVerifier`, der den @@ -91,11 +95,11 @@ Schick ein `GET` an diesen Well-Known-Pfad, und du bekommst **Protected Resource In jedem Handler ist **`get_access_token()`** das `AccessToken`, das dein Verifier für den aktuellen Request zurückgegeben hat: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` -* Es funktioniert in Tools, Ressourcen und Prompts, und du musst nichts herumreichen: Die Auth-Middleware speichert es pro Request in einer Context-Variablen. +* Es funktioniert in Tools, Ressourcen und Prompts, und du musst nichts herumreichen: Die Auth-Middleware speichert es pro Request in einer Kontextvariablen. * Du bekommst **dasselbe Objekt zurück, das dein Verifier gebaut hat**: `client_id`, `scopes`, `subject`, `expires_at` und alle zusƤtzlichen `claims`, die du angehƤngt hast. Das ist der Ansatzpunkt für Regeln pro Tool: Lies die Scopes und lehne ab. * Außerhalb eines authentifizierten HTTP-Requests gibt es `None` zurück. In-Memory und über `stdio` ist es immer `None`. @@ -125,6 +129,6 @@ Ein Authorization Server kann statt einer Person, die sich durch einen Consent-S * `token_verifier=` und `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` treten immer gemeinsam auf. * Das SDK verƶffentlicht Protected Resource Metadata nach [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) unter `/.well-known/oauth-protected-resource/...` und beantwortet nicht authentifizierte Requests mit einer 401, deren `WWW-Authenticate`-Header darauf zeigt. Das ist die ganze Discovery-Geschichte. * `get_access_token()` in jedem Handler sagt dir, wer aufruft. -* Autorisierung ist eine HTTP-Angelegenheit. `stdio` und der In-Memory-Client bekommen sie nie zu sehen. +* Autorisierung ist eine HTTP-Angelegenheit. `stdio` und der In-Memory-Test-Client bekommen sie nie zu sehen. Die Client-HƤlfte (deinen Authorization Server finden und das Token für dich holen) steht unter **[OAuth-Clients](../client/oauth-clients.md)**. Und ein Client, der eine IdentitƤt *behauptet*, statt eine Person danach zu fragen, steht unter **[Identity Assertion](../client/identity-assertion.md)**. diff --git a/i18n/de/pages/run/deploy.md b/i18n/de/pages/run/deploy.md index ffb06c04..66e451d1 100644 --- a/i18n/de/pages/run/deploy.md +++ b/i18n/de/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Bereitstellen und skalieren {#deploy-scale} @@ -47,6 +47,22 @@ Hinter einem echten Hostnamen bereitgestellt, weist genau dieser Standard **jede bereitgestellter Server, der jede Verbindung ablehnt, ist bis zum Beweis des Gegenteils eine Host-Allowlist. Auch **[Fehlerbehebung](../troubleshooting.md)** fƤngt hier an. +## Hinter einem TLS-terminierenden Proxy {#behind-a-tls-terminating-proxy} + +Wenn TLS an einem Proxy endet (einem Ingress, einem Load Balancer, Caddy, nginx) und uvicorn dahinter unverschlüsseltes HTTP ausliefert, weise uvicorn an, den `X-Forwarded-*`-Headern des Proxys zu vertrauen: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Ohne das glaubt die App, sie werde über `http://` ausgeliefert, und jeder Redirect, den sie auslƶst (der übliche ist `/mcp` → `/mcp/`), zeigt auf `http://…`. Der Python-Client weigert sich, von einem HTTPS-Endpunkt zu unverschlüsseltem HTTP zu folgen, und sagt das auch: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Die clientseitige Notlƶsung ist, genau die URL zu konfigurieren, die der Server ausliefert (`https://mcp.example.com/mcp/`, SchrƤgstrich inklusive), sodass kein Redirect passiert. Die eigentliche Lƶsung ist das Flag oben. `FORWARDED_ALLOW_IPS` ist die Schreibweise als Umgebungsvariable; `*` vertraut jedem Hop, was nur richtig ist, wenn nichts außer dem Proxy uvicorn erreichen kann. + ## Worker – und wer sticky sein muss {#workers-and-who-has-to-be-sticky} Sobald der Hostname antwortet, stellst du mehr als einen Worker dahinter. Dafür gibt es keinen Schalter im SDK; du skalierst eine Starlette-App wie jede andere ASGI-App, indem du das Objekt an etwas übergibst, das forken kann: @@ -170,6 +186,7 @@ Ein `MCPServer` ist eine Protokollimplementierung, kein Anwendungsserver. Die De ## Zusammenfassung {#recap} * Ohne weitere Konfiguration beantwortet die App nur Requests an localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ist die Schranke zum Livegang: Bis du es übergibst, ist jeder Request hinter einem echten Hostnamen ein `421`, und der Grund steht nur im Log des Servers. +* Hinter einem TLS-terminierenden Proxy startest du uvicorn mit `--proxy-headers --forwarded-allow-ips=...`, sonst zeigen seine Redirects auf `http://`, und der Client lehnt sie ab. * Auf 2026-07-28 gibt es keine Session und nichts, woran ein Load Balancer sticky sein kƶnnte. `stateless_http=True` ist ein reiner Legacy-Schalter, weil ein moderner Request geroutet und beantwortet ist, bevor dieses Flag überhaupt gelesen wird. * Der Standardschlüssel für `requestState` ist `os.urandom(32)`, pro Prozess erzeugt. Ein Multi-Roundtrip-Retry, der bei einem anderen Worker landet, scheitert mit `-32602` *ā€žInvalid or expired requestStateā€œ*. * Die Lƶsung ist `RequestStateSecurity(keys=[...])` **und** derselbe Servername auf jeder Instanz. Der Name ist der Standard-Audience-Claim des Tokens. Dieselben Schlüssel, derselbe Name. diff --git a/i18n/de/pages/run/index.md b/i18n/de/pages/run/index.md index 54e65f7e..031fbdd5 100644 --- a/i18n/de/pages/run/index.md +++ b/i18n/de/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Den Server betreiben {#running-your-server} @@ -75,6 +75,11 @@ Jeder Transport hat eigene Keyword-Argumente, alle an `run()`: * `max_request_body_size`: größter akzeptierter Request-Body in Bytes. Standardwert 4 MiB; größere Requests erhalten HTTP 413, bevor geparst oder eine Session angelegt wird. Erhƶhe ihn nur, wenn legitime MCP-Nachrichten diese Größe überschreiten. +* `session_idle_timeout`: Sekunden, die eine Legacy-Session ohne laufenden Request verbringen darf, bevor der + Server sie schließt. Standardwert 1800. `None` schaltet den Timeout ab. Siehe + [Lebensdauer und Limits von Sessions](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: wie viele Legacy-Sessions ein Prozess gleichzeitig hƤlt. Standardwert 10 000. `None` + hebt das Limit auf. Im selben Abschnitt beschrieben. * `event_store`, `retry_interval`, `transport_security`: Wiederaufnahme und Schutz vor DNS-Rebinding. Sie kƶnnen warten, bis du anderswo als auf localhost bereitstellst; **[Bereitstellen und skalieren](deploy.md)** behandelt `transport_security`. !!! warning diff --git a/i18n/de/pages/run/legacy-clients.md b/i18n/de/pages/run/legacy-clients.md index 63fdbca6..e0606aee 100644 --- a/i18n/de/pages/run/legacy-clients.md +++ b/i18n/de/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Legacy-Clients unterstützen {#serving-legacy-clients} @@ -22,15 +22,25 @@ Ein Legacy-Client ist also nichts, *wofür* du etwas baust. Er ist etwas, das si ## Ein Handler, beide Generationen {#one-handler-both-eras} -Hier ist ein Tool, das die Person am Host etwas fragen muss, und Clients beider Generationen, die es aufrufen: +Hier ist ein Tool, das die Person am Host etwas fragen muss: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` braucht eine Sache, die das Modell nicht geliefert hat: wie viele Exemplare. Mit `Annotated[..., Resolve(ask_quantity)]` deklariert ein Tool genau das (alles Weitere steht in **[AbhƤngigkeiten](../handlers/dependencies.md)**). Nichts in `reserve` nennt eine Version, prüft eine Capability oder verzweigt. -Die beiden Clients sind **gleichzeitig** offen, am selben `mcp`-Objekt. `mode="legacy"` führt den `initialize`-Handshake aus: genau die Verbindung, die ein Client von vor 2026 ƶffnet. Der andere nimmt den Standardwert und landet bei `2026-07-28`. +Stelle es über HTTP bereit, und hier sind Clients beider Generationen, die es aufrufen: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Die beiden Clients sind **gleichzeitig** offen, gegen denselben laufenden Server. `mode="legacy"` führt den `initialize`-Handshake aus: genau die Verbindung, die ein Client von vor 2026 ƶffnet. Der andere nimmt den Standardwert und landet bei `2026-07-28`. Führe `python client.py` in einem zweiten Terminal aus: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -44,7 +54,7 @@ Es lohnt sich, beim *Wie* kurz innezuhalten, denn den beiden Clients wurde diese !!! tip Genau diese PortabilitƤt über Generationen hinweg ist der Grund, *warum* `Resolve` die API ist, auf die du bauen solltest. Sein Ƥlterer Verwandter `ctx.elicit()` - (**[Elicitation](../handlers/elicitation.md)**, die Rückfrage bei der Person am Host) sendet + (**[Elicitation](../handlers/elicitation.md)** – Rückfrage bei der Person am Host) sendet immer nur `elicitation/create` und funktioniert deshalb immer nur auf einer Legacy-Verbindung. Auf einer `2026-07-28`-Verbindung schlƤgt der Aufruf fehl. Wenn ein Tool es noch verwendet, ist die Lƶsung die, die du oben siehst, und kein Versionscheck. @@ -65,6 +75,41 @@ Auf einem Worker ist das unsichtbar. Auf zweien ist es das ganze Problem: Ein Re verbindet), kein Session-Store. Es macht eine Session nie von einem anderen Prozess aus erreichbar. +## Lebensdauer und Grenzen von Sessions {#session-lifetime-and-limits} + +Eine Legacy-Session lebt nicht ewig, und ein Prozess hƤlt nicht unbegrenzt viele davon. Zwei +Einstellungen steuern das. Beide sind Keyword-Argumente an `run()`, `streamable_http_app()` und +`Server.streamable_http_app()`. Moderne (`2026-07-28`-)Verbindungen und `stateless_http=True` +haben keine Sessions, also gilt keine der beiden Einstellungen für sie. + +| Einstellung | Standardwert | Was sie tut | Was der Client sieht | Abschalten mit | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Schließt eine Session, bei der so lange nichts in Bearbeitung war. | `404 Session not found`. Er muss erneut `initialize` senden. | `None` | +| `max_sessions` | `10_000` | Lehnt es ab, über diese Anzahl hinaus eine Session zu ƶffnen. Bestehende Sessions bleiben unberührt, und nichts wird verdrƤngt. | `503 Too many open sessions` mit JSON-RPC-Code `-32603`. | `None` | + +Was als ā€žin Bearbeitungā€œ zƤhlt: + +* Ein offener `GET`-Stream. Die SDK-Clients halten einen offen, also lƤuft die Session eines + verbundenen Clients nie ab. +* Ein Request, der noch beantwortet wird. Ein Tool-Aufruf, der lƤnger lƤuft als das Timeout, wird + nicht unterbrochen, und der Countdown beginnt erst, wenn er fertig ist. +* Sonst nichts. Zwischen Requests lƤuft die Uhr. Jeder Request auf der Session setzt sie zurück, + `ping` eingeschlossen. Ist eine Session einmal abgelaufen, belebt nichts sie wieder. + +Ein Client, der seine Session mit `DELETE` beendet, gibt sie sofort frei. Dasselbe gilt für einen +Client, dessen erƶffnender Request abgelehnt wurde. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Beide Ereignisse erscheinen im Server-Log. Ein Ablauf steht als `Session idle timeout` auf +`INFO`. Ein abgelehntes Ɩffnen als `Refusing to open a new session: sessions are already open` +auf `WARNING`. + +Die Limits gelten pro Prozess. Mit vier Workern liegt die Obergrenze beim Vierfachen von +`max_sessions`, und jeder Worker lƤsst seine eigenen Sessions ablaufen. + ## Die eine Stellschraube: `stateless_http` {#the-one-knob-stateless_http} Wenn Stickiness ein Preis ist, den du nicht zahlen willst, gibt es genau eine Sache, die du Ƥndern kannst. @@ -91,8 +136,8 @@ Zwei Dinge daran sind wichtiger als das, was es tut. !!! check Mach es absichtlich falsch. `reserve` ist genau das Tool, das eben beide Clients bedient hat. - Stelle es mit `stateless_http=True` bereit, verbinde dieselben zwei Clients über HTTP und rufe - es von jedem aus auf. + Stelle es mit `stateless_http=True` bereit, verbinde dieselben zwei Clients und rufe es von + jedem aus auf. Der moderne Client bekommt weiterhin `Reserved 2 of 'Dune'.` Der moderne Zweig hat sich nicht verƤndert. diff --git a/i18n/de/pages/troubleshooting.md b/i18n/de/pages/troubleshooting.md index 84c61bb8..dc98239c 100644 --- a/i18n/de/pages/troubleshooting.md +++ b/i18n/de/pages/troubleshooting.md @@ -1,18 +1,24 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Fehlerbehebung {#troubleshooting} Jede Überschrift auf dieser Seite ist der exakte Text eines Fehlers, den das SDK erzeugt, gefolgt davon, was er bedeutet, und der Lƶsung in einem Schritt. Suche die letzte Zeile deines Tracebacks (oder deines Server-Logs) hier mit der Seitensuche des Browsers und lies nur diesen Eintrag. -Mehrere EintrƤge laufen gegen diesen einen Server: ein Tool und eine Ressource mit Template, die beide bei einer Stadt, die sie nicht kennen, eine Exception auslƶsen: +Mehrere EintrƤge laufen gegen diesen einen Server. Ein Tool und eine Ressource mit Template, die beide bei einer Stadt, die sie nicht kennen, eine Exception auslƶsen: ```python title="server.py" --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Diese EintrƤge erreichen ihn unter `http://localhost:8000/mcp`, lass ihn also über HTTP laufen: + +```console +uv run mcp run server.py --transport streamable-http +``` + Die Fehler, die diese Seite zitiert, sind echt: Die Testsuite des SDK selbst reproduziert jeden einzelnen. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Das ist kein MCP-Fehler. Es ist Rauschen von anyio, und dein eigentlicher Fehler ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Damit machst du zwei Dinge: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Betritt den Kontextmanager. `__aenter__` ist die Verbindung: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Der Server erkennt die `Mcp-Session-Id`, die dein Client gesendet hat, nicht – fast immer, weil der Server **neu gestartet** wurde (oder du zu einer anderen Instanz geroutet wurdest). Sessions leben im Speicher dieses einen Prozesses. +Der Server erkennt die `Mcp-Session-Id`, die dein Client gesendet hat, nicht. Entweder wurde der Server **neu gestartet** (oder du wurdest zu einer anderen Instanz geroutet), oder die Session ist **abgelaufen**, weil für die Dauer von `session_idle_timeout` – standardmäßig 30 Minuten – nichts unterwegs war. Siehe [Lebensdauer und Limits von Sessions](run/legacy-clients.md#session-lifetime-and-limits). Sessions leben im Speicher dieses einen Prozesses. Es gibt keinen Server-Bug zu finden. Die HTTP-Response ist ein `404`, dessen Body JSON-RPC *ist*, deshalb zeigt dir der Python-`Client` – anders als beim `421` oben – diese hier wƶrtlich: @@ -261,9 +267,9 @@ Es gibt keinen Server-Bug zu finden. Die HTTP-Response ist ein `404`, dessen Bod Die Lƶsung ist, dich neu zu verbinden: Verlasse den `async with Client(...)`-Block und betritt einen neuen, der eine frische Session aushandelt. Für einen langlebigen Client heißt das, `MCPError` um deine Aufrufe herum abzufangen und bei dieser Meldung neu zu verbinden, statt es in einer toten Session erneut zu versuchen. -Passiert es *ohne* Neustart, betreibst du mehr als einen Worker ohne Sticky Sessions: Jeder Worker hƤlt seine eigene Session-Tabelle, sodass ein Request, der zum falschen geroutet wird, hier landet. **[Bereitstellen und skalieren](run/deploy.md)** und **[Legacy-Clients unterstützen](run/legacy-clients.md)** behandeln dieses Thema und seine beiden Lƶsungen (Sticky Routing oder `stateless_http=True`). +Passiert es *ohne* Neustart und ohne dass der Client so lange still war, betreibst du mehr als einen Worker ohne Sticky Sessions: Jeder Worker hƤlt seine eigene Session-Tabelle, sodass ein Request, der zum falschen geroutet wird, hier landet. **[Bereitstellen und skalieren](run/deploy.md)** und **[Legacy-Clients unterstützen](run/legacy-clients.md)** behandeln dieses Thema und seine beiden Lƶsungen (Sticky Routing oder `stateless_http=True`). -Für alle, die den Server betreiben, lautet die passende Log-Zeile `Rejected request with unknown or expired session ID: `. Sie wird auf `INFO` geloggt, ist also bei der üblichen `WARNING`-Schwelle unsichtbar. Sie direkt nach einem Deployment stoßweise zu sehen ist normal; jeder verbundene Client verbindet sich neu. +Für alle, die den Server betreiben, lautet die passende Log-Zeile `Rejected request with unknown or expired session ID: `. Sie wird auf `INFO` geloggt, ist also bei der üblichen `WARNING`-Schwelle unsichtbar. Sie direkt nach einem Deployment stoßweise zu sehen ist normal; jeder verbundene Client verbindet sich neu. Ist die Session stattdessen abgelaufen, steht vor dieser Zeile `Session idle timeout`, ebenfalls auf `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Eines erzeugt diesen Fehler **nicht**, obwohl es ein Request ist, den das modern Dein Server mƶchte die Person am Host etwas fragen, und dieser Client hat nie gesagt, dass man ihn fragen kann. -Ein Resolver für Elicitation (Rückfrage bei der Person am Host) lehnt von vornherein ab, wenn der verbundene Client keine Form-Elicitation deklariert hat, und `e.error.data` nennt genau, was fehlt: +Dieses Bistro fragt über einen Resolver zurück, bevor es bucht: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Betreibe es anstelle des Weather-Servers und rufe `book_table` von einem Client aus auf, der keinen `elicitation_callback` übergeben hat. Der Resolver lehnt von vornherein ab, weil der verbundene Client nie Form-Elicitation (Rückfrage bei der Person am Host) deklariert hat, und `e.error.data` nennt genau, was fehlt: ```json { @@ -289,7 +301,7 @@ Ein Resolver für Elicitation (Rückfrage bei der Person am Host) lehnt von vorn ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ Du siehst diese bei `ctx.elicit()` auf einer Legacy-Verbindung, und auf jeder be Dein Handler hat versucht, den Client mitten im Request zu erreichen, auf einer Verbindung, deren Aufruf keinen Rückkanal (back-channel) hat, der einen Request vom Server tragen kann. Drei Server-Konfigurationen bringen einen Aufruf in diese Lage. -**Eine `2026-07-28`-Verbindung: jeder Transport, immer.** Das moderne Protokoll kennt überhaupt keine vom Server initiierten Requests, deshalb weigert sich der Server, bevor irgendetwas gesendet wird. `ctx.elicit()` innerhalb eines Tools ist der klassische Weg, dem zu begegnen (schon beim allerersten In-Memory-Test, denn `Client(server)` handelt ungefragt `2026-07-28` aus), und `elicitation_callback=` zu übergeben Ƥndert nichts, weil nie ein Request beim Client ankommt, den er beantworten kƶnnte: +**Eine `2026-07-28`-Verbindung: jeder Transport, immer.** Das moderne Protokoll kennt überhaupt keine vom Server initiierten Requests, deshalb weigert sich der Server, bevor irgendetwas gesendet wird. `ctx.elicit()` innerhalb eines Tools ist der klassische Weg, dem zu begegnen, meist im allerersten In-Memory-**[Test](get-started/testing.md)** dieses Tools, denn `Client(mcp)` handelt ungefragt `2026-07-28` aus. `elicitation_callback=` zu übergeben Ƥndert nichts, weil nie ein Request beim Client ankommt, den er beantworten kƶnnte: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ Der Server konnte das `requestState`-Token, das dein Client zurückgespielt hat, ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -418,7 +430,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` im Server-Log ist das einzige Zeichen, dass zwei gleichnamige Tools zu einem zusammengefallen sind. * Ein 421, drei Schreibweisen: `Server returned an error response` (der Python-`Client`), `421 Misdirected Request` / `Invalid Host header` (alles andere), `Invalid Host header: ` (das Server-Log). Lƶsung: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> eine eingehƤngte App, deren Host-Lifespan nie `mcp.session_manager.run()` betreten hat. -* `Session not found` -> der Server wurde neu gestartet; verbinde dich neu. +* `Session not found` -> der Server wurde neu gestartet oder die Session ist abgelaufen (`session_idle_timeout`); verbinde dich neu. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` braucht einen Kanal vom Server zum Client: Eine `2026-07-28`-Verbindung hat nie einen, `stateless_http=True` nimmt den Legacy-Kanal weg, und `json_response=True` nimmt den Request-gebundenen weg. Verwende einen Resolver (ein Legacy-Client braucht außerdem einen Server, der den Kanal behƤlt). Sein Nachbar `Method not found` ist ein Request für eine Methode, die die Protokollrevision der anderen Seite nicht hat. * `Client did not declare the form elicitation capability ...` und `Elicitation not supported` -> dem Client fehlt `elicitation_callback=`. * `Invalid or expired requestState` sagt auf der Leitung nie, warum. Das Server-Log schon; `unknown key` heißt: Teile `RequestStateSecurity(keys=[...])` über alle Worker hinweg. diff --git a/i18n/de/pages/whats-new.md b/i18n/de/pages/whats-new.md index a45f5a59..44b57adf 100644 --- a/i18n/de/pages/whats-new.md +++ b/i18n/de/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Was ist neu in v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ Nicht alles, was ein Tool braucht, sollte vom Modell kommen. Neu in v2: Ein Tool v1 gab dir drei verschachtelte Schichten: einen Transport-Kontextmanager, der rohe Streams liefert, eine darum gewickelte `ClientSession` und ein von Hand aufgerufenes `await session.initialize()`. v2 hat ein einziges Objekt: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` nimmt ein Server-Objekt (im Speicher, ohne Transport: das ist der Testansatz), eine URL (Streamable HTTP), ein `StdioServerParameters` (ein stdio-Subprozess) oder einen beliebigen anderen Transport-Kontextmanager wie `sse_client(...)`. Das Betreten von `async with` verbindet und handelt die Protokollversion aus, welche Generation der Server auch spricht; `client.server_capabilities` und `client.protocol_version` sind danach einfach da, ebenso `client.server_info`, wenn der Server sich zu erkennen gibt (das ist jetzt `Implementation | None`, weil die IdentitƤt in der 2026er-Generation optional ist). Die Sampling- und Elicitation-Callbacks, die du in v1 registriert hast, funktionieren weiter (ihre Bodies sehen dieselbe Umbenennung der Attribute auf snake_case wie alles andere auf dieser Seite), sie beantworten jetzt außerdem die Requests-in-Results im 2026er-Stil (unten), und sie laufen nebenlƤufig statt nacheinander. `ClientSession` liegt für alle, die die Low-Level-OberflƤche wollen, weiterhin darunter, und `client.session` reicht sie dir; auch sie hat sich bewegt (sie lƤuft auf der neuen Dispatcher-Engine, und einige ihrer eigenen Signaturen haben sich geƤndert), lies also den **[Migrationsleitfaden](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, bevor du hinabsteigst. +`Client` nimmt eine URL (Streamable HTTP), ein `StdioServerParameters` (ein stdio-Subprozess), einen beliebigen anderen Transport-Kontextmanager wie `sse_client(...)` oder, in Tests, das Server-Objekt selbst (im Speicher, ohne Transport). Das Betreten von `async with` verbindet und handelt die Protokollversion aus, welche Generation der Server auch spricht; `client.server_capabilities` und `client.protocol_version` sind danach einfach da, ebenso `client.server_info`, wenn der Server sich zu erkennen gibt (das ist jetzt `Implementation | None`, weil die IdentitƤt in der 2026er-Generation optional ist). Die Sampling- und Elicitation-Callbacks, die du in v1 registriert hast, funktionieren weiter (ihre Bodies sehen dieselbe Umbenennung der Attribute auf snake_case wie alles andere auf dieser Seite), sie beantworten jetzt außerdem die Requests-in-Results im 2026er-Stil (unten), und sie laufen nebenlƤufig statt nacheinander. `ClientSession` liegt für alle, die die Low-Level-OberflƤche wollen, weiterhin darunter, und `client.session` reicht sie dir; auch sie hat sich bewegt (sie lƤuft auf der neuen Dispatcher-Engine, und einige ihrer eigenen Signaturen haben sich geƤndert), lies also den **[Migrationsleitfaden](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, bevor du hinabsteigst. **[Der Client](client/index.md)** stellt ihn vor, **[Client-Transporte](client/transports.md)** behandelt die vier Verbindungsformen, **[Client-Callbacks](client/callbacks.md)** die Callbacks selbst, und **[Testen](get-started/testing.md)** zeigt das In-Memory-Muster, das den Helfer `create_connected_server_and_client_session()` aus v1 ersetzt. @@ -113,7 +113,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentB Das Beispiel ist das Muster. Allgemeiner: Jeder Handler hat dieselbe Form, mit typisierten Params hinein und einem vollstƤndigen Result-Typ hinaus; die alte jsonschema-Prüfung der Tool-Argumente ist entfernt; eine Exception ist ein Protokollfehler, nie ein Tool-Result mit `is_error=True`; und die umgebende ContextVar `server.request_context` ist entfernt. Eigene Methoden mit Vendor-Namespace sind über `add_request_handler(method, params_type, handler)` vollwertig unterstützt; das validiert eingehende Params gegen dein Modell, bevor dein Handler lƤuft. Und eine `middleware`-Liste (bewusst als vorlƤufig markiert) umhüllt jede eingehende Nachricht und ersetzt die privaten `_handle_*`-Methoden, die früher überschrieben wurden. -Unter der Haube wurde die `BaseSession`-Empfangsschleife aus v1 durch eine Dispatcher-Engine ersetzt, die Client und Server sich jetzt teilen, und sie macht mehrere Dinge auf dieser Seite gleichzeitig wahr: Ein einziges `Server`-Objekt bedient beide Protokollgenerationen, `Client(server)` dispatcht im Prozess ohne JSON-RPC-Framing, und ein Client-Request, der in den Timeout lƤuft, bricht jetzt tatsƤchlich den serverseitigen Handler ab. +Unter der Haube wurde die `BaseSession`-Empfangsschleife aus v1 durch eine Dispatcher-Engine ersetzt, die Client und Server sich jetzt teilen, und sie macht mehrere Dinge auf dieser Seite gleichzeitig wahr: Ein einziges `Server`-Objekt bedient beide Protokollgenerationen, `Client(server)` verteilt Requests direkt im Prozess, ohne JSON-RPC-Framing, und ein Client-Request, der in den Timeout lƤuft, bricht jetzt tatsƤchlich den serverseitigen Handler ab. Die Seite dazu ist **[Der Low-Level-Server](advanced/low-level-server.md)**; der **[Migrationsleitfaden](migration.md#lowlevel-server-decorator-based-handlers-replaced-with-constructor-on_-params)** geht jeden entfernten Hook durch. Wenn du nie unter `MCPServer` hinabgestiegen bist, betrifft dich nichts davon. @@ -171,11 +171,15 @@ Jeder vom Server ausgehende Request ist bei 2026-07-28 entfernt: Push-Elicitatio Der Ersatz dreht den Aufruf um. Ein Tool, das etwas von der Person am Host braucht, *gibt* die Frage *zurück* (`InputRequiredResult`), der Client beantwortet sie mit denselben Callbacks, die er schon immer hatte, und der Aufruf wird mit angehƤngten Antworten erneut versucht. `Client` treibt diese Schleife für dich. Auf dem Server baust du das Result selten selbst, weil eine **[AbhƤngigkeit](handlers/dependencies.md)** das übernimmt: Annotiere einen Parameter mit `Resolve(ask_quantity)`, wobei `ask_quantity` eine gewƶhnliche Funktion ist, die du schreibst, und das SDK fragt über den Mechanismus, den die Verbindung unterstützt – ein Live-Elicitation-Request auf einer Legacy-Session oder ein Multi-Roundtrip bei 2026. Ein Tool-Body, beide Generationen: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Diese Datei ist das ganze Versprechen an einem Ort: ein Server, ein Tool mit `Resolve` dahinter, und ein Legacy-Client plus ein moderner Client, die beide ihre Antwort bekommen, im Speicher. **[Multi-Roundtrip-Requests](handlers/multi-round-trip.md)** erklƤrt den Mechanismus (einschließlich `request_state`, den das SDK für dich versiegelt und verifiziert); **[Elicitation](handlers/elicitation.md)** behandelt das Fragen. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Diese beiden Dateien sind das ganze Versprechen: ein Server, ein Tool mit `Resolve` dahinter, und ein Legacy-Client plus ein moderner Client, die beide ihre Antwort von demselben laufenden Server bekommen (**[Legacy-Clients unterstützen](run/legacy-clients.md)** geht sie durch). **[Multi-Roundtrip-Requests](handlers/multi-round-trip.md)** erklƤrt den Mechanismus (einschließlich `request_state`, den das SDK für dich versiegelt und verifiziert); **[Elicitation](handlers/elicitation.md)** behandelt das Fragen. !!! warning "Das ist die eine Stelle, an der ein portierter v1-Server sein Verhalten Ƥndert" Deine eigenen Tests treffen es zuerst: `Client(mcp)` handelt gegen deinen v2-Server standardmäßig diff --git a/i18n/es/pages/advanced/apps.md b/i18n/es/pages/advanced/apps.md index 682404a1..163ceb90 100644 --- a/i18n/es/pages/advanced/apps.md +++ b/i18n/es/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -25,7 +25,7 @@ y luego vuelve. ## Un reloj con cara visible {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -50,21 +50,42 @@ dentro de tu HTML. Te da `ontoolresult`, `callServerTool`, No todos los clientes muestran apps. La especificación es tajante sobre lo que eso significa para ti: -> Tools **MUST** return a meaningful `content` array even when UI is available. +> Las herramientas **DEBEN** devolver un array `content` significativo incluso cuando hay una UI disponible. El modelo lee `content`; el iframe es para humanos. Un host capaz de mostrar UI sigue entregando el resultado en texto al modelo, y un cliente solo de texto recibe *solo* eso. AsĆ­ que el patrón canónico es una herramienta, dos respuestas. Mira `get_time` de nuevo: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` `client_supports_apps(ctx)` es `True` solo cuando el cliente declaró la extensión `io.modelcontextprotocol/ui` **y** incluyó `text/html;profile=mcp-app` en su configuración `mimeTypes`. El campo es obligatorio, asĆ­ que un cliente que lo omite -no cuenta. Eso es exactamente lo que declara `main()` en el mismo archivo: la -mitad cliente de la negociación, y vuelve la respuesta enriquecida. +no cuenta. Esta es la mitad cliente de la negociación: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Volvió la respuesta enriquecida. Quita `extensions=[APPS_SUPPORT]` de la llamada a `Client` +y el mismo programa imprime `The time is 2026-06-26T12:00:00Z.` en su lugar, que es +todo lo que un cliente solo de texto llega a ver. !!! warning Nunca devuelvas un marcador de posición como `"[Rendered UI]"` como Ćŗnico contenido. Si el diff --git a/i18n/es/pages/advanced/extensions.md b/i18n/es/pages/advanced/extensions.md index 74a21630..095ed236 100644 --- a/i18n/es/pages/advanced/extensions.md +++ b/i18n/es/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensiones {#extensions} @@ -62,7 +62,7 @@ especificadas por el propio proyecto MCP. La extensión Ćŗtil mĆ”s pequeƱa es una herramienta y un mapa de ajustes: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -74,18 +74,25 @@ La extensión Ćŗtil mĆ”s pequeƱa es una herramienta y un mapa de ajustes: * La extensión nunca recibe el servidor. Declara sus aportaciones como datos; `MCPServer` las consume. No hay un `self.server` que mutar. -Y `main()` es la prueba, un cliente en memoria directamente contra `mcp`: +SĆ­rvela por HTTP, y un cliente es la prueba: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Cada `server.py` de esta pĆ”gina se sirve con ese comando, y cada `client.py` +se ejecuta a su lado con `python client.py` desde una segunda terminal. + ### Servir tus propios mĆ©todos {#serving-your-own-methods} Una extensión puede registrar **nuevos mĆ©todos de solicitud**: sus propios verbos, servidos junto a los de la especificación: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -112,10 +119,10 @@ tiempo de ejecución: ### El lado del cliente {#the-client-side} -El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades: +El cliente es su propio programa, y lleva las dos mitades de la historia del cliente: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` declara la extensión. Las @@ -127,6 +134,9 @@ El `main()` del mismo archivo es toda la historia del cliente, sus dos mitades: * Los mĆ©todos de proveedor bajan una capa hasta `client.session.send_request(...)`; `Client` solo incorpora mĆ©todos de primera clase para los verbos de la especificación. `send_request` acepta cualquier subclase de `Request`, asĆ­ que la solicitud de proveedor pasa tal cual. +* `SearchRequest` y los dos modelos que lleva son el contrato de la extensión en el canal, + asĆ­ que el cliente los declara por su cuenta. Una extensión publicada los distribuirĆ­a en + un paquete que ambos lados importan. ### Interceptar `tools/call` {#intercepting-toolscall} @@ -159,13 +169,20 @@ El hook envuelve `tools/call` y nada mĆ”s. Para lo que afecta a cada mensaje, us ## Usar una extensión de cliente {#using-a-client-extension} Una **extensión de cliente** es el mismo contrato desde el lado que consume: un paquete de -comportamiento del lado del cliente detrĆ”s de un Ćŗnico identificador. Pasa las instancias a -`Client(extensions=[...])` y llama a las herramientas con normalidad: +comportamiento del lado del cliente detrĆ”s de un Ćŗnico identificador. AquĆ­ el servidor responde a `buy` +con un recibo para canjear en lugar de la mercancĆ­a, y solo para un cliente que declaró la +extensión: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +En el cliente, pasa las instancias a `Client(extensions=[...])` y llama a las herramientas con normalidad: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` devuelve un `CallToolResult` normal, como cualquier otra llamada. Lo que cambió la extensión: el servidor ahora puede responder a `buy` con una **forma de resultado** `receipt` en lugar de un resultado final, y `Receipts` la termina (aquĆ­ canjeando el @@ -184,7 +201,7 @@ segĆŗn la capacidad, el cliente no hace nada, como en el cliente de bĆŗsqueda de ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## Escribir una extensión de cliente {#writing-a-client-extension} @@ -192,8 +209,8 @@ client = Client(mcp, extensions=[advertise("com.example/search")]) Crea una subclase de `ClientExtension` y sobrescribe solo lo que necesites. Tres tipos de aportación, cada uno con un valor por defecto: `settings()`, `claims()` y `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * El identificador sigue la misma gramĆ”tica que el del servidor y se valida cuando se @@ -231,14 +248,21 @@ forma reclamada que llega a un llamador del nivel de sesión lanza `UnexpectedCl Los mĆ©todos de solicitud propios de una extensión no necesitan registro del lado del cliente. Un tipo de solicitud de proveedor es una subclase de `mcp.types.Request` y pasa por `client.session.send_request`, -como en [Servir tus propios mĆ©todos](#serving-your-own-methods). Un aƱadido: cuando una -clave de los parĆ”metros debe viajar en el header `Mcp-Name` (las especificaciones de extensión, como -tasks, lo exigen para sus verbos), el tipo de solicitud declara `name_param`: +como en [Servir tus propios mĆ©todos](#serving-your-own-methods). Toma un servidor cuya +extensión sirve un Ćŗnico verbo sobre un trabajo con nombre: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +Un aƱadido en el cliente: cuando una clave de los parĆ”metros debe viajar en el header `Mcp-Name` +(las especificaciones de extensión, como tasks, lo exigen para sus verbos), el tipo de solicitud +declara `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + La sesión replica `params["jobId"]` en `Mcp-Name` en cada ruta de envĆ­o, y un valor ausente falla de forma visible en lugar de omitir en silencio un header obligatorio. diff --git a/i18n/es/pages/advanced/low-level-server.md b/i18n/es/pages/advanced/low-level-server.md index a9fe35e2..71286b66 100644 --- a/i18n/es/pages/advanced/low-level-server.md +++ b/i18n/es/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # El Server de bajo nivel {#the-low-level-server} @@ -36,18 +36,22 @@ Cambiaron tres cosas, y son toda la API de bajo nivel: ### PruĆ©balo {#try-it} -AquĆ­ no hay Inspector: `mcp dev` y `mcp run` solo aceptan un `MCPServer`. Al `Client` en memoria le da igual; acepta un `Server` de bajo nivel exactamente igual que acepta un `MCPServer`: +`mcp dev` y `mcp run` solo aceptan un `MCPServer`, asĆ­ que este lo sirves por tu cuenta. La Ćŗltima lĆ­nea de `server.py` construye una app ASGI comĆŗn a partir de Ć©l, y uvicorn la ejecuta: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Apunta el Inspector, o cualquier cliente, a `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ El mismo texto que produjo la versión con `@mcp.tool()`. Dos diferencias honest * `result.structured_content` es `None`. El servidor de alto nivel envuelve un `-> str` en `{"result": ...}` por ti; aquĆ­ nadie construye lo que no construiste. * `list_tools` devuelve el esquema que escribiste **tĆŗ**, carĆ”cter por carĆ”cter. La versión de alto nivel tenĆ­a `"title": "Query"` en cada propiedad y un `"title": "search_booksArguments"` en la raĆ­z: artefactos de Pydantic. AquĆ­ abajo, si se transmite, es porque lo pusiste ahĆ­. +En una prueba te ahorras uvicorn y el puerto: `Client(server)` acepta un `Server` de bajo nivel en el mismo proceso exactamente igual que acepta un `MCPServer`, y **[Pruebas](../get-started/testing.md)** es ese patrón. + ## Nada se comprueba por ti {#nothing-is-checked-for-you} `MCPServer` rechaza un argumento incorrecto antes de que tu función llegue a ejecutarse, validando la llamada contra el esquema que generó (**[Herramientas](../servers/tools.md)**). @@ -215,4 +221,4 @@ Cada uno de estos es una idea para la que ya tienes el vocabulario; cada uno tie * `add_request_handler(method, params_type, handler)` sirve cualquier mĆ©todo. `initialize` estĆ” reservado. * Las capacidades que anuncia un `Server` se derivan de los handlers que registraste. -`Client(server)` trató a ambos servidores de forma idĆ©ntica porque *son* el mismo protocolo, que es justamente la idea. La siguiente capa hacia abajo no es una clase: es **[Middleware](middleware.md)**. +El cliente trató a ambos servidores de forma idĆ©ntica porque *son* el mismo protocolo, que es justamente la idea. La siguiente capa hacia abajo no es una clase: es **[Middleware](middleware.md)**. diff --git a/i18n/es/pages/advanced/pagination.md b/i18n/es/pages/advanced/pagination.md index d1abdfc0..49a007d5 100644 --- a/i18n/es/pages/advanced/pagination.md +++ b/i18n/es/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Paginación {#pagination} @@ -31,9 +31,13 @@ La paginación es para el servidor cuya lista de recursos es en realidad una bas ### PruĆ©balo {#try-it} -`Client(server)` se conecta a un `Server` de bajo nivel en memoria exactamente igual que se conecta a un `MCPServer`. +`mcp run` solo acepta un `MCPServer`, asĆ­ que este lo sirves tĆŗ mismo. La Ćŗltima lĆ­nea de `server.py` construye una app ASGI normal a partir del `Server`, y uvicorn la ejecuta: -Llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Apunta cualquier cliente (**[El cliente](../client/index.md)**, o el Inspector) a `http://localhost:8000/mcp` y llama a `list_resources()` sin argumentos. Obtienes diez recursos, de `book-1` a `book-10`, y `next_cursor` es la cadena `"10"`. DevuĆ©lvelo con `list_resources(cursor="10")` y el primer recurso es `book-11`; el nuevo `next_cursor` es `"20"`. @@ -43,7 +47,7 @@ La dĆ©cima pĆ”gina vuelve con `next_cursor` en `None`. Listo. Cada mĆ©todo `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) acepta el argumento nombrado `cursor=`. Vaciar una lista paginada es un solo `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Cada mĆ©todo `list_*` de `Client` (`list_tools`, `list_resources`, `list_resourc * Extiende **antes** de mirar `next_cursor`: la Ćŗltima pĆ”gina tambiĆ©n tiene recursos. * `next_cursor is None` es la salida. Cualquier otra cosa vuelve directamente a `cursor=`, sin tocarla. -Ejecuta su `main()` e imprime `100 resources`: diez pĆ”ginas de diez, unidas por un bucle que nunca supo que habĆ­a diez pĆ”ginas. +Con uvicorn todavĆ­a sirviendo `server.py`, ejecuta `python client.py` en una segunda terminal. Imprime `100 resources`: diez pĆ”ginas de diez, unidas por un bucle que nunca supo que habĆ­a diez pĆ”ginas. Es el mismo bucle que **[El cliente](../client/index.md)** muestra para cada verbo `list_*`, y no cuesta nada frente a un servidor que no pagina: `next_cursor` es `None` en la primera respuesta y el bucle se ejecuta una vez. diff --git a/i18n/es/pages/client/caching.md b/i18n/es/pages/client/caching.md index e8d1613c..34174862 100644 --- a/i18n/es/pages/client/caching.md +++ b/i18n/es/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Sugerencias de cachĆ© {#caching-hints} @@ -31,7 +31,7 @@ Por defecto, cada resultado dice `ttlMs: 0, cacheScope: "private"`: caducado de En el `Server` de bajo nivel, los handlers construyen sus resultados a mano, y `ttl_ms` / `cache_scope` son simplemente campos de los modelos de resultado. Un handler que los define explĆ­citamente siempre gana al mapa del constructor, campo por campo: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -45,10 +45,24 @@ Una salvedad sobre las listas paginadas: el protocolo exige el **mismo `cacheSco En una sesión 2026-07-28, `Client` respeta las sugerencias por ti: tiene una cachĆ© de respuestas integrada, activada por defecto. Un resultado que llega con un `ttlMs` se almacena, y una llamada idĆ©ntica dentro de ese TTL se sirve desde la cachĆ© sin ida y vuelta. Un resultado que llega *sin* sugerencia no se guarda en cachĆ©: los resultados sin sugerencia reciben `CacheConfig.default_ttl_ms`, que es `0` por defecto (caducado de inmediato), asĆ­ que un servidor que no declara nada ve exactamente el mismo trĆ”fico llamada por llamada de siempre. -```python title="client.py" hl_lines="33 35 38" +Para verlo en acción, sirve el `server.py` de la sección anterior con uvicorn (su Ćŗltima lĆ­nea construye la app ASGI). El handler imprime una lĆ­nea cada vez que se ejecuta de verdad: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Ejecuta `python client.py` desde una segunda terminal. Imprime las sugerencias que llevaba el primer resultado: el `ttlMs` del handler junto al `cacheScope` del mapa: + +```text +1000 public +``` + +La terminal del servidor cuenta el resto: entre los logs de solicitudes de uvicorn, `tools/list served` aparece tres veces. + Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una entrada vigente y nunca llegó al servidor; adelantar el reloj (inyectado) mĆ”s allĆ” del TTL hizo que la tercera volviera a consultar; la cuarta dijo `cache_mode="refresh"`. Ese argumento nombrado existe en los cinco verbos con cachĆ© (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (el valor por defecto) sirve una entrada vigente si la hay, y almacena lo consultado si no. @@ -57,7 +71,7 @@ Cuatro llamadas, tres consultas al servidor. La segunda llamada encontró una en Hay una regla por encima de `"use"`: **las llamadas que llevan `meta` siempre llegan al servidor.** Una solicitud con `meta` definido (un token de progreso, campos de trazado) espera una solicitud real por el canal, asĆ­ que con `cache_mode="use"` se trata como `"refresh"`: se omite la lectura de la cachĆ©, y el resultado obtenido sigue reemplazando la entrada en cachĆ©. `"bypass"` y un `"refresh"` explĆ­cito se comportan como siempre. -Para desactivar la cachĆ© por completo, construye con `Client(server, cache=None)`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada. +Para desactivar la cachĆ© por completo, pasa `cache=None` al construir el `Client`: cada llamada vuelve a ser una ida y vuelta, y `cache_mode`, aunque se sigue aceptando, no hace nada. El alcance tambiĆ©n se respeta automĆ”ticamente: las entradas `"private"` se asocian a la *partición* de la cachĆ© (mĆ”s abajo), mientras que las `"public"` pueden optar por compartirse mĆ”s ampliamente. Y **las notificaciones ganan al TTL** para las entradas exactas que nombran: una notificación `list_changed` desaloja el listado en cachĆ© correspondiente, y `resources/updated` desaloja la lectura en cachĆ© almacenada exactamente bajo su URI, por muy vigentes que estuvieran. En una conexión 2026-07-28 esas notificaciones llegan por un stream `subscriptions/listen` que abres con `client.listen(...)`, y el desalojo se completa antes de que tu observador vea el evento; **[Suscripciones](subscriptions.md)** es esa pĆ”gina. diff --git a/i18n/es/pages/client/callbacks.md b/i18n/es/pages/client/callbacks.md index 2369552e..a221b324 100644 --- a/i18n/es/pages/client/callbacks.md +++ b/i18n/es/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Callbacks del cliente {#client-callbacks} @@ -63,7 +63,7 @@ Un `tools/call` tuyo, un `elicitation/create` de vuelta desde el servidor, respo `mode="legacy"` en la llamada a `Client(...)` hace trabajo real. Por defecto, `Client(...)` negocia la ruta moderna del protocolo, y esa ruta no tiene canal de retorno (back-channel) para las solicitudes del servidor al cliente: `ctx.elicit` falla antes de que tu callback llegue a ejecutarse. No lo decide el transporte; lo decide el protocolo - negociado, tanto en memoria como a travĆ©s de una URL. Fija `mode="legacy"` siempre que tu cliente tenga + negociado. Fija `mode="legacy"` siempre que tu cliente tenga que responder a una; todas las pruebas detrĆ”s de esta pĆ”gina lo hacen. **[Versiones del protocolo](../protocol-versions.md)** tiene todos los detalles. En una sesión 2026-07-28 el callback no estĆ” muerto, se alimenta de otra forma: cuando una herramienta devuelve un @@ -151,4 +151,4 @@ Dos mĆ”s. Ninguno declara nada. * `sampling_callback` y `list_roots_callback` funcionan igual pero atienden funcionalidades obsoletas; los servidores modernos usan solicitudes de varias idas y vueltas en su lugar. * `logging_callback` y `message_handler` reciben notificaciones. No declaran nada. -El primer argumento de `Client(...)` es un objeto de transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos. +El primer argumento de `Client(...)` elige el transporte. **[Transportes del cliente](transports.md)** cubre todos los tipos. diff --git a/i18n/es/pages/client/index.md b/i18n/es/pages/client/index.md index f2bb09a5..a61c7104 100644 --- a/i18n/es/pages/client/index.md +++ b/i18n/es/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # El cliente {#the-client} @@ -11,13 +11,23 @@ Es un solo objeto con un solo ciclo de vida: lo construyes, entras en `async wit ## Tu primer cliente {#your-first-client} -```python title="client.py" hl_lines="14-18" +Un cliente necesita un servidor con el que hablar. Este Bookshop es al que se conectan todos los fragmentos de esta pĆ”gina. GuĆ”rdalo como `server.py` y dĆ©jalo ejecutĆ”ndose por HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -El servidor del principio solo estĆ” ahĆ­ para que tengas algo a lo que conectarte. El cliente son las cinco lĆ­neas resaltadas. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` recibe **el propio objeto servidor**. Ese es el transporte en memoria: sin subproceso, sin puerto, sin HTTP. AsĆ­ se conectan todos los ejemplos de esta pĆ”gina y todas las pruebas que escribas. +Con eso queda disponible en `http://localhost:8000/mcp`. El cliente es un programa aparte. GuĆ”rdalo como `client.py` y ejecuta `python client.py` en una segunda terminal: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` recibe una **URL**, asĆ­ que se conecta por Streamable HTTP al servidor que acabas de iniciar. * `async with` es el **ciclo de vida**. Al entrar se conecta y negocia; al salir se desconecta. No hay un par `connect()` / `close()`, y un `Client` no se puede reutilizar una vez que termina el bloque. * Dentro del bloque, los datos de la conexión ya estĆ”n ahĆ­ como propiedades simples. @@ -25,10 +35,10 @@ El servidor del principio solo estĆ” ahĆ­ para que tengas algo a lo que conectar `Client` recibe un solo argumento posicional y resuelve el transporte a partir de su tipo: -* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**. -* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el camino de producción. -* Un `StdioServerParameters`: el comando que se lanza como **subproceso**, con el que se habla a travĆ©s de su stdin y su stdout. +* Una cadena con una URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, el transporte con el que despliegas. +* Un `StdioServerParameters`: el comando que se lanza como **subproceso** local, con el que se habla a travĆ©s de su stdin y su stdout. * Un **transporte**: cualquier cosa que puedas usar con `async with ... as (read, write)`, como `streamable_http_client(url, http_client=...)` envolviendo tu propio cliente HTTP. +* Una instancia de `MCPServer` (o del `Server` de bajo nivel): se conecta **en el mismo proceso**, sin subproceso y sin puerto. Ese caso es para las pruebas, y **[Pruebas](../get-started/testing.md)** se construye sobre Ć©l. Todo lo demĆ”s en esta pĆ”gina es idĆ©ntico en los cuatro casos. Los encabezados, los subprocesos, los timeouts y el protocolo `Transport` tienen su propia pĆ”gina: **[Transportes del cliente](transports.md)**. @@ -49,11 +59,11 @@ Nunca elegiste una versión del protocolo. Por defecto, el `Client` sondea el se ## Listar herramientas {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` devuelve un `ListToolsResult`; las herramientas estĆ”n en `.tools`. Cada una es la definición completa que un host le entregarĆ­a a un modelo: +`list_tools()` devuelve un `ListToolsResult`; las herramientas estĆ”n en `.tools`. Cada una es la definición completa que un host le entregarĆ­a a un modelo. Esta es la primera: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ y `tool.input_schema` es el JSON Schema que el servidor derivó de las anotacion Ese esquema es todo lo que una UI necesita para renderizar un formulario de argumentos, y todo lo que un modelo necesita para producir argumentos vĆ”lidos. +La segunda herramienta, `lookup_book`, se registró sin `title=`, asĆ­ que su `tool.title` es `None`. + !!! tip `title` es opcional, asĆ­ que una UI que muestra herramientas a una persona tiene que elegir: el `title` si lo hay, el `name` si no. `from mcp.shared.metadata_utils import get_display_name` hace exactamente eso, @@ -86,7 +98,7 @@ Ese esquema es todo lo que una UI necesita para renderizar un formulario de argu `call_tool(name, arguments)` ejecuta la herramienta y te devuelve un `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ Una herramienta que lanza una excepción **no** la lanza en tu cliente. Vuelve c Los verbos de recursos vienen en pares: dos formas de listar, una de leer. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ A un cliente tambiĆ©n se le puede avisar cuando cambia un recurso. En conexiones ## Prompts {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ Un host le entrega esos mensajes directamente al modelo. Esa es toda la funciona Un servidor con un handler de autocompletado puede autocompletar argumentos de prompts y de plantillas de recursos mientras el usuario escribe. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ La respuesta estĆ” en `result.completion.values`. Escribe `"p"` y el servidor de Cada mĆ©todo `list_*` acepta un argumento nombrado `cursor=` y cada resultado trae un `next_cursor`. Cuando `next_cursor` es `None`, ya lo tienes todo. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Este bucle es correcto contra cualquier servidor. `MCPServer` devuelve todo en una sola pĆ”gina, asĆ­ que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayorĆ­a del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, estĆ”n en **[Paginación](../advanced/pagination.md)**. +`list_all_tools` es correcta contra cualquier servidor. `MCPServer` devuelve todo en una sola pĆ”gina, asĆ­ que `next_cursor` es `None` y el bucle se ejecuta una vez; por eso la mayorĆ­a del código nunca lo escribe. Los servidores que realmente paginan, y las reglas que siguen los cursores, estĆ”n en **[Paginación](../advanced/pagination.md)**. ## En las pruebas {#in-tests} -`Client(mcp)`, sin proceso y sin puerto, ya es un banco de pruebas para tu servidor. +Cada `client.py` de esta pĆ”gina llegó a `server.py` por HTTP. En una prueba te saltas la red y le pasas a `Client` el propio objeto servidor: `from server import mcp` y luego `Client(mcp)`. Sin proceso, sin puerto, y todos los mĆ©todos anteriores funcionan igual. -Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en memoria, y **[Pruebas](../get-started/testing.md)** es la pĆ”gina que la explica y construye todo el patrón a su alrededor. +Hay una opción del constructor pensada para eso: `Client(mcp, raise_exceptions=True)`. Solo tiene efecto en conexiones en el mismo proceso, y **[Pruebas](../get-started/testing.md)** es la pĆ”gina que la explica y construye todo el patrón a su alrededor. ## Resumen {#recap} -* `Client(x)` se conecta en memoria a un objeto servidor, por Streamable HTTP a una cadena con una URL, y por cualquier otra cosa mediante un transporte. +* `Client(x)` se conecta por Streamable HTTP a una cadena con una URL, lanza un subproceso para un `StdioServerParameters`, entra directamente en un transporte y, en las pruebas, recibe el propio objeto servidor. * `async with` es todo el ciclo de vida. Dentro, `server_capabilities` y `protocol_version` ya estĆ”n rellenas; `server_info` e `instructions` tambiĆ©n, cuando el servidor las proporciona. * `list_tools()` te da el `name`, `title`, `description` e `input_schema` de cada herramienta. * `call_tool()` devuelve `content` para el modelo, `structured_content` para tu código, e `is_error`. Una herramienta que lanza una excepción es un resultado, no una excepción. diff --git a/i18n/es/pages/client/oauth-clients.md b/i18n/es/pages/client/oauth-clients.md index 9bccb9f8..6451495d 100644 --- a/i18n/es/pages/client/oauth-clients.md +++ b/i18n/es/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clientes OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Mira `main()`. El proveedor va en el **cliente httpx2**, el cliente httpx2 va en La primera vez que `Client` envĆ­a una solicitud, el servidor responde `401`. El proveedor toma el control: -1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua quĆ© servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor. +1. **Descubrimiento.** Lee la cabecera `WWW-Authenticate`, obtiene los Protected Resource Metadata del servidor desde `/.well-known/oauth-protected-resource`, averigua quĆ© servidor de autorización protege este recurso y obtiene los metadatos de *ese* servidor. (A un servidor mĆ”s antiguo que no publica metadatos del recurso se le piden, en su lugar, los metadatos del servidor de autorización en su propio origen.) En cualquier caso, los metadatos deben nombrar como `issuer` al servidor para el que se obtuvieron; cualquier otra cosa se rechaza. 2. **Registro.** ĀæNo hay nada en el almacenamiento? Te registra dinĆ”micamente con tu `OAuthClientMetadata` y guarda el resultado. 3. **Autorización.** Genera el par PKCE y un `state`, construye la URL de autorización, espera tu `redirect_handler` y luego espera tu `callback_handler` para obtener el código. 4. **Intercambio.** Cambia el código por un `OAuthToken`, lo guarda y repite tu solicitud original con `Authorization: Bearer ...`. DespuĆ©s de eso, se queda callado. Los tokens salen del almacenamiento, un token de acceso caducado se renueva con el token de actualización y solo cuando nada de eso funciona vuelve a ejecutar el flujo. +Una regla de transporte se aplica a todas estas solicitudes: igual que la solicitud MCP dentro de la que se ejecutan, siguen una redirección solo cuando permanece en el mismo origen y conserva el mĆ©todo (un 307/308 por una barra final, por ejemplo), y tratan cualquier otra redirección como si esa URL no respondiera. + No escribiste nada de eso. Quedan dos argumentos nombrados (`client_metadata_url` y `validate_resource_url`), y este archivo no necesita ninguno. `client_metadata_url` es el que vale la pena conocer; tiene su propia sección mĆ”s abajo. ### PruĆ©balo {#try-it} -La mayorĆ­a de los ejemplos de esta documentación puedes comprobarlos con un `Client(server)` en memoria. Este no: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor. +El `Client(server)` en memoria que usan tus pruebas no sirve aquĆ­: todo el sentido del flujo es un `401` HTTP, y no hay HTTP entre un cliente en memoria y su servidor. El repositorio incluye la versión real. `examples/servers/simple-auth/` ejecuta un servidor de autorización independiente y un servidor MCP protegido; `examples/clients/simple-auth-client/` es el cliente de esta pĆ”gina convertido en una pequeƱa CLI. Su README tiene los dos comandos: inicia los servidores, ejecuta el cliente contra ellos y verĆ”s pasar los cuatro pasos. @@ -110,13 +112,14 @@ Un trabajo nocturno, un paso de CI, otro servicio. No hay navegador ni nadie que `ClientCredentialsOAuthProvider` es el mismo `httpx2.Auth`, sin el humano: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` QuĆ© cambió: * Sin `OAuthClientMetadata`, sin handlers. Pasas `client_id` y `client_secret`; el proveedor construye un registro `client_credentials` mĆ­nimo en torno a ellos y se salta el registro dinĆ”mico por completo. +* `issuer` nombra el servidor de autorización que emitió esas credenciales; usa el valor `issuer` que devuelve su documento `/.well-known/oauth-authorization-server`. El descubrimiento sigue ejecutĆ”ndose como arriba, pero las solicitudes de token solo se construyen a partir de los metadatos de *ese* emisor; si el servidor MCP apunta a cualquier otro sitio, el flujo se detiene con un `OAuthFlowError`. Omitirlo estĆ” obsoleto y pasa a ser obligatorio en 3.0 (consulta **[Funcionalidades obsoletas](../deprecated.md#deprecated-sdk-helpers)**); hasta entonces, el proveedor emite un aviso y usa el servidor de autorización que encuentre el descubrimiento. * `scope` es una cadena separada por espacios, el formato que OAuth usa en lo que se transmite. * Todo lo que viene despuĆ©s es idĆ©ntico: el mismo `TokenStorage`, el mismo `httpx2.AsyncClient(auth=...)`, el mismo `streamable_http_client`. @@ -129,7 +132,7 @@ Por defecto, el secreto viaja como autenticación HTTP Basic en la solicitud de Hay un proveedor mĆ”s en `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, para clientes que se autentican con un JWT en lugar de un secreto compartido (`private_key_jwt`, la variante de par de claves e identidad de carga de trabajo). Sigue - el mismo patrón: construye uno y ponlo en `auth=`. El mismo módulo incluye + el mismo patrón: construye uno (acepta el mismo `issuer` opcional) y ponlo en `auth=`. El mismo módulo incluye `SignedJWTParameters` y `static_assertion_provider`, dos utilidades que construyen su aserción. Hay una situación mĆ”s sin humanos: el cliente pertenece a una empresa cuyo proveedor de identidad, y no el usuario, decide a quĆ© servidores MCP puede acceder. Ese es un grant distinto, con su propio modelo de confianza y su propia pĆ”gina, **[Aserción de identidad](identity-assertion.md)**. diff --git a/i18n/es/pages/client/session-groups.md b/i18n/es/pages/client/session-groups.md index 438e68c1..164f49f7 100644 --- a/i18n/es/pages/client/session-groups.md +++ b/i18n/es/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Grupos de sesiones {#session-groups} @@ -78,7 +78,7 @@ Si ya tienes una `ClientSession` conectada (`Client.session` lo es), pĆ”sala a ` ## Resumen {#recap} * `ClientSessionGroup` mantiene muchas conexiones a servidores y reĆŗne sus herramientas, recursos y prompts en un `dict` para cada tipo. -* `connect_to_server(params)` por servidor. Recibe parĆ”metros de transporte, nunca el objeto servidor ni la URL que recibe un `Client`. +* `connect_to_server(params)` por servidor. Recibe parĆ”metros de transporte, nunca la URL ni el `Transport` que recibe un `Client`. * `group.call_tool(name, arguments)` enruta por ti al servidor al que pertenece. * Los nombres deben ser Ćŗnicos en todo el grupo; dos servidores con una herramienta `search` no pueden coexistir por sĆ­ solos. * `component_name_hook=` reescribe cada nombre registrado. La clave del dict cambia; el nombre que se transmite por el canal, no. diff --git a/i18n/es/pages/client/transports.md b/i18n/es/pages/client/transports.md index 4f25dc11..84f91ff2 100644 --- a/i18n/es/pages/client/transports.md +++ b/i18n/es/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transportes del cliente {#client-transports} @@ -11,30 +11,15 @@ Nunca configuras uno por separado. `Client` recibe un Ćŗnico argumento posiciona El lado del *servidor* de cada uno (lo que hace `mcp.run()` y lo que despliegas) estĆ” en **[Ejecutar el servidor](../run/index.md)**. -## En memoria {#in-memory} - -Pasa el propio objeto del servidor: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Sin subproceso, sin puerto, sin bytes por ningĆŗn canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun asĆ­ la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP. - -Eso lo convierte en dos cosas a la vez: - -* **Un arnĆ©s de pruebas.** Todos los ejemplos de esta documentación se ejercitan asĆ­, y la pĆ”gina **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello. -* **Una API de integración.** Una aplicación que construye el servidor no necesita un salto de red para llamar a sus herramientas. - ## Streamable HTTP {#streamable-http} -Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte con el que despliegas: +Pasa una cadena con una URL y obtienes **Streamable HTTP**, el transporte detrĆ”s del que despliegas y el primero al que deberĆ­as recurrir: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: `follow_redirects=True`, un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta. +Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_http_client(...)` por ti, encima de un `httpx2.AsyncClient` configurado como MCP necesita: un timeout de 30 segundos para connect/write/pool y un timeout de lectura de 300 segundos, porque el servidor puede mantener abierto un flujo de respuesta. !!! check Un `Client` que has construido **no** estĆ” conectado. La construcción solo elige el transporte; @@ -50,7 +35,7 @@ Ese es todo el cliente de producción. `Client` envuelve la URL en `streamable_h En cuanto necesites un encabezado `Authorization`, una cookie, un proxy, mTLS o un timeout distinto, construye tĆŗ mismo el `httpx2.AsyncClient` y entrĆ©gaselo a `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ estĆ”ndar `SSL_CERT_FILE`/`SSL_CERT_DIR` o pasa un `verify=ssl_context` explĆ­ci !!! info `httpx2` conserva la API conocida de `httpx`, asĆ­ que si conoces `httpx` ya sabes cómo hacer la autenticación, los proxies, los event hooks, los reintentos y los lĆ­mites de conexión aquĆ­. El SDK no aƱade nada encima ni quita - nada. TambiĆ©n es donde se conecta OAuth: + nada, salvo el [manejo de redirecciones](#redirects). TambiĆ©n es donde se conecta OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Todo ese flujo estĆ” en **[Clientes OAuth](oauth-clients.md)**. +### Redirecciones {#redirects} + +El transporte se conecta a la URL que le diste, y solo a ese origen. + +* Una redirección `307`/`308` que se queda en el mismo esquema, host y puerto se sigue, y tambiĆ©n `http://` → `https://` en el mismo host. Eso cubre la redirección habitual de barra final `/mcp` → `/mcp/`. +* Una redirección a cualquier otro sitio **no** se sigue. La llamada falla con: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Si esa URL es el servidor que querĆ­as, ponla en tu configuración. Si no, el servidor o un proxy delante de Ć©l estĆ” mal configurado. + +Esto vale para cualquier `httpx2.AsyncClient` que pases: su ajuste `follow_redirects` no se consulta para las solicitudes MCP, en ningĆŗn sentido. Los proveedores OAuth del SDK aplican la misma regla a sus propias solicitudes. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` significa que el + servidor estĆ” detrĆ”s de un proxy que termina TLS del que no sabe nada y estĆ” emitiendo redirecciones `http://`. + Eso se arregla en el servidor (**[Desplegar y escalar](../run/deploy.md#behind-a-tls-terminating-proxy)**), + o usando la URL `https://…/` exacta que sugiere el mensaje. + ## stdio {#stdio} Un servidor **stdio** es un subproceso. El cliente lo lanza, escribe JSON-RPC en su stdin y lee JSON-RPC de su stdout. AsĆ­ es como un host de escritorio ejecuta un servidor en tu mĆ”quina: un host *es* este código mĆ”s una interfaz de usuario, y **[Conectar a un host real](../get-started/real-host.md)** es la misma relación vista desde el lado del host, como archivo de configuración. @@ -105,6 +111,18 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tĆŗ Un servidor que necesita una clave de API no la encontrarĆ” ahĆ­. PĆ”sala explĆ­citamente con `env=`; esas variables se fusionan encima de la lista de permitidos. Eso es lo que hace `BOOKSHOP_API_KEY` arriba. +## En memoria {#in-memory} + +En una prueba no hay nada que desplegar ni nada que lanzar. Pasa el propio objeto del servidor: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Sin subproceso, sin puerto, sin bytes por ningĆŗn canal. El cliente y el servidor son dos objetos en el mismo proceso, y aun asĆ­ la llamada pasa por la capa real del protocolo: `search_books` se lista, se valida y se invoca exactamente igual que por HTTP. **[Pruebas](../get-started/testing.md)** construye todo el patrón en torno a ello. + +La misma forma sirve ademĆ”s como API de integración: una aplicación que construye ella misma el servidor puede llamar a sus herramientas sin un salto de red. + ## SSE {#sse} `sse_client(url)`, de `mcp.client.sse`, es el transporte HTTP al que reemplazó Streamable HTTP. EnvuĆ©lvelo igual, `Client(sse_client("http://localhost:8000/sse"))`, para hablar con un servidor que todavĆ­a lo usa, y no construyas nada nuevo sobre Ć©l. @@ -113,15 +131,16 @@ El stderr del proceso hijo va al tuyo. Para enviarlo a otro sitio, construye tĆŗ Para `Client`, todo lo anterior es lo mismo. -Un **transporte** es cualquier gestor de contexto asĆ­ncrono que produce un par `(read, write)` de flujos de mensajes: formalmente, el protocolo `Transport` de `mcp.client`. `Client` resuelve su argumento por tipo: un objeto de servidor se conecta dentro del proceso, un `str` se convierte en `streamable_http_client(url)`, un `StdioServerParameters` se convierte en `stdio_client(params)` y cualquier otra cosa se entra directamente como transporte. Esa Ćŗltima regla es la razón por la que `stdio_client(...)`, `streamable_http_client(...)` y `sse_client(...)` encajan todos en el mismo hueco, y por la que puedes escribir el tuyo. +Un **transporte** es cualquier gestor de contexto asĆ­ncrono que produce un par `(read, write)` de flujos de mensajes: formalmente, el protocolo `Transport` de `mcp.client`. `Client` resuelve su argumento por tipo: un `str` se convierte en `streamable_http_client(url)`, un `StdioServerParameters` se convierte en `stdio_client(params)`, un objeto de servidor se conecta dentro del proceso y cualquier otra cosa se entra directamente como transporte. Esa Ćŗltima regla es la razón por la que `stdio_client(...)`, `streamable_http_client(...)` y `sse_client(...)` encajan todos en el mismo hueco, y por la que puedes escribir el tuyo. ## Resumen {#recap} -* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo para pruebas y para integración. * `Client("http://.../mcp")` (una URL) se conecta por Streamable HTTP, el transporte de producción. * Los encabezados, la autenticación, los proxies y los timeouts van en un `httpx2.AsyncClient` que pasas a `streamable_http_client(url, http_client=...)`. No existe el argumento nombrado `headers=`. +* Las redirecciones se siguen solo dentro del propio origen de la URL (un `307`/`308` de barra final), mĆ”s `http`→`https` en el mismo host. Cualquier otra cosa falla con `Redirect to … not followed`; configura la URL final. * stdio es `Client(StdioServerParameters(...))`. EnvuĆ©lvelo tĆŗ mismo en `stdio_client(...)` solo para redirigir el stderr del proceso hijo. * El subproceso recibe un entorno con lista de permitidos, no el tuyo; `env=` se aƱade a Ć©l. +* `Client(mcp)` (el objeto del servidor) se conecta en memoria. Úsalo en pruebas, o para integrar un servidor en la aplicación que lo construyó. * Un transporte es cualquier cosa con la que puedas hacer `async with x as (read, write)`. `Client` entrega directamente a ese protocolo todo lo que no sea un objeto de servidor, una URL ni un `StdioServerParameters`. * Construir un `Client` elige el transporte. `async with` lo abre. diff --git a/i18n/es/pages/deprecated.md b/i18n/es/pages/deprecated.md index 43beb87a..9e93dfe7 100644 --- a/i18n/es/pages/deprecated.md +++ b/i18n/es/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Funcionalidades obsoletas {#deprecated-features} -La especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**. Una función auxiliar del SDK queda obsoleta por su cuenta y aparece [al final](#deprecated-sdk-helpers). +La especificación 2026-07-28 retira cinco cosas. El SDK sigue implementando todas y cada una, y todas llevan ahora un **aviso de obsolescencia**. Algunas partes del SDK quedan obsoletas por su cuenta y aparecen [al final](#deprecated-sdk-helpers). La tabla siguiente nombra cada funcionalidad obsoleta, explica por quĆ© desaparece e indica el reemplazo sobre el que construir. @@ -137,11 +137,13 @@ Esa es toda la API. No hay un interruptor por mĆ©todo, y tampoco lo quieres: la ## Funciones auxiliares del SDK obsoletas {#deprecated-sdk-helpers} -No son cambios de la especificación, solo detalles internos del SDK con un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning` y se eliminarĆ”n en 3.0. +No son cambios de la especificación, solo usos del SDK que tienen un reemplazo mejor. Avisan con el mismo `MCPDeprecationWarning`, y 3.0 elimina la forma antigua. | Obsoleto | QuĆ© hacer en su lugar | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` y despuĆ©s `FuncMetadata.call_fn()`. Solo lo llamaba el código que maneja `FuncMetadata` directamente (una subclase propia de `Tool`, por ejemplo). | +| `AuthSettings(resource_server_url=...)` sin `validate_token_resource=` | DefĆ­nelo: `True` hace que el servidor rechace los tokens bearer que tu verificador no declara como emitidos para `resource_server_url`; `False` indica que tu verificador comprueba por sĆ­ mismo la audiencia del token (consulta **[Autorización](run/authorization.md#a-token-verifier)**). Sin definir se comporta como `False`; 3.0 hace que `True` sea el valor por defecto siempre que `resource_server_url` estĆ© definido. | +| `ClientCredentialsOAuthProvider(...)` o `PrivateKeyJWTOAuthProvider(...)` sin `issuer=` | Pasa `issuer=` con el servidor de autorización que emitió las credenciales (consulta **[Escribir clientes OAuth](client/oauth-clients.md#machine-to-machine)**). Sin Ć©l, el servidor MCP decide quĆ© servidor de autorización las recibe; 3.0 hace obligatorio el argumento nombrado. | ## Resumen {#recap} @@ -150,7 +152,7 @@ No son cambios de la especificación, solo detalles internos del SDK con un reem * Obsoleto es solo un aviso: no hay cambios en lo que se transmite, todo sigue funcionando contra sesiones anteriores a 2026 y recibes un `MCPDeprecationWarning` visible (un `UserWarning`, asĆ­ que estĆ” activo por defecto). * El muestreo y los roots necesitan ademĆ”s un canal de retorno que una sesión 2026-07-28 no tiene. En una conexión moderna avisan y despuĆ©s lanzan una excepción. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia toda la categorĆ­a; `"error::mcp.MCPDeprecationWarning"` en pytest la convierte en un fallo de prueba. -* Una función auxiliar del SDK, `FuncMetadata.call_fn_with_arg_validation()`, queda obsoleta por separado y se eliminarĆ” en 3.0. +* Las [partes del SDK obsoletas](#deprecated-sdk-helpers) siguen la misma regla: avisan ahora, y 3.0 elimina la forma antigua. * El código nuevo no deberĆ­a construirse sobre nada de esto. Todas las demĆ”s pĆ”ginas de esta documentación enseƱan la API actual. diff --git a/i18n/es/pages/get-started/first-steps.md b/i18n/es/pages/get-started/first-steps.md index 86fce406..a0f3b3e8 100644 --- a/i18n/es/pages/get-started/first-steps.md +++ b/i18n/es/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Primeros pasos {#first-steps} @@ -17,7 +17,7 @@ Tres palabras que verĆ”s en cada pĆ”gina a partir de aquĆ­: * Un **cliente** vive dentro del host y habla MCP. El host ejecuta un cliente por cada servidor al que estĆ” conectado. * Un **servidor** es lo que construyes con este SDK. Expone cosas a los clientes. Nunca habla directamente con el modelo. -TĆŗ escribes el servidor. Los hosts son el producto de otra persona. El SDK tambiĆ©n te da un `Client`. Lo usarĆ”s para probar tus servidores, y aparece mĆ”s adelante en esta pĆ”gina. +TĆŗ escribes el servidor. Los hosts son el producto de otra persona. El SDK tambiĆ©n te da un `Client`, la misma clase que usarĆ­a un host para llegar a un servidor por URL o lanzarlo como subproceso. Aparece mĆ”s adelante en esta pĆ”gina, y es tambiĆ©n la forma en que probarĆ”s tus servidores. ## Las tres primitivas {#the-three-primitives} @@ -83,22 +83,20 @@ Viste tres pestaƱas en el Inspector. ĀæCómo supo que habĆ­a tres? Cuando un cliente se conecta, el servidor declara sus **capacidades**: quĆ© familias de solicitudes va a responder. El cliente usa esa declaración para decidir quĆ© vale la pena pedir siquiera. Nunca la escribiste; `MCPServer` la declara por ti. -MĆ­ralo tĆŗ mismo. El `Client` del SDK acepta el objeto servidor directamente y se conecta a Ć©l **en memoria** (sin subproceso, sin puerto): +MĆ­ralo tĆŗ mismo. Deja `server.py` ejecutĆ”ndose sobre HTTP en una terminal: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +y apunta un cliente hacia Ć©l desde otra: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,17 +116,18 @@ Ese diccionario son las **capacidades** declaradas de tu servidor. Es lo primero FĆ­jate en lo que no estĆ”. `completions` (autocompletado de argumentos para plantillas de recurso y prompts) necesita un handler que escribes tĆŗ, este servidor no tiene uno, asĆ­ que la capacidad estĆ” ausente y un cliente bien hecho no la pedirĆ”. Esa es la regla para todo lo opcional: registra la cosa y la capacidad aparece; **[Autocompletado](../servers/completions.md)** lo demuestra. !!! info - `Client(mcp)` es el mismo cliente en memoria con el que se prueba cada ejemplo de esta - documentación, y es como probarĆ”s los tuyos. Tiene una pĆ”gina entera: **[Pruebas](testing.md)**. + Ese `client.py` es un cliente MCP completo, y **[El cliente](../client/index.md)** es su pĆ”gina. + En una prueba te saltas la terminal y el puerto y le pasas a `Client` el propio objeto servidor, + `Client(mcp)`. Eso tambiĆ©n tiene una pĆ”gina entera: **[Pruebas](testing.md)**. ## Lo que no escribiste {#what-you-did-not-write} Repasa esta pĆ”gina. Escribiste tres pequeƱas funciones de Python. **No** escribiste: * Un JSON Schema. `a: int, b: int` *es* el esquema de `add`. -* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: todos servidos por ti. +* Un handler de solicitudes. `tools/list`, `resources/read`, `prompts/get`: el SDK los atiende todos por ti. * Una declaración de capacidades. `MCPServer` la hizo por ti. -* Una lĆ­nea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `Client(mcp)`, y nunca lo viste. +* Una lĆ­nea de protocolo. La negociación de versión, el encuadre JSON-RPC, el intercambio de capacidades: todo ocurrió dentro de `mcp dev` y `client.py`, y nunca lo viste. Esa proporción es la razón de ser del SDK. @@ -139,6 +138,6 @@ Esa proporción es la razón de ser del SDK. * Un decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nombre, descripción y esquema salen de la función. * Una URI con un `{param}` crea una **plantilla** de recurso, que se lista aparte de los recursos concretos. * Las **capacidades** del servidor se declaran por ti, y un cliente solo pide lo que un servidor declara. -* `Client(mcp)` se conecta al objeto servidor en memoria: tu entorno de pruebas desde el primer dĆ­a. +* `Client("http://localhost:8000/mcp")` habla con tu servidor en ejecución. PĆ”sale en su lugar el objeto servidor, `Client(mcp)`, y es tu entorno de pruebas desde el primer dĆ­a. Lo siguiente es **[Conectar a un host real](real-host.md)**: este servidor dentro de Claude Desktop o un IDE, de verdad. DespuĆ©s, **[Pruebas](testing.md)**: una pĆ”gina, un cliente en memoria, y nunca mĆ”s adivinas si funciona. Tras eso, cada primitiva tiene su propia pĆ”gina, empezando por la que maneja el modelo: **[Herramientas](../servers/tools.md)**. diff --git a/i18n/es/pages/get-started/testing.md b/i18n/es/pages/get-started/testing.md index 2c11eaac..4d67310c 100644 --- a/i18n/es/pages/get-started/testing.md +++ b/i18n/es/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Pruebas {#testing} -El SDK de Python incluye una clase `Client` con un **transporte en memoria**: le pasas tu objeto servidor y se conecta a Ć©l directamente. +La clase `Client` del SDK, la misma que se conecta a una URL o lanza un subproceso, tambiĆ©n se conecta **en memoria**: le pasas tu objeto servidor y habla con Ć©l directamente. -Sin subproceso. Sin puerto. Sin transporte alguno. Es la misma idea que el `TestClient` de FastAPI. +Sin subproceso. Sin puerto. Nada que se transmita por ningĆŗn canal. Es la misma idea que el `TestClient` de FastAPI. ## Uso bĆ”sico {#basic-usage} @@ -97,11 +97,11 @@ remoto. En una prueba eso es exactamente lo que *no* quieres, y es lo que cambia DĆ©jalo activado en las pruebas. No tiene ningĆŗn sentido en código de producción. -## En proceso por defecto {#in-process-by-default} +## Neutral respecto a la generación por defecto {#era-neutral-by-default} !!! note `Client(mcp)` se conecta en proceso y es **neutral respecto a la generación** por defecto: sondea - el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba ejercita + el servidor y elige la ruta de protocolo adecuada. Fija `mode="legacy"` si tu prueba comprueba comportamientos especĆ­ficos de las conexiones heredadas (envĆ­o de muestreo (sampling) o elicitación (elicitation), `message_handler`), y quita `raise_exceptions=True` en ese caso: una conexión heredada nunca depura los errores en primer lugar, y el indicador relanza el fallo diff --git a/i18n/es/pages/handlers/multi-round-trip.md b/i18n/es/pages/handlers/multi-round-trip.md index 75555cb4..4c0e10c6 100644 --- a/i18n/es/pages/handlers/multi-round-trip.md +++ b/i18n/es/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # Solicitudes de varias idas y vueltas {#multi-round-trip-requests} @@ -164,7 +164,7 @@ El `Server` de bajo nivel es el nivel sin pilas incluidas: a diferencia de `MCPS ## Un resultado de 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `Client(server)` en memoria la negocia por ti; a travĆ©s del canal, `mode="auto"` la descubre. Tras conectar, `client.protocol_version` te dice quĆ© obtuviste. +`InputRequiredResult` solo existe en la versión del protocolo **2026-07-28**. El `mode="auto"` por defecto de `Client` la descubre en cualquier conexión. Tras conectar, `client.protocol_version` te dice quĆ© obtuviste. !!! warning Una sesión anterior a 2026 no tiene dónde poner un `InputRequiredResult`. Devuelve uno desde tu diff --git a/i18n/es/pages/handlers/progress.md b/i18n/es/pages/handlers/progress.md index 8f454ded..6e84b938 100644 --- a/i18n/es/pages/handlers/progress.md +++ b/i18n/es/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progreso {#progress} @@ -29,19 +29,17 @@ Tres argumentos, y tĆŗ decides quĆ© significan: El cliente lo activa **por llamada**, pasando `progress_callback=` a `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,28 +54,31 @@ anyio.run(main) El callback es una función `async` que recibe exactamente lo que reportó el servidor: `progress`, `total`, `message`. !!! info - `Client(mcp)` se conecta directamente al objeto servidor, en memoria; es el mismo cliente sobre el que se construye la pĆ”gina **[Pruebas](../get-started/testing.md)**. - `progress_callback` es el mismo parĆ”metro sea cual sea el transporte que use el `Client`; - los *tiempos* que vas a ver son los de la conexión en memoria. Ejecuta tu callback - de forma directa, asĆ­ que cada reporte llega antes de que `call_tool` devuelva. Con un transporte real, - las notificaciones compiten con el resultado, y un callback lento puede seguir ejecutĆ”ndose despuĆ©s de que `call_tool` - haya devuelto. + `progress_callback` es el mismo parĆ”metro le pases lo que le pases a `Client`: una URL como aquĆ­, un + `StdioServerParameters` o el objeto servidor en una prueba. Eso sĆ­, ten en cuenta los tiempos con un + transporte real. Cada notificación se entrega por su cuenta, al margen de la respuesta, asĆ­ que un callback + lento puede seguir ejecutĆ”ndose despuĆ©s de que `call_tool` haya devuelto. Solo la conexión de prueba en el + mismo proceso ejecuta el callback de forma directa y garantiza que cada reporte llegue antes. ### PruĆ©balo {#try-it} -Pon `client.py` junto a `server.py` y ejecĆŗtalo: +Sirve `server.py` por HTTP y luego ejecuta el cliente desde una segunda terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden, y ambas lĆ­neas se imprimieron **antes** de que `call_tool` devolviera. El progreso no va empaquetado en el resultado; se transmite mientras la herramienta sigue trabajando. +Cada `await ctx.report_progress(...)` en el servidor se convirtió en una llamada a `show` en el cliente, en orden. El progreso no va empaquetado en el resultado. Se transmite mientras la herramienta sigue trabajando. !!! warning `progress_callback` pertenece a la **llamada**, no al `Client`. No hay un argumento del constructor diff --git a/i18n/es/pages/protocol-versions.md b/i18n/es/pages/protocol-versions.md index 9d7211f1..a9f035a9 100644 --- a/i18n/es/pages/protocol-versions.md +++ b/i18n/es/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Versiones del protocolo {#protocol-versions} @@ -11,9 +11,17 @@ Los servidores publicados antes de 2026-07-28 abren cada conexión con el **hand Casi nunca tienes que preocuparte por esto, porque `Client` negocia por ti. Esta pĆ”gina trata del Ćŗnico argumento del constructor que lo controla, `mode=`, y de las tres ocasiones en que lo cambias. +Cada fragmento de esta pĆ”gina es un `client.py` que habla con el `server.py` de Bookshop de **[El cliente](client/index.md)**. Inicia ese servidor en una terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Luego ejecuta cada fragmento en una segunda terminal con `python client.py`. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ En cualquier caso terminas conectado, y `client.protocol_version` te dice cuĆ”l Esa es toda la funcionalidad. Un solo `Client`, servidores de cualquier generación, sin ramificaciones en tu código. !!! info - `MCPServer` responde a `server/discover` en todos los transportes (en memoria, stdio, Streamable - HTTP), asĆ­ que contra tu propio servidor `auto` siempre llega a `2026-07-28`. El mecanismo de - respaldo solo se activa contra un servidor real anterior a 2026, que es exactamente cuando quieres que lo haga. + `MCPServer` responde a `server/discover` en todos los transportes (Streamable HTTP, stdio y la + conexión en proceso que usan tus pruebas), asĆ­ que contra tu propio servidor `auto` siempre llega + a `2026-07-28`. El mecanismo de respaldo solo se activa contra un servidor real anterior a 2026, + que es exactamente cuando quieres que lo haga. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ En 2026-07-28 ya no existe. El servidor *devuelve* sus preguntas y tĆŗ repites l `mode` tambiĆ©n acepta una cadena con una versión moderna del protocolo. Hoy ese conjunto es exactamente `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ El sondeo es barato, pero sigue siendo una ida y vuelta que pagas en cada recone AsĆ­ que guĆ”rdala. Tras una conexión `auto`, `client.session.discover_result` contiene el `DiscoverResult` exacto que envió el servidor: sus `supported_versions`, sus `capabilities`, sus `instructions` y la identidad que el servidor grabó en el `_meta` del resultado. DevuĆ©lveselo como `prior_discover=` la próxima vez: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/es/pages/run/asgi.md b/i18n/es/pages/run/asgi.md index 5d936386..9133ebfa 100644 --- a/i18n/es/pages/run/asgi.md +++ b/i18n/es/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # AƱadir a una app existente {#add-to-an-existing-app} @@ -100,7 +100,7 @@ Ese `/mcp` final es `streamable_http_path`. Ponlo en `"/"` y el prefijo de monta --8<-- "docs_src/asgi/tutorial004.py" ``` -Ahora los clientes se conectan a `/notes`, no a `/notes/mcp`. +Ahora los clientes se conectan a `/notes/`, no a `/notes/mcp`. ## CORS para clientes de navegador {#cors-for-browser-clients} @@ -143,4 +143,4 @@ Un cliente basado en navegador necesita dos permisos de tu parte: **enviar** sus * Los clientes de navegador necesitan CORS: `allow_headers` para los encabezados de solicitud `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para la respuesta. * `@mcp.custom_route()` aƱade endpoints HTTP simples, sin autenticación, junto a `/mcp`. -Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a Ć©l con esa URL en lugar de con un objeto servidor. +Una vez que el servidor es accesible en una URL real, **[El cliente](../client/index.md)** se conecta a Ć©l con esa URL. diff --git a/i18n/es/pages/run/authorization.md b/i18n/es/pages/run/authorization.md index c39fcdce..432031a1 100644 --- a/i18n/es/pages/run/authorization.md +++ b/i18n/es/pages/run/authorization.md @@ -1,13 +1,13 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorización {#authorization} -Sobre Streamable HTTP, tu servidor MCP es un servicio web comĆŗn y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1. +Sobre Streamable HTTP tu servidor MCP es un servicio web comĆŗn y corriente, y lo proteges igual que proteges cualquier servicio web: con tokens bearer de OAuth 2.1. -En tĆ©rminos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es vĆ”lido. +En tĆ©rminos de OAuth, el servidor es un **servidor de recursos**. Nunca inicia la sesión de nadie y nunca emite un token. Hace una sola cosa: mirar el header `Authorization` de cada solicitud y decidir si el token que trae es bueno. Esta pĆ”gina es el lado del servidor. Un cliente que descubre tu servidor de autorización y obtiene el token estĆ” en **[Clientes OAuth](../client/oauth-clients.md)**. @@ -21,21 +21,25 @@ Ese es todo el triĆ”ngulo. Todo lo que hay en esta pĆ”gina es el punto del medio ## Un verificador de tokens {#a-token-verifier} -El SDK no opina sobre cómo debe ser un token vĆ”lido. Se lo dices tĆŗ, implementando **`TokenVerifier`**: +El SDK no tiene opinión sobre cómo es un token vĆ”lido. Se lo dices tĆŗ, implementando **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` -* `TokenVerifier` es un protocolo con un solo mĆ©todo asĆ­ncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es vĆ”lido, `None` si no lo es. No hay nada mĆ”s que implementar. -* Este busca el token en una tabla. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización. Ese código es tuyo; el SDK solo lo llama. -* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender ninguna solicitud. +* `TokenVerifier` es un protocolo con un Ćŗnico mĆ©todo asĆ­ncrono. `verify_token` recibe el token en bruto del header `Authorization` y devuelve un **`AccessToken`** si es vĆ”lido, `None` si no lo es. No hay nada mĆ”s que implementar. +* Este busca el token en una tabla; cada entrada registra el recurso para el que se emitió. Uno real verifica la firma de un JWT o llama al endpoint de introspección de tokens del servidor de autorización, e informa para quiĆ©n se emitió el token (su `aud`) en `AccessToken.resource`. Ese código es tuyo; el SDK solo lo llama. +* `token_verifier=` y `auth=` siempre van juntos. Pasa uno sin el otro y `MCPServer(...)` lanza un `ValueError` antes de atender una sola solicitud. `AuthSettings` es la cara pĆŗblica de tu servidor de recursos: * `issuer_url`: el servidor de autorización que emite tus tokens. -* `resource_server_url`: la URL pĆŗblica de este endpoint MCP. Indica *para quĆ©* recurso es un token y es donde vive el documento de descubrimiento. +* `resource_server_url`: la URL pĆŗblica de este endpoint MCP. Indica para *quĆ©* recurso es un token, y es donde vive el documento de descubrimiento. * `required_scopes`: todo token debe traerlos todos. +* `validate_token_resource`: rechaza cualquier token cuyo `AccessToken.resource` no sea `resource_server_url`. Dejarlo sin definir mientras `resource_server_url` estĆ” definido emite un aviso (`MCPDeprecationWarning`) y se comporta como `False`; la versión 3.0 hace que `True` sea el valor por defecto para los servidores de recursos. + * ActĆ­valo cuando tu servidor de autorización vincula los tokens al `resource` que pidió el cliente, que los clientes MCP siempre envĆ­an. MantĆ©n `resource_server_url` como la URL exacta a la que se conectan los clientes. + * DĆ©jalo desactivado cuando tu servidor de autorización usa sus propios identificadores de audiencia (un identificador de API de Auth0, un ID de aplicación de Entra) y comprueba `aud` en tu verificador, devolviendo `None` para un token que no sea para este servidor. + * Si `aud` es una lista, pon en `resource` la entrada que sea igual a `resource_server_url`. !!! tip `examples/servers/simple-auth/` en el repositorio del SDK tiene un `IntrospectionTokenVerifier` que llama @@ -43,7 +47,7 @@ El SDK no opina sobre cómo debe ser un token vĆ”lido. Se lo dices tĆŗ, implemen ## Lo que obtienes sobre HTTP {#what-you-get-over-http} -La autorización vive en los headers HTTP, asĆ­ que solo existe en los transportes HTTP. EjecĆŗtala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar el servidor](index.md)** tiene el resto. La app ahora tiene dos rutas: +La autorización vive en los headers HTTP, asĆ­ que solo existe en los transportes HTTP. EjecĆŗtala en el que despliegues: `mcp.run(transport="streamable-http")` la pone en `http://127.0.0.1:8000/mcp`, y **[Ejecutar tu servidor](index.md)** tiene el resto. La app ahora tiene dos rutas: ```text /mcp @@ -65,7 +69,7 @@ Haz un `GET` a esa ruta well-known y obtienes los **Protected Resource Metadata } ``` -Este documento es la forma en que un cliente que nunca ha oĆ­do hablar de tu servidor encuentra la entrada: lee `authorization_servers` y va ahĆ­ a buscar un token. No escribiste nada de Ć©l. +Este documento es la forma en que un cliente que nunca ha oĆ­do hablar de tu servidor encuentra el camino de entrada: lee `authorization_servers` y va allĆ­ a buscar un token. No escribiste nada de Ć©l. !!! check Llama a `/mcp` sin token (o con uno para el que tu verificador devolvió `None`) y la solicitud @@ -78,25 +82,25 @@ Este documento es la forma en que un cliente que nunca ha oĆ­do hablar de tu ser {"error": "invalid_token", "error_description": "Authentication required"} ``` - No se analizó nada ni se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es + No se analizó nada y no se ejecutó ninguna herramienta. Y ese puntero `resource_metadata` en `WWW-Authenticate` es lo que hace automĆ”tico el descubrimiento: 401 -> documento de metadatos -> servidor de autorización -> token -> reintento. !!! warning - Nada de esto protege a `stdio`. Una tuberĆ­a no tiene header `Authorization`, asĆ­ que ahĆ­ nunca se - consulta `token_verifier`. La frontera de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo - vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto servidor + Nada de esto protege `stdio`. Una tuberĆ­a no tiene header `Authorization`, asĆ­ que `token_verifier` nunca + se consulta ahĆ­. El lĆ­mite de seguridad de un servidor `stdio` es el proceso que lo lanzó. Lo mismo + vale para el `Client(mcp)` en memoria que usas en las pruebas: se conecta directamente al objeto del servidor y se salta la capa HTTP, autorización incluida. ## La identidad de quien llama {#the-callers-identity} Dentro de cualquier handler, **`get_access_token()`** es el `AccessToken` que tu verificador devolvió para la solicitud actual: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` * Funciona en herramientas, recursos y prompts, y no hay nada que pasar de un lado a otro: el middleware de autenticación lo guarda en una variable de contexto por solicitud. -* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas aƱadido. Ese es el punto de enganche para reglas por herramienta: lee los scopes y rechaza. +* Recibes el **mismo objeto que construyó tu verificador**: `client_id`, `scopes`, `subject`, `expires_at` y cualquier `claims` extra que hayas adjuntado. Ese es el gancho para reglas por herramienta: lee los scopes y rechaza. * Fuera de una solicitud HTTP autenticada devuelve `None`. En memoria y sobre `stdio` siempre es `None`. Llama a `whoami` con `Authorization: Bearer alice-token` y el modelo lee: @@ -109,14 +113,14 @@ alice (scopes: notes:read) El SDK te da la mitad del servidor de recursos: verificar, anunciar, rechazar. No te da una pĆ”gina de inicio de sesión, una pantalla de consentimiento ni un token. -Para ver a las tres partes en movimiento, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeƱo servidor de autorización y un servidor de recursos configurado exactamente como en esta pĆ”gina) y luego apunta `examples/clients/simple-auth-client/` hacia Ć©l para ver el recorrido completo de descubrimiento y token. +Para ver moverse a las tres partes, ejecuta `examples/servers/simple-auth/` del repositorio del SDK (un pequeƱo servidor de autorización y un servidor de recursos configurados exactamente como en esta pĆ”gina) y luego apunta `examples/clients/simple-auth-client/` hacia Ć©l para ver la coreografĆ­a completa de descubrimiento y token. !!! info Hay un segundo argumento del constructor, `auth_server_provider=`, que incrusta un servidor de autorización - completo dentro de tu servidor MCP. Es anterior a la separación AS/RS sobre la que se construye la especificación - de autorización de MCP. Los servidores nuevos no deberĆ­an recurrir a Ć©l. + completo dentro de tu servidor MCP. Es anterior a la separación AS/RS alrededor de la cual estĆ” construida + la especificación de autorización de MCP. Los servidores nuevos no deberĆ­an recurrir a Ć©l. -Un servidor de autorización tambiĆ©n puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario haga clic en una pantalla de consentimiento, y el SDK admite los dos lados de ese intercambio. El grant, y el cliente que lo presenta, estĆ”n en **[Aserción de identidad](../client/identity-assertion.md)**. +Un servidor de autorización tambiĆ©n puede aceptar la aserción firmada de un proveedor de identidad empresarial en lugar de que un usuario pase por una pantalla de consentimiento, y el SDK admite ambos lados de ese intercambio. El grant, y el cliente que lo presenta, estĆ” en **[Aserción de identidad](../client/identity-assertion.md)**. ## Resumen {#recap} @@ -124,7 +128,7 @@ Un servidor de autorización tambiĆ©n puede aceptar la aserción firmada de un p * `TokenVerifier` es toda la superficie de integración: un mĆ©todo asĆ­ncrono, entra un token, sale `AccessToken | None`. * `token_verifier=` y `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` siempre van juntos. * El SDK publica los Protected Resource Metadata de [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) en `/.well-known/oauth-protected-resource/...` y responde a las solicitudes no autenticadas con un 401 cuyo header `WWW-Authenticate` apunta a ellos. Ese es todo el mecanismo de descubrimiento. -* `get_access_token()` en cualquier handler te dice quiĆ©n llama. -* La autorización es un asunto de HTTP. `stdio` y el cliente en memoria nunca la ven. +* `get_access_token()` en cualquier handler es quiĆ©n estĆ” llamando. +* La autorización es asunto de HTTP. `stdio` y el cliente de prueba en memoria nunca la ven. La mitad del cliente (descubrir tu servidor de autorización y obtener el token por ti) estĆ” en **[Clientes OAuth](../client/oauth-clients.md)**. Y un cliente que *afirma* una identidad en lugar de pedĆ­rsela a un usuario estĆ” en **[Aserción de identidad](../client/identity-assertion.md)**. diff --git a/i18n/es/pages/run/deploy.md b/i18n/es/pages/run/deploy.md index 3ff88dc1..53cd02cb 100644 --- a/i18n/es/pages/run/deploy.md +++ b/i18n/es/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Desplegar y escalar {#deploy-scale} @@ -47,6 +47,22 @@ Desplegada detrĆ”s de un nombre de host real, esa misma configuración por defec desplegado que rechaza todas las conexiones es una lista de hosts permitidos hasta que se demuestre lo contrario. **[Solución de problemas](../troubleshooting.md)** tambiĆ©n empieza por aquĆ­. +## DetrĆ”s de un proxy que termina TLS {#behind-a-tls-terminating-proxy} + +Si TLS termina en un proxy (un ingress, un balanceador de carga, Caddy, nginx) y uvicorn sirve HTTP plano detrĆ”s de Ć©l, dile a uvicorn que confĆ­e en los encabezados `X-Forwarded-*` del proxy: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sin eso, la app cree que se estĆ” sirviendo por `http://`, y cualquier redirección que emita (la habitual es `/mcp` → `/mcp/`) apunta a `http://…`. El cliente de Python se niega a seguir un endpoint HTTPS hacia HTTP plano y lo dice: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +El parche del lado del cliente es configurar la URL exacta que sirve el servidor (`https://mcp.example.com/mcp/`, barra incluida) para que no haya ninguna redirección. La solución es el flag de arriba. `FORWARDED_ALLOW_IPS` es la forma en variable de entorno; `*` confĆ­a en todos los saltos, lo que solo es correcto cuando nada salvo el proxy puede llegar a uvicorn. + ## Workers, y quiĆ©n tiene que ser sticky {#workers-and-who-has-to-be-sticky} Una vez que el nombre de host responde, pon mĆ”s de un worker detrĆ”s. No hay ningĆŗn ajuste del SDK para eso; una app Starlette se escala como cualquier app ASGI, entregando el objeto a algo que sepa hacer fork: @@ -170,6 +186,7 @@ Un `MCPServer` es una implementación del protocolo, no un servidor de aplicacio ## Resumen {#recap} * Por defecto, la app responde solo a las solicitudes dirigidas a localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` es la puerta de salida a producción: hasta que lo pases, cada solicitud detrĆ”s de un nombre de host real es un `421` y el motivo solo estĆ” en el log del servidor. +* DetrĆ”s de un proxy que termina TLS, ejecuta uvicorn con `--proxy-headers --forwarded-allow-ips=...`, o sus redirecciones apuntarĆ”n a `http://` y el cliente las rechazarĆ”. * En 2026-07-28 no hay sesión ni nada sobre lo que un balanceador de carga pueda ser sticky. `stateless_http=True` es un ajuste solo para lo heredado porque una solicitud moderna se enruta y se responde antes de que ese flag llegue a leerse. * La clave por defecto de `requestState` es `os.urandom(32)`, acuƱada por proceso. Un reintento de varias idas y vueltas que llega a otro worker falla con `-32602` *"Invalid or expired requestState"*. * La solución es `RequestStateSecurity(keys=[...])` **y** el mismo nombre de servidor en cada instancia. El nombre es el claim de audiencia por defecto del token. Las mismas claves, el mismo nombre. diff --git a/i18n/es/pages/run/index.md b/i18n/es/pages/run/index.md index a8964b70..8dd948d0 100644 --- a/i18n/es/pages/run/index.md +++ b/i18n/es/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Ejecutar el servidor {#running-your-server} @@ -75,6 +75,11 @@ Cada transporte tiene sus propios argumentos nombrados, todos en `run()`: * `max_request_body_size`: el cuerpo de solicitud mĆ”s grande que se acepta, en bytes. Es 4 MiB por defecto; las solicitudes mayores reciben HTTP 413 antes del anĆ”lisis o de la creación de la sesión. SĆŗbelo solo cuando los mensajes MCP legĆ­timos superen ese tamaƱo. +* `session_idle_timeout`: segundos que una sesión heredada puede pasar sin nada en curso antes de que el + servidor la cierre. Por defecto 1800. `None` lo desactiva. Consulta + [Duración de la sesión y lĆ­mites](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: cuĆ”ntas sesiones heredadas mantiene un proceso a la vez. Por defecto 10 000. `None` + quita el lĆ­mite. Se trata en la misma sección. * `event_store`, `retry_interval`, `transport_security`: reanudabilidad y protección contra DNS rebinding. Pueden esperar hasta que despliegues en algĆŗn lugar que no sea localhost; **[Desplegar y escalar](deploy.md)** cubre `transport_security`. !!! warning diff --git a/i18n/es/pages/run/legacy-clients.md b/i18n/es/pages/run/legacy-clients.md index 8b1e2bf2..f7a1414c 100644 --- a/i18n/es/pages/run/legacy-clients.md +++ b/i18n/es/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Atender clientes heredados {#serving-legacy-clients} @@ -21,15 +21,25 @@ AsĆ­ que un cliente heredado (legacy) no es algo *para* lo que construyes. Es al ## Un handler, ambas generaciones {#one-handler-both-eras} -AquĆ­ tienes una herramienta que necesita preguntarle algo al usuario, y clientes de ambas generaciones que la llaman: +AquĆ­ tienes una herramienta que necesita preguntarle algo al usuario: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` necesita una cosa que el modelo no proporcionó: cuĆ”ntos ejemplares. `Annotated[..., Resolve(ask_quantity)]` es la forma en que una herramienta lo declara (**[Dependencias](../handlers/dependencies.md)** tiene todos los detalles). Nada en `reserve` nombra una versión, comprueba una capacidad ni se bifurca. -Los dos clientes estĆ”n abiertos **al mismo tiempo**, sobre el mismo objeto `mcp`. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`. +SĆ­rvela por HTTP, y aquĆ­ tienes clientes de ambas generaciones que la llaman: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Los dos clientes estĆ”n abiertos **al mismo tiempo**, contra el mismo servidor en ejecución. `mode="legacy"` ejecuta el handshake `initialize`: exactamente la conexión que abre un cliente anterior a 2026. El otro toma el valor por defecto y queda en `2026-07-28`. Ejecuta `python client.py` desde una segunda terminal: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,41 @@ Con un solo worker eso no se nota. Con dos, es todo el problema: una solicitud q perdidos a un cliente que se reconecta a la *misma* sesión), no un almacĆ©n de sesiones. Nunca hace que una sesión sea alcanzable desde otro proceso. +## Duración y lĆ­mites de la sesión {#session-lifetime-and-limits} + +Una sesión heredada no vive para siempre, y un proceso no mantiene un nĆŗmero ilimitado de +ellas. Dos ajustes controlan esto. Ambos son argumentos nombrados de `run()`, `streamable_http_app()` +y `Server.streamable_http_app()`. Las conexiones modernas (`2026-07-28`) y `stateless_http=True` +no tienen sesiones, asĆ­ que ninguno de los dos ajustes se les aplica. + +| Ajuste | Por defecto | QuĆ© hace | QuĆ© ve el cliente | Desactivarlo | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Cierra una sesión que no ha tenido nada en curso durante ese tiempo. | `404 Session not found`. Tiene que volver a hacer `initialize`. | `None` | +| `max_sessions` | `10_000` | Se niega a abrir una sesión mĆ”s allĆ” de esa cantidad. Las sesiones existentes no se tocan y no se expulsa ninguna. | `503 Too many open sessions` con el código JSON-RPC `-32603`. | `None` | + +QuĆ© cuenta como "en curso": + +* Un stream `GET` abierto. Los clientes del SDK mantienen uno abierto, asĆ­ que la sesión de un + cliente conectado nunca expira. +* Una solicitud que todavĆ­a se estĆ” respondiendo. Una llamada a herramienta que dura mĆ”s que el + timeout no se interrumpe, y el plazo solo empieza a contar cuando termina. +* Nada mĆ”s. Entre solicitudes el reloj corre. Cualquier solicitud en la sesión lo reinicia, + `ping` incluido. Una vez que una sesión ha expirado, nada la revive. + +Un cliente que termina su sesión con `DELETE` la libera de inmediato. TambiĆ©n uno cuya +solicitud de apertura fue rechazada. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Ambos eventos aparecen en el log del servidor. Una expiración es `Session idle timeout` en +nivel `INFO`. Una apertura rechazada es `Refusing to open a new session: sessions are already open` +en nivel `WARNING`. + +Los lĆ­mites son por proceso. Con cuatro workers el techo es cuatro veces `max_sessions`, y cada +worker expira sus propias sesiones. + ## El Ćŗnico ajuste: `stateless_http` {#the-one-knob-stateless_http} Si la afinidad es un costo que te niegas a pagar, hay exactamente una cosa que puedes cambiar. @@ -87,7 +132,7 @@ Dos cosas sobre Ć©l importan mĆ”s que lo que hace. !!! check Haz lo incorrecto. `reserve` es exactamente la herramienta que acaba de atender a ambos clientes. - DespliĆ©gala con `stateless_http=True`, conecta los mismos dos clientes por HTTP y llĆ”mala desde + DespliĆ©gala con `stateless_http=True`, conecta los mismos dos clientes y llĆ”mala desde cada uno. El cliente moderno sigue recibiendo `Reserved 2 of 'Dune'.` El tramo moderno no cambió. diff --git a/i18n/es/pages/troubleshooting.md b/i18n/es/pages/troubleshooting.md index 3779ff72..04bf6378 100644 --- a/i18n/es/pages/troubleshooting.md +++ b/i18n/es/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Solución de problemas {#troubleshooting} @@ -13,6 +13,12 @@ Varias entradas se ejecutan contra este mismo servidor. Una herramienta y un rec --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Esas entradas se conectan a Ć©l en `http://localhost:8000/mcp`, asĆ­ que dĆ©jalo ejecutĆ”ndose por HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Los errores que cita esta pĆ”gina son reales: la propia suite de pruebas del SDK reproduce cada uno de ellos. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Esto no es un error de MCP. Es ruido de anyio, y tu error real es la **Ćŗltima l ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Dos cosas que hacer con eso: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entra en Ć©l. `__aenter__` es la conexión: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -El servidor no reconoce el `Mcp-Session-Id` que envió tu cliente, casi siempre porque el servidor **se reinició** (o te enrutaron a otra instancia). Las sesiones viven en la memoria de ese Ćŗnico proceso. +El servidor no reconoce el `Mcp-Session-Id` que envió tu cliente. O bien el servidor **se reinició** (o te enrutaron a otra instancia), o bien la sesión **expiró** porque no hubo nada en curso durante `session_idle_timeout`, que es de 30 minutos por defecto. Consulta [Duración y lĆ­mites de la sesión](run/legacy-clients.md#session-lifetime-and-limits). Las sesiones viven en la memoria de ese Ćŗnico proceso. No hay ningĆŗn bug del servidor que encontrar. La respuesta HTTP es un `404` cuyo cuerpo *sĆ­* es JSON-RPC, asĆ­ que, a diferencia del `421` de arriba, el `Client` de python te muestra este tal cual: @@ -261,9 +267,9 @@ No hay ningĆŗn bug del servidor que encontrar. La respuesta HTTP es un `404` cuy La solución es reconectar: sal del bloque `async with Client(...)` y entra en uno nuevo, que negocia una sesión nueva. Para un cliente de larga duración, eso significa capturar `MCPError` alrededor de tus llamadas y reconectar ante este mensaje en lugar de reintentar dentro de una sesión muerta. -Si ocurre *sin* un reinicio, estĆ”s ejecutando mĆ”s de un worker sin sticky sessions: cada worker mantiene su propia tabla de sesiones, asĆ­ que una solicitud enrutada al equivocado acaba aquĆ­. **[Desplegar y escalar](run/deploy.md)** y **[Atender clientes heredados](run/legacy-clients.md)** tienen todos los detalles y las dos soluciones (enrutamiento sticky o `stateless_http=True`). +Si ocurre *sin* un reinicio y sin que el cliente haya estado en silencio tanto tiempo, estĆ”s ejecutando mĆ”s de un worker sin sticky sessions: cada worker mantiene su propia tabla de sesiones, asĆ­ que una solicitud enrutada al equivocado acaba aquĆ­. **[Desplegar y escalar](run/deploy.md)** y **[Atender clientes heredados](run/legacy-clients.md)** tienen todos los detalles y las dos soluciones (enrutamiento sticky o `stateless_http=True`). -Para quien opera el servidor, la lĆ­nea de log correspondiente es `Rejected request with unknown or expired session ID: `. Se registra con nivel `INFO`, asĆ­ que es invisible con el umbral habitual de `WARNING`. Verla en rĆ”fagas justo despuĆ©s de un despliegue es normal; todos los clientes conectados estĆ”n reconectando. +Para quien opera el servidor, la lĆ­nea de log correspondiente es `Rejected request with unknown or expired session ID: `. Se registra con nivel `INFO`, asĆ­ que es invisible con el umbral habitual de `WARNING`. Verla en rĆ”fagas justo despuĆ©s de un despliegue es normal; todos los clientes conectados estĆ”n reconectando. Cuando lo que ocurrió es que la sesión expiró, a esa lĆ­nea la precede `Session idle timeout`, tambiĆ©n con nivel `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Hay una cosa que **no** produce este error, aunque es una solicitud que el proto Tu servidor quiere preguntarle algo al usuario, y este cliente nunca dijo que se le pudiera preguntar. -Un resolutor de elicitación (elicitation) se niega de entrada cuando el cliente conectado no declaró la elicitación por formulario, y `e.error.data` nombra exactamente lo que falta: +Este Bistro pregunta antes de reservar, mediante un resolutor: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +SĆ­rvelo en lugar del servidor Weather y llama a `book_table` desde un cliente que no pasó ningĆŗn `elicitation_callback`. El resolutor se niega de entrada, porque el cliente conectado nunca declaró la elicitación (elicitation) por formulario, y `e.error.data` nombra exactamente lo que falta: ```json { @@ -289,7 +301,7 @@ Pasa `elicitation_callback=` a `Client(...)`. Registrar el callback *es* la decl ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ Este lo ves desde `ctx.elicit()` en una conexión heredada, y en cualquier conex Tu handler intentó contactar con el cliente a mitad de solicitud, en una conexión cuya llamada no tiene ningĆŗn canal capaz de llevar una solicitud desde el servidor. Hay tres configuraciones de servidor que ponen una llamada en esa situación. -**Una conexión `2026-07-28`: cualquier transporte, siempre.** El protocolo moderno no tiene solicitudes iniciadas por el servidor en absoluto, asĆ­ que el servidor se niega antes de enviar nada. `ctx.elicit()` dentro de una herramienta es la forma clĆ”sica de toparse con esto (en la primera prueba en memoria, ya que `Client(server)` negocia `2026-07-28` sin que se lo pidas), y pasar `elicitation_callback=` no cambia nada, porque ninguna solicitud llega nunca al cliente para que la responda: +**Una conexión `2026-07-28`: cualquier transporte, siempre.** El protocolo moderno no tiene solicitudes iniciadas por el servidor en absoluto, asĆ­ que el servidor se niega antes de enviar nada. `ctx.elicit()` dentro de una herramienta es la forma clĆ”sica de toparse con esto, normalmente en la primerĆ­sima **[prueba](get-started/testing.md)** en memoria de esa herramienta, ya que `Client(mcp)` negocia `2026-07-28` sin que se lo pidas. Pasar `elicitation_callback=` no cambia nada, porque ninguna solicitud llega nunca al cliente para que la responda: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ El servidor no pudo verificar el token `requestState` que tu cliente devolvió c ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` en el log del servidor es la Ćŗnica seƱal de que dos herramientas con el mismo nombre se fundieron en una. * Un 421, tres formas de escribirlo: `Server returned an error response` (el `Client` de python), `421 Misdirected Request` / `Invalid Host header` (todo lo demĆ”s), `Invalid Host header: ` (el log del servidor). Solución: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> una app montada cuyo lifespan de la app host nunca entró en `mcp.session_manager.run()`. -* `Session not found` -> el servidor se reinició; reconecta. +* `Session not found` -> el servidor se reinició o la sesión expiró (`session_idle_timeout`); reconecta. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` necesita un canal de servidor a cliente: una conexión `2026-07-28` nunca lo tiene, `stateless_http=True` quita el heredado y `json_response=True` quita el ligado a la solicitud. Usa un resolutor (un cliente heredado necesita ademĆ”s un servidor que conserve el canal). Su vecino `Method not found` es una solicitud de un mĆ©todo que la revisión del protocolo del otro lado no tiene. * `Client did not declare the form elicitation capability ...` y `Elicitation not supported` -> al cliente le falta `elicitation_callback=`. * `Invalid or expired requestState` nunca dice por quĆ© en lo que se transmite. El log del servidor sĆ­; `unknown key` significa compartir `RequestStateSecurity(keys=[...])` entre los workers. diff --git a/i18n/es/pages/whats-new.md b/i18n/es/pages/whats-new.md index 417f1eed..01a07527 100644 --- a/i18n/es/pages/whats-new.md +++ b/i18n/es/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Novedades de la v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ No todo lo que una herramienta necesita deberĆ­a venir del modelo. Novedad de la La v1 te entregaba tres capas anidadas: un gestor de contexto de transporte que producĆ­a flujos en crudo, una `ClientSession` envolviĆ©ndolos y un `await session.initialize()` llamado a mano. La v2 tiene un solo objeto: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` acepta un objeto servidor (en memoria, sin transporte: la historia de las pruebas), una URL (Streamable HTTP), un `StdioServerParameters` (un subproceso stdio) o cualquier otro gestor de contexto de transporte como `sse_client(...)`. Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente estĆ”n ahĆ­ despuĆ©s, y `client.server_info` tambiĆ©n cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demĆ”s en esta pĆ”gina), ahora tambiĆ©n responden a las solicitudes dentro de resultados al estilo 2026 (mĆ”s abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; tambiĆ©n cambió (se ejecuta sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), asĆ­ que lee la **[GuĆ­a de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel. +`Client` acepta una URL (Streamable HTTP), un `StdioServerParameters` (un subproceso stdio), cualquier otro gestor de contexto de transporte como `sse_client(...)` o, en las pruebas, el propio objeto servidor (en memoria, sin transporte). Entrar en `async with` conecta y negocia la versión del protocolo, sea cual sea la generación que hable el servidor; `client.server_capabilities` y `client.protocol_version` simplemente estĆ”n ahĆ­ despuĆ©s, y `client.server_info` tambiĆ©n cuando el servidor se identifica (ahora es `Implementation | None`, porque la identidad en la generación 2026 es opcional). Los callbacks de muestreo y elicitación que registraste en la v1 siguen funcionando (sus cuerpos ven el mismo renombramiento de atributos a snake_case que todo lo demĆ”s en esta pĆ”gina), ahora tambiĆ©n responden a las solicitudes dentro de resultados al estilo 2026 (mĆ”s abajo) y se ejecutan concurrentemente en lugar de una a una. `ClientSession` sigue debajo para quien quiera la superficie de bajo nivel, y `client.session` te la entrega; tambiĆ©n cambió (se ejecuta sobre el nuevo motor de despacho, y algunas de sus propias firmas cambiaron), asĆ­ que lee la **[GuĆ­a de migración](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de bajar a ese nivel. **[El Client](client/index.md)** lo presenta, **[Transportes del cliente](client/transports.md)** cubre las cuatro formas de conexión, **[Callbacks del cliente](client/callbacks.md)** cubre los callbacks en sĆ­ y **[Pruebas](get-started/testing.md)** muestra el patrón en memoria que sustituye al helper `create_connected_server_and_client_session()` de la v1. @@ -172,11 +172,15 @@ Toda solicitud iniciada por el servidor desaparece en 2026-07-28: elicitación p El reemplazo le da la vuelta a la llamada. Una herramienta que necesita algo del usuario *devuelve* la pregunta (`InputRequiredResult`), el cliente la responde con los mismos callbacks que siempre tuvo, y la llamada se reintenta con las respuestas adjuntas. `Client` dirige ese bucle por ti. En el servidor rara vez construyes tĆŗ el resultado, porque lo hace una **[dependencia](handlers/dependencies.md)**: anota un parĆ”metro con `Resolve(ask_quantity)`, donde `ask_quantity` es una función ordinaria que escribes tĆŗ, y el SDK pregunta por el mecanismo que la conexión admita, una solicitud de elicitación en vivo en una sesión heredada o una solicitud de varias idas y vueltas en 2026. Un solo cuerpo de herramienta, ambas generaciones: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Ese archivo es la propuesta en un solo lugar: un servidor, una herramienta respaldada por `Resolve`, y un cliente heredado mĆ”s un cliente moderno recibiendo ambos su respuesta, en memoria. **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)** explica el mecanismo (incluido `request_state`, que el SDK sella y verifica por ti); **[Elicitación](handlers/elicitation.md)** cubre cómo preguntar. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Esos dos archivos son toda la propuesta: un servidor, una herramienta respaldada por `Resolve`, y un cliente heredado mĆ”s un cliente moderno recibiendo ambos su respuesta del mismo servidor en ejecución (**[Atender clientes heredados](run/legacy-clients.md)** los recorre). **[Solicitudes de varias idas y vueltas](handlers/multi-round-trip.md)** explica el mecanismo (incluido `request_state`, que el SDK sella y verifica por ti); **[Elicitación](handlers/elicitation.md)** cubre cómo preguntar. !!! warning "Este es el Ćŗnico lugar donde un servidor v1 portado cambia de comportamiento" Tus propias pruebas se lo encuentran primero: `Client(mcp)` negocia 2026-07-28 contra tu diff --git a/i18n/fr/pages/advanced/apps.md b/i18n/fr/pages/advanced/apps.md index 8e19e4db..414eee7e 100644 --- a/i18n/fr/pages/advanced/apps.md +++ b/i18n/fr/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ Le SDK fournit cela sous la forme de l’extension intĆ©grĆ©e `Apps` (`io.modelc ## Une horloge avec un cadran {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -31,7 +31,7 @@ Quatre Ć©tapes : Le HTML lui-mĆŖme Ć©coute le `postMessage` de l’hĆ“te et affiche le rĆ©sultat. Pour de vraies applications, utilisez dans votre HTML le SDK navigateur officiel [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Il vous donne `ontoolresult`, `callServerTool`, `getHostContext` et `onhostcontextchanged` au lieu d’évĆ©nements de message bruts. -## DĆ©gradation gracieuse {#graceful-degradation} +## DĆ©gradation Ć©lĆ©gante {#graceful-degradation} Tous les clients n’affichent pas les apps. La spĆ©cification dit sans dĆ©tour ce que cela implique pour vous : @@ -39,11 +39,31 @@ Tous les clients n’affichent pas les apps. La spĆ©cification dit sans dĆ©tour Le modĆØle lit `content` ; l’iframe est pour les humains. Un hĆ“te capable d’afficher une interface transmet quand mĆŖme le rĆ©sultat textuel au modĆØle, et un client purement textuel ne reƧoit *que* cela. Le schĆ©ma canonique est donc : un outil, deux rĆ©ponses. Regardez Ć  nouveau `get_time` : -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` ne vaut `True` que lorsque le client a dĆ©clarĆ© l’extension `io.modelcontextprotocol/ui` **et** listĆ© `text/html;profile=mcp-app` dans ses paramĆØtres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. C’est exactement ce que dĆ©clare `main()` dans le mĆŖme fichier : la moitiĆ© client de la nĆ©gociation, et la rĆ©ponse riche revient. +`client_supports_apps(ctx)` ne vaut `True` que lorsque le client a dĆ©clarĆ© l’extension `io.modelcontextprotocol/ui` **et** listĆ© `text/html;profile=mcp-app` dans ses paramĆØtres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. Voici la moitiĆ© client de la nĆ©gociation : + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Servez `server.py` en HTTP, puis lancez le client depuis un second terminal : + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +La rĆ©ponse riche est revenue. Retirez `extensions=[APPS_SUPPORT]` de l’appel Ć  `Client` et le mĆŖme programme affiche `The time is 2026-06-26T12:00:00Z.` Ć  la place, c’est-Ć -dire tout ce qu’un client purement textuel voit jamais. !!! warning Ne renvoyez jamais un texte de substitution comme `"[Rendered UI]"` pour seul contenu. Si le texte de repli est inutile, l’outil est inutile pour tout client purement textuel et pour le modĆØle lui-mĆŖme. Ɖcrivez la phrase. diff --git a/i18n/fr/pages/advanced/extensions.md b/i18n/fr/pages/advanced/extensions.md index 2ecdc7b2..bea862f3 100644 --- a/i18n/fr/pages/advanced/extensions.md +++ b/i18n/fr/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensions {#extensions} @@ -49,7 +49,7 @@ Utilisez comme prĆ©fixe un domaine que vous contrĆ“lez. `io.modelcontextprotocol La plus petite extension utile, c’est un outil et une table de paramĆØtres : -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ La plus petite extension utile, c’est un outil et une table de paramĆØtres : * `settings()` est la valeur annoncĆ©e sous `capabilities.extensions["com.example/stamps"]`. Renvoyez `{}` (la valeur par dĆ©faut) pour annoncer l’extension sans paramĆØtres. * L’extension ne reƧoit jamais le serveur. Elle dĆ©clare ses contributions sous forme de donnĆ©es ; `MCPServer` les consomme. Il n’y a pas de `self.server` Ć  modifier. -Et `main()` en est la preuve, un client en mĆ©moire branchĆ© directement sur `mcp` : +Servez-la en HTTP, et un client en est la preuve : -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Chaque `server.py` de cette page est servi avec cette commande, et chaque `client.py` tourne Ć  cĆ“tĆ©, lancĆ© avec `python client.py` depuis un second terminal. + ### Servir vos propres mĆ©thodes {#serving-your-own-methods} Une extension peut enregistrer de **nouvelles mĆ©thodes de requĆŖte** : ses propres verbes, servis Ć  cĆ“tĆ© de ceux de la spĆ©cification : -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ Les mĆ©thodes sont **strictement additives**. Le SDK le fait respecter Ć  la con ### Le cĆ“tĆ© client {#the-client-side} -Le `main()` du mĆŖme fichier raconte toute l’histoire cĆ“tĆ© client, ses deux moitiĆ©s : +Le client est un programme Ć  part entiĆØre, et il porte les deux moitiĆ©s de l’histoire cĆ“tĆ© client : -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` dĆ©clare l’extension. Les dĆ©clarations deviennent `ClientCapabilities.extensions` : sur une connexion 2026-07-28, la table voyage dans l’enveloppe `_meta` de chaque requĆŖte, donc le serveur la voit sur **chaque** requĆŖte ; sur une connexion historique, elle transite par la poignĆ©e de main `initialize`. Le code serveur ne s’en soucie pas : `require_client_extension(ctx, ...)` et `ctx.session.check_client_capability(...)` lisent la bonne source dans les deux cas. * Les mĆ©thodes propres Ć  un Ć©diteur descendent d’un niveau, vers `client.session.send_request(...)` ; `Client` n’acquiert de mĆ©thodes de premier rang que pour les verbes de la spĆ©cification. `send_request` accepte n’importe quelle sous-classe de `Request`, donc la requĆŖte de l’éditeur passe telle quelle. +* `SearchRequest` et les deux modĆØles qu’elle transporte constituent le contrat de liaison de l’extension, donc le client les dĆ©clare de son cĆ“tĆ©. Une extension publiĆ©e les fournirait dans un paquet que les deux cĆ“tĆ©s importent. ### Intercepter `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ Le hook enveloppe `tools/call` et rien d’autre. Pour ce qui concerne chaque me ## Utiliser une extension client {#using-a-client-extension} -Une **extension client**, c’est le mĆŖme contrat vu du cĆ“tĆ© consommateur : un ensemble de comportements cĆ“tĆ© client derriĆØre un seul identifiant. Passez des instances Ć  `Client(extensions=[...])` et appelez les outils normalement : +Une **extension client**, c’est le mĆŖme contrat vu du cĆ“tĆ© consommateur : un ensemble de comportements cĆ“tĆ© client derriĆØre un seul identifiant. Ici, le serveur rĆ©pond Ć  `buy` par un reƧu Ć  Ć©changer au lieu de la marchandise, et seulement pour un client qui a dĆ©clarĆ© l’extension : -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +CĆ“tĆ© client, passez des instances Ć  `Client(extensions=[...])` et appelez les outils normalement : + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` renvoie un simple `CallToolResult`, comme tout autre appel. Ce que l’extension a changĆ© : le serveur peut dĆ©sormais rĆ©pondre Ć  `buy` par une **forme de rĆ©sultat** `receipt` au lieu d’un rĆ©sultat final, et `Receipts` la termine (ici en Ć©changeant le reƧu via un appel de suivi) avant que `call_tool` ne renvoie. Rien ne bouge au point d’appel. Retirez l’extension et rien de tout cela n’existe : le garde-fou du serveur refuse un client qui ne l’a pas dĆ©clarĆ©e (erreur -32021), et une forme revendiquĆ©e venant d’un serveur qui saute le garde-fou Ć©choue Ć  la validation, exactement comme la spĆ©cification l’exige pour un `resultType` non reconnu. DĆ©sactivĆ© par dĆ©faut, aux deux bouts de la liaison. @@ -124,15 +137,15 @@ Pour annoncer un identifiant **sans aucun** comportement cĆ“tĆ© client (le serve ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## Ɖcrire une extension client {#writing-a-client-extension} DĆ©rivez `ClientExtension` et ne redĆ©finissez que ce dont vous avez besoin. Trois types de contributions, chacun avec une valeur par dĆ©faut : `settings()`, `claims()` et `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * L’identifiant suit la mĆŖme grammaire que celui du serveur, validĆ© au moment où la classe est dĆ©finie. @@ -153,12 +166,18 @@ Deux rĆØgles discrĆØtes. Les claims ne sont actifs que sur les connexions 2026-0 ### Verbes d’extension {#extension-verbs} -Les mĆ©thodes de requĆŖte propres Ć  une extension n’ont besoin d’aucun enregistrement cĆ“tĆ© client. Un type de requĆŖte d’éditeur dĆ©rive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres mĆ©thodes](#serving-your-own-methods). Un ajout : lorsqu’une clĆ© des paramĆØtres doit transiter par l’en-tĆŖte `Mcp-Name` (des spĆ©cifications d’extension comme tasks l’exigent pour leurs verbes), le type de requĆŖte dĆ©clare `name_param` : +Les mĆ©thodes de requĆŖte propres Ć  une extension n’ont besoin d’aucun enregistrement cĆ“tĆ© client. Un type de requĆŖte d’éditeur dĆ©rive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres mĆ©thodes](#serving-your-own-methods). Prenez un serveur dont l’extension sert un seul verbe portant sur un job nommĆ© : -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +Un ajout cĆ“tĆ© client : lorsqu’une clĆ© des paramĆØtres doit transiter par l’en-tĆŖte `Mcp-Name` (des spĆ©cifications d’extension comme tasks l’exigent pour leurs verbes), le type de requĆŖte dĆ©clare `name_param` : + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + La session reflĆØte `params["jobId"]` dans `Mcp-Name` sur chaque chemin d’envoi, et une valeur manquante Ć©choue bruyamment au lieu d’omettre silencieusement un en-tĆŖte obligatoire. ## Ce qu’une extension ne peut pas faire {#what-an-extension-cannot-do} diff --git a/i18n/fr/pages/advanced/low-level-server.md b/i18n/fr/pages/advanced/low-level-server.md index 4b2d150c..3fc1758b 100644 --- a/i18n/fr/pages/advanced/low-level-server.md +++ b/i18n/fr/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # Le Server de bas niveau {#the-low-level-server} @@ -36,18 +36,22 @@ Trois choses ont changĆ©, et elles constituent toute l’API de bas niveau : ### Essayer {#try-it} -Pas d’Inspector pour celui-ci : `mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`. Le `Client` en mĆ©moire s’en moque ; il accepte un `Server` de bas niveau exactement comme il accepte un `MCPServer` : +`mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`, vous servez donc celui-ci vous-mĆŖme. La derniĆØre ligne de `server.py` construit Ć  partir de lui une application ASGI ordinaire, et uvicorn l’exĆ©cute : -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Pointez l’Inspector, ou n’importe quel client, vers `http://localhost:8000/mcp` : + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ Le mĆŖme texte que celui produit par la version `@mcp.tool()`. Deux diffĆ©rences * `result.structured_content` vaut `None`. Le serveur de haut niveau enveloppe pour vous un `-> str` dans `{"result": ...}` ; ici, personne ne construit ce que vous n’avez pas construit. * `list_tools` renvoie le schĆ©ma que **vous** avez saisi, caractĆØre pour caractĆØre. La version de haut niveau avait `"title": "Query"` sur chaque propriĆ©tĆ© et un `"title": "search_booksArguments"` Ć  la racine : des artefacts de Pydantic. ƀ ce niveau, si quelque chose est sur la liaison, c’est vous qui l’y avez mis. +Dans un test, vous vous passez d’uvicorn et du port : `Client(server)` accepte un `Server` de bas niveau dans le mĆŖme processus exactement comme il accepte un `MCPServer`, et **[Tests](../get-started/testing.md)** dĆ©crit prĆ©cisĆ©ment ce schĆ©ma. + ## Rien n’est vĆ©rifiĆ© pour vous {#nothing-is-checked-for-you} `MCPServer` rejette un mauvais argument avant mĆŖme que votre fonction s’exĆ©cute, en validant l’appel par rapport au schĆ©ma qu’il a gĆ©nĆ©rĆ© (**[Outils](../servers/tools.md)**). @@ -215,4 +221,4 @@ Chacun d’eux correspond Ć  une idĆ©e pour laquelle vous avez dĆ©sormais le voc * `add_request_handler(method, params_type, handler)` sert n’importe quelle mĆ©thode. `initialize` est rĆ©servĆ©e. * Les capacitĆ©s qu’annonce un `Server` dĆ©coulent des gestionnaires que vous avez enregistrĆ©s. -`Client(server)` a traitĆ© les deux serveurs de faƧon identique parce qu’ils *sont* le mĆŖme protocole, et c’est tout l’intĆ©rĆŖt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**. +Le client a traitĆ© les deux serveurs de faƧon identique parce qu’ils *sont* le mĆŖme protocole, et c’est tout l’intĆ©rĆŖt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**. diff --git a/i18n/fr/pages/advanced/pagination.md b/i18n/fr/pages/advanced/pagination.md index 4e0a9eff..2e6c50da 100644 --- a/i18n/fr/pages/advanced/pagination.md +++ b/i18n/fr/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Pagination {#pagination} @@ -31,9 +31,13 @@ La pagination sert au serveur dont la liste de ressources est en rĆ©alitĆ© une b ### Essayer {#try-it} -`Client(server)` se connecte Ć  un `Server` de bas niveau en mĆ©moire exactement comme il se connecte Ć  un `MCPServer`. +`mcp run` n’accepte qu’un `MCPServer`, vous servez donc celui-ci vous-mĆŖme. La derniĆØre ligne de `server.py` construit une application ASGI ordinaire Ć  partir du `Server`, et uvicorn l’exĆ©cute : -Appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` Ć  `book-10`, et `next_cursor` vaut la chaĆ®ne `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Pointez n’importe quel client (**[Le client](../client/index.md)**, ou l’Inspector) vers `http://localhost:8000/mcp` et appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` Ć  `book-10`, et `next_cursor` vaut la chaĆ®ne `"10"`. Renvoyez-la avec `list_resources(cursor="10")` : la premiĆØre ressource est `book-11`, le nouveau `next_cursor` vaut `"20"`. @@ -43,7 +47,7 @@ La dixiĆØme page revient avec `next_cursor` Ć  `None`. TerminĆ©. Chaque mĆ©thode `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) accepte un argument nommĆ© `cursor=`. Vider une liste paginĆ©e tient en un `while True` : -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Chaque mĆ©thode `list_*` de `Client` (`list_tools`, `list_resources`, `list_reso * Ɖtendez la liste **avant** de regarder `next_cursor` : la derniĆØre page contient elle aussi des ressources. * `next_cursor is None` est la sortie. Toute autre valeur repart directement dans `cursor=`, telle quelle. -Lancez son `main()` et il affiche `100 resources` : dix pages de dix, assemblĆ©es par une boucle qui n’a jamais su qu’il y avait dix pages. +Pendant qu’uvicorn sert toujours `server.py`, lancez `python client.py` dans un second terminal. Il affiche `100 resources` : dix pages de dix, assemblĆ©es par une boucle qui n’a jamais su qu’il y avait dix pages. C’est la mĆŖme boucle que montre **[Le client](../client/index.md)** pour chaque verbe `list_*`, et elle ne coĆ»te rien face Ć  un serveur qui ne pagine pas : `next_cursor` vaut `None` dĆØs la premiĆØre rĆ©ponse et la boucle s’exĆ©cute une seule fois. diff --git a/i18n/fr/pages/client/caching.md b/i18n/fr/pages/client/caching.md index a319b784..6ccb5aab 100644 --- a/i18n/fr/pages/client/caching.md +++ b/i18n/fr/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Indications de mise en cache {#caching-hints} @@ -30,7 +30,7 @@ Par dĆ©faut, chaque rĆ©sultat indique `ttlMs: 0, cacheScope: "private"` : pĆ©rim Sur le `Server` bas niveau, les gestionnaires (handlers) construisent leurs rĆ©sultats Ć  la main, et `ttl_ms` / `cache_scope` sont de simples champs des modĆØles de rĆ©sultat. Un gestionnaire qui les dĆ©finit explicitement l’emporte toujours sur le dictionnaire du constructeur, champ par champ : -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Une rĆ©serve sur les listes paginĆ©es : le protocole exige **le mĆŖme `cacheScop Sur une session 2026-07-28, `Client` respecte les indications pour vous : il embarque un cache de rĆ©ponses, activĆ© par dĆ©faut. Un rĆ©sultat qui arrive avec un `ttlMs` est stockĆ©, et un appel identique effectuĆ© dans ce TTL est servi depuis le cache, sans aller-retour. Un rĆ©sultat qui ne porte *aucune* indication n’est pas mis en cache : les rĆ©sultats sans indication reƧoivent `CacheConfig.default_ttl_ms`, dont la valeur par dĆ©faut est `0` (pĆ©rimĆ© immĆ©diatement), si bien qu’un serveur qui ne dĆ©clare rien voit exactement le mĆŖme trafic, appel pour appel, qu’auparavant. -```python title="client.py" hl_lines="33 35 38" +Pour le voir Ć  l’œuvre, servez avec uvicorn le `server.py` de la section prĆ©cĆ©dente (sa derniĆØre ligne construit l’application ASGI). Le gestionnaire affiche une ligne chaque fois qu’il s’exĆ©cute rĆ©ellement : + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Lancez `python client.py` depuis un second terminal. Il affiche les indications que portait le premier rĆ©sultat, le `ttlMs` du gestionnaire Ć  cĆ“tĆ© du `cacheScope` du dictionnaire : + +```text +1000 public +``` + +Le terminal du serveur raconte la suite : entre les journaux de requĆŖtes d’uvicorn, `tools/list served` apparaĆ®t trois fois. + Quatre appels, trois rĆ©cupĆ©rations. Le deuxiĆØme appel a trouvĆ© une entrĆ©e fraĆ®che et n’a jamais atteint le serveur ; avancer l’horloge (injectĆ©e) au-delĆ  du TTL a fait que le troisiĆØme rĆ©cupĆØre Ć  nouveau ; le quatriĆØme a indiquĆ© `cache_mode="refresh"`. Cet argument nommĆ© existe sur les cinq verbes avec cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`) : * `"use"` (la valeur par dĆ©faut) sert une entrĆ©e fraĆ®che s’il y en a une, et stocke le rĆ©sultat rĆ©cupĆ©rĆ© sinon. @@ -56,7 +70,7 @@ Quatre appels, trois rĆ©cupĆ©rations. Le deuxiĆØme appel a trouvĆ© une entrĆ©e f Une rĆØgle prime sur `"use"` : **les appels portant `meta` atteignent toujours le serveur.** Une requĆŖte avec `meta` dĆ©fini (un jeton de progression, des champs de traƧage) attend une requĆŖte sur la liaison ; sous `cache_mode="use"`, elle est donc traitĆ©e comme `"refresh"` : la lecture du cache est sautĆ©e, et le rĆ©sultat rĆ©cupĆ©rĆ© remplace quand mĆŖme l’entrĆ©e en cache. `"bypass"` et un `"refresh"` explicite se comportent comme d’habitude. -Pour dĆ©sactiver entiĆØrement la mise en cache, construisez avec `Client(server, cache=None)` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours acceptĆ©, n’a aucun effet. +Pour dĆ©sactiver entiĆØrement la mise en cache, passez `cache=None` Ć  la construction du `Client` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours acceptĆ©, n’a aucun effet. La portĆ©e est elle aussi respectĆ©e automatiquement : les entrĆ©es `"private"` sont indexĆ©es sur la *partition* du cache (ci-dessous), tandis que les entrĆ©es `"public"` peuvent opter pour un partage plus large. Et **les notifications priment sur le TTL** pour les entrĆ©es exactes qu’elles dĆ©signent : une notification `list_changed` Ć©vince la liste correspondante en cache, et `resources/updated` Ć©vince la lecture en cache stockĆ©e exactement sous son URI, aussi fraĆ®ches soient-elles. Sur une connexion 2026-07-28, ces notifications arrivent sur un flux `subscriptions/listen` que vous ouvrez avec `client.listen(...)`, et l’éviction se termine avant que votre observateur ne voie l’évĆ©nement ; tous les dĆ©tails sont dans **[Abonnements](subscriptions.md)**. diff --git a/i18n/fr/pages/client/callbacks.md b/i18n/fr/pages/client/callbacks.md index 551453f2..2235e0f3 100644 --- a/i18n/fr/pages/client/callbacks.md +++ b/i18n/fr/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Fonctions de rappel du client {#client-callbacks} @@ -63,7 +63,7 @@ Un `tools/call` de votre part, un `elicitation/create` en retour du serveur, auq `mode="legacy"` dans l’appel `Client(...)` fait un vrai travail. Par dĆ©faut, `Client(...)` nĆ©gocie le chemin moderne du protocole, et ce chemin n’a pas de canal de retour (back-channel) pour les requĆŖtes du serveur vers le client : `ctx.elicit` Ć©choue avant mĆŖme que votre fonction de rappel ne s’exĆ©cute. Ce n’est pas le transport qui en dĆ©cide ; c’est le - protocole nĆ©gociĆ©, en mĆ©moire comme via une URL. Fixez `mode="legacy"` dĆØs que votre client doit + protocole nĆ©gociĆ©. Fixez `mode="legacy"` dĆØs que votre client doit rĆ©pondre Ć  l’une d’elles ; tous les tests derriĆØre cette page le font. Tous les dĆ©tails sont dans **[Versions du protocole](../protocol-versions.md)**. Sur une session 2026-07-28, la fonction de rappel n’est pas morte, elle est alimentĆ©e autrement : quand un outil renvoie un @@ -151,4 +151,4 @@ Deux de plus. Aucune ne dĆ©clare quoi que ce soit. * `sampling_callback` et `list_roots_callback` fonctionnent de la mĆŖme maniĆØre mais servent des fonctionnalitĆ©s obsolĆØtes ; les serveurs modernes utilisent Ć  la place les requĆŖtes Ć  plusieurs allers-retours. * `logging_callback` et `message_handler` reƧoivent des notifications. Ils ne dĆ©clarent rien. -Le premier argument de `Client(...)` est un objet transport. **[Transports client](transports.md)** couvre tous les types. +Le premier argument de `Client(...)` choisit le transport. **[Transports client](transports.md)** couvre tous les types. diff --git a/i18n/fr/pages/client/index.md b/i18n/fr/pages/client/index.md index 3a85f884..291933a4 100644 --- a/i18n/fr/pages/client/index.md +++ b/i18n/fr/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # Le client {#the-client} @@ -11,13 +11,23 @@ C’est un seul objet avec un seul cycle de vie : vous le construisez, vous entr ## Votre premier client {#your-first-client} -```python title="client.py" hl_lines="14-18" +Un client a besoin d’un serveur avec qui dialoguer. Ce serveur Bookshop est celui auquel se connecte chaque extrait de cette page. Enregistrez-le sous le nom `server.py` et laissez-le tourner en HTTP : + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Le serveur en haut n’est lĆ  que pour vous donner quelque chose Ć  quoi vous connecter. Le client, ce sont les cinq lignes surlignĆ©es. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` reƧoit **l’objet serveur lui-mĆŖme**. C’est le transport en mĆ©moire : pas de sous-processus, pas de port, pas de HTTP. C’est ainsi que se connectent tous les exemples de cette page, et tous les tests que vous Ć©crivez. +Cela le sert Ć  l’adresse `http://localhost:8000/mcp`. Le client est un programme Ć  part. Enregistrez-le sous le nom `client.py` et lancez `python client.py` dans un second terminal : + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` reƧoit une **URL**, il se connecte donc en Streamable HTTP au serveur que vous venez de dĆ©marrer. * `async with` est le **cycle de vie**. Y entrer connecte et nĆ©gocie ; en sortir dĆ©connecte. Il n’y a pas de paire `connect()` / `close()`, et un `Client` ne peut pas ĆŖtre rĆ©utilisĆ© une fois le bloc terminĆ©. * ƀ l’intĆ©rieur du bloc, les informations de connexion sont dĆ©jĆ  lĆ , sous forme de simples propriĆ©tĆ©s. @@ -25,10 +35,10 @@ Le serveur en haut n’est lĆ  que pour vous donner quelque chose Ć  quoi vous c `Client` prend un seul argument positionnel et dĆ©duit le transport de son type : -* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**. -* Une chaĆ®ne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, la voie de production. -* Un `StdioServerParameters` : la commande Ć  lancer en **sous-processus**, avec laquelle le client dialogue via son stdin et son stdout. +* Une chaĆ®ne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, le transport derriĆØre lequel vous dĆ©ployez. +* Un `StdioServerParameters` : la commande Ć  lancer comme **sous-processus** local, avec laquelle le client dialogue via son stdin et son stdout. * Un **transport** : tout ce sur quoi vous pouvez faire `async with ... as (read, write)`, comme `streamable_http_client(url, http_client=...)` autour de votre propre client HTTP. +* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**, sans sous-processus ni port. Celle-ci sert aux tests, et **[Tests](../get-started/testing.md)** s’appuie dessus. Tout le reste de cette page est identique pour les quatre. Les en-tĆŖtes, les sous-processus, les dĆ©lais d’expiration et le protocole `Transport` ont leur propre page : **[Transports cĆ“tĆ© client](transports.md)**. @@ -49,11 +59,11 @@ Vous n’avez jamais choisi de version du protocole. Par dĆ©faut, le `Client` so ## Lister les outils {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` renvoie un `ListToolsResult` ; les outils sont dans `.tools`. Chacun est la dĆ©finition complĆØte qu’un hĆ“te transmettrait Ć  un modĆØle : +`list_tools()` renvoie un `ListToolsResult` ; les outils sont dans `.tools`. Chacun est la dĆ©finition complĆØte qu’un hĆ“te transmettrait Ć  un modĆØle. Voici le premier : ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ et `tool.input_schema` est le JSON Schema que le serveur a dĆ©rivĆ© des annotati Ce schĆ©ma est tout ce dont une interface a besoin pour afficher un formulaire d’arguments, et tout ce dont un modĆØle a besoin pour produire des arguments valides. +Le second outil, `lookup_book`, a Ć©tĆ© enregistrĆ© sans `title=`, donc son `tool.title` vaut `None`. + !!! tip `title` est facultatif, donc une interface qui prĆ©sente des outils Ć  un humain doit choisir : le `title` s’il existe, le `name` sinon. `from mcp.shared.metadata_utils import get_display_name` fait exactement cela, @@ -86,7 +98,7 @@ Ce schĆ©ma est tout ce dont une interface a besoin pour afficher un formulaire d `call_tool(name, arguments)` exĆ©cute l’outil et vous renvoie un `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ Un outil qui lĆØve une exception ne lĆØve **rien** dans votre client. Il revient Les verbes des ressources vont par paires : deux faƧons de lister, une faƧon de lire. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ Un client peut aussi ĆŖtre prĆ©venu quand une ressource change. Sur les connexio ## Prompts {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ Un hĆ“te transmet ces messages tels quels au modĆØle. C’est toute la fonctionn Un serveur dotĆ© d’un gestionnaire (handler) de complĆ©tion peut complĆ©ter automatiquement les arguments des prompts et des modĆØles de ressource au fil de la saisie de l’utilisateur. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ La rĆ©ponse se trouve dans `result.completion.values`. Tapez `"p"` et le serveur Chaque mĆ©thode `list_*` accepte un argument nommĆ© `cursor=` et chaque rĆ©sultat porte un `next_cursor`. Quand `next_cursor` vaut `None`, vous avez tout. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Cette boucle est correcte face Ć  n’importe quel serveur. `MCPServer` renvoie tout en une seule page, donc `next_cursor` vaut `None` et la boucle s’exĆ©cute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent rĆ©ellement, et les rĆØgles auxquelles obĆ©issent les curseurs, sont dans **[Pagination](../advanced/pagination.md)**. +La fonction `list_all_tools` est correcte face Ć  n’importe quel serveur. `MCPServer` renvoie tout en une seule page, donc `next_cursor` vaut `None` et la boucle s’exĆ©cute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent rĆ©ellement, et les rĆØgles auxquelles obĆ©issent les curseurs, sont dans **[Pagination](../advanced/pagination.md)**. ## Dans les tests {#in-tests} -`Client(mcp)`, sans processus ni port, est dĆ©jĆ  un banc de test pour votre serveur. +Chaque `client.py` de cette page a atteint `server.py` en HTTP. Dans un test, vous vous passez du rĆ©seau et donnez Ć  `Client` l’objet serveur lui-mĆŖme : `from server import mcp`, puis `Client(mcp)`. Pas de processus, pas de port, et chaque mĆ©thode ci-dessus fonctionne de la mĆŖme faƧon. -Il existe un drapeau du constructeur conƧu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions en mĆ©moire, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modĆØle autour de lui. +Il existe un drapeau du constructeur conƧu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions dans le processus, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modĆØle autour de lui. ## RĆ©capitulatif {#recap} -* `Client(x)` se connecte en mĆ©moire Ć  un objet serveur, en Streamable HTTP Ć  une chaĆ®ne d’URL, et Ć  tout le reste via un transport. +* `Client(x)` se connecte en Streamable HTTP Ć  une chaĆ®ne d’URL, lance un sous-processus pour un `StdioServerParameters`, entre directement dans un transport et, dans les tests, prend l’objet serveur lui-mĆŖme. * `async with` est tout le cycle de vie. ƀ l’intĆ©rieur, `server_capabilities` et `protocol_version` sont dĆ©jĆ  renseignĆ©s ; `server_info` et `instructions` le sont aussi lorsque le serveur les fournit. * `list_tools()` vous donne le `name`, le `title`, la `description` et le `input_schema` de chaque outil. * `call_tool()` renvoie `content` pour le modĆØle, `structured_content` pour votre code, et `is_error`. Un outil qui lĆØve une exception est un rĆ©sultat, pas une exception. diff --git a/i18n/fr/pages/client/oauth-clients.md b/i18n/fr/pages/client/oauth-clients.md index 346d2f49..e89a259e 100644 --- a/i18n/fr/pages/client/oauth-clients.md +++ b/i18n/fr/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clients OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Regardez `main()`. Le fournisseur va sur le **client httpx2**, le client httpx2 La premiĆØre fois que `Client` envoie une requĆŖte, le serveur rĆ©pond `401`. Le fournisseur prend le relais : -1. **DĆ©couverte.** Il lit l’en-tĆŖte `WWW-Authenticate`, rĆ©cupĆØre les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protĆØge cette ressource, et rĆ©cupĆØre les mĆ©tadonnĆ©es de *ce* serveur-lĆ . +1. **DĆ©couverte.** Il lit l’en-tĆŖte `WWW-Authenticate`, rĆ©cupĆØre les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protĆØge cette ressource, et rĆ©cupĆØre les mĆ©tadonnĆ©es de *ce* serveur-lĆ . (Un serveur plus ancien qui ne publie aucune mĆ©tadonnĆ©e de ressource se voit demander, Ć  la place, les mĆ©tadonnĆ©es de serveur d’autorisation Ć  sa propre origine.) Dans les deux cas, les mĆ©tadonnĆ©es doivent dĆ©signer, comme `issuer`, le serveur pour lequel elles ont Ć©tĆ© rĆ©cupĆ©rĆ©es ; tout autre cas est refusĆ©. 2. **Enregistrement.** Rien dans le stockage ? Il vous enregistre dynamiquement avec votre `OAuthClientMetadata` et stocke le rĆ©sultat. 3. **Autorisation.** Il gĆ©nĆØre la paire PKCE et un `state`, construit l’URL d’autorisation, attend votre `redirect_handler`, puis attend votre `callback_handler` pour obtenir le code. 4. **Ɖchange.** Il Ć©change le code contre un `OAuthToken`, le stocke, et rejoue votre requĆŖte d’origine avec `Authorization: Bearer ...`. AprĆØs cela, il se fait discret. Les jetons sortent du stockage, un jeton d’accĆØs expirĆ© est actualisĆ© avec le jeton d’actualisation, et ce n’est que lorsque rien de tout cela ne fonctionne qu’il relance le flux. +Une rĆØgle de transport s’applique Ć  toutes ces requĆŖtes : comme la requĆŖte MCP Ć  l’intĆ©rieur de laquelle elles s’exĆ©cutent, elles ne suivent une redirection que si celle-ci reste sur la mĆŖme origine et conserve la mĆ©thode (un 307/308 pour une barre oblique finale, par exemple), et traitent toute autre redirection comme si cette URL ne rĆ©pondait pas. + Vous n’avez rien Ć©crit de tout cela. Il reste deux arguments nommĆ©s (`client_metadata_url` et `validate_resource_url`), et ce fichier n’a besoin d’aucun des deux. `client_metadata_url` est celui qui mĆ©rite d’être connu ; il a sa propre section plus bas. ### Essayer {#try-it} -La plupart des exemples de cette documentation se vĆ©rifient avec un `Client(server)` en mĆ©moire. Pas celui-ci : tout l’intĆ©rĆŖt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mĆ©moire et son serveur. +Le `Client(server)` en mĆ©moire qu’utilisent vos tests n’est d’aucune aide ici : tout l’intĆ©rĆŖt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mĆ©moire et son serveur. Le dĆ©pĆ“t fournit la version rĆ©elle. `examples/servers/simple-auth/` exĆ©cute un serveur d’autorisation autonome et un serveur MCP protĆ©gĆ© ; `examples/clients/simple-auth-client/` est le client de cette page devenu une petite CLI. Son README donne les deux commandes : dĆ©marrez les serveurs, lancez le client contre eux, et vous voyez dĆ©filer les quatre Ć©tapes. @@ -110,13 +112,14 @@ Une tĆ¢che nocturne, une Ć©tape de CI, un autre service. Il n’y a pas de navig `ClientCredentialsOAuthProvider` est le mĆŖme `httpx2.Auth`, l’humain en moins : -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` Ce qui a changĆ© : * Aucun `OAuthClientMetadata`, aucun gestionnaire. Vous passez `client_id` et `client_secret` ; le fournisseur construit autour d’eux un enregistrement `client_credentials` minimal et saute entiĆØrement l’enregistrement dynamique. +* `issuer` dĆ©signe le serveur d’autorisation qui a Ć©mis ces identifiants ; utilisez la valeur `issuer` que renvoie son document `/.well-known/oauth-authorization-server`. La dĆ©couverte se dĆ©roule toujours comme ci-dessus, mais les requĆŖtes de jeton ne sont jamais construites qu’à partir des mĆ©tadonnĆ©es de *cet* Ć©metteur-lĆ  ; si le serveur MCP pointe ailleurs, le flux s’arrĆŖte alors avec une `OAuthFlowError`. L’omettre est obsolĆØte et il devient obligatoire en 3.0 (voir **[FonctionnalitĆ©s obsolĆØtes](../deprecated.md#deprecated-sdk-helpers)**) ; d’ici lĆ , le fournisseur Ć©met un avertissement et utilise le serveur d’autorisation que la dĆ©couverte trouve, quel qu’il soit. * `scope` est une chaĆ®ne sĆ©parĆ©e par des espaces, le format qu’OAuth utilise sur la liaison. * Tout ce qui se trouve en aval est identique : le mĆŖme `TokenStorage`, le mĆŖme `httpx2.AsyncClient(auth=...)`, le mĆŖme `streamable_http_client`. @@ -129,7 +132,7 @@ Par dĆ©faut, le secret voyage en authentification HTTP Basic sur la requĆŖte de Un fournisseur de plus se trouve dans `mcp.client.auth.extensions.client_credentials` : **`PrivateKeyJWTOAuthProvider`**, pour les clients qui s’authentifient avec un JWT plutĆ“t qu’avec un secret partagĆ© (`private_key_jwt`, la variante Ć  paire de clĆ©s et identitĆ© de charge de travail). Il suit - le mĆŖme schĆ©ma : construisez-en un, placez-le sur `auth=`. Le mĆŖme module fournit + le mĆŖme schĆ©ma : construisez-en un (il accepte le mĆŖme `issuer` optionnel), placez-le sur `auth=`. Le mĆŖme module fournit `SignedJWTParameters` et `static_assertion_provider`, deux utilitaires qui construisent son assertion. Il existe une autre situation sans humain : le client appartient Ć  une entreprise dont le fournisseur d’identitĆ©, et non l’utilisateur, dĆ©cide quels serveurs MCP il peut atteindre. C’est un type d’octroi diffĆ©rent, avec son propre modĆØle de confiance et sa propre page, **[Assertion d’identitĆ©](identity-assertion.md)**. diff --git a/i18n/fr/pages/client/session-groups.md b/i18n/fr/pages/client/session-groups.md index 6ab8e19f..0f518986 100644 --- a/i18n/fr/pages/client/session-groups.md +++ b/i18n/fr/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Groupes de sessions {#session-groups} @@ -78,7 +78,7 @@ Si vous dĆ©tenez dĆ©jĆ  une `ClientSession` connectĆ©e (`Client.session` en est ## RĆ©capitulatif {#recap} * `ClientSessionGroup` dĆ©tient de nombreuses connexions serveur et fusionne leurs outils, ressources et prompts en un `dict` chacun. -* `connect_to_server(params)` par serveur. Il prend des paramĆØtres de transport, jamais l’objet serveur ni l’URL que prend un `Client`. +* `connect_to_server(params)` par serveur. Il prend des paramĆØtres de transport, jamais l’URL ni le `Transport` que prend un `Client`. * `group.call_tool(name, arguments)` achemine l’appel vers le serveur propriĆ©taire Ć  votre place. * Les noms doivent ĆŖtre uniques dans tout le groupe ; deux serveurs dotĆ©s d’un outil `search` ne peuvent pas coexister tels quels. * `component_name_hook=` rƩƩcrit chaque nom enregistrĆ©. La clĆ© du dict change, pas le nom sur la liaison. diff --git a/i18n/fr/pages/client/transports.md b/i18n/fr/pages/client/transports.md index dd644ff6..e7aea554 100644 --- a/i18n/fr/pages/client/transports.md +++ b/i18n/fr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transports cĆ“tĆ© client {#client-transports} @@ -11,30 +11,15 @@ Vous n’en configurez jamais un sĆ©parĆ©ment. `Client` prend un seul argument p Le cĆ“tĆ© *serveur* de chacun (ce que fait `mcp.run()` et ce que vous dĆ©ployez) est traitĆ© dans **[ExĆ©cuter votre serveur](../run/index.md)**. -## En mĆ©moire {#in-memory} - -Passez l’objet serveur lui-mĆŖme : - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le mĆŖme processus, et l’appel passe tout de mĆŖme par la vĆ©ritable couche protocolaire : `search_books` est listĆ©, validĆ© et invoquĆ© exactement comme il le serait via HTTP. - -Cela en fait deux choses Ć  la fois : - -* **Un banc de test.** Chaque exemple de cette documentation est exĆ©cutĆ© de cette faƧon, et la page **[Tests](../get-started/testing.md)** construit tout son modĆØle autour de lui. -* **Une API d’intĆ©gration.** Une application qui construit le serveur n’a pas besoin d’un saut rĆ©seau pour appeler ses outils. - ## Streamable HTTP {#streamable-http} -Passez une URL sous forme de chaĆ®ne et vous obtenez **Streamable HTTP**, le transport derriĆØre lequel vous dĆ©ployez : +Passez une URL sous forme de chaĆ®ne et vous obtenez **Streamable HTTP**, le transport derriĆØre lequel vous dĆ©ployez et le premier vers lequel vous tourner : ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configurĆ© comme MCP l’exige : `follow_redirects=True`, un dĆ©lai d’expiration de 30 secondes pour connect/write/pool, et un dĆ©lai de lecture de 300 secondes parce que le serveur peut garder un flux de rĆ©ponse ouvert. +C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configurĆ© comme MCP l’exige : un dĆ©lai d’expiration de 30 secondes pour connect/write/pool, et un dĆ©lai de lecture de 300 secondes parce que le serveur peut garder un flux de rĆ©ponse ouvert. !!! check Un `Client` que vous venez de construire n’est **pas** connectĆ©. La construction ne fait que choisir le transport ; @@ -50,7 +35,7 @@ C’est tout le client de production. `Client` enveloppe l’URL dans `streamabl DĆØs que vous avez besoin d’un en-tĆŖte `Authorization`, d’un cookie, d’un proxy, de mTLS ou d’un dĆ©lai d’expiration diffĆ©rent, construisez le `httpx2.AsyncClient` vous-mĆŖme et passez-le Ć  `streamable_http_client` : -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ ou passez un `verify=ssl_context` explicite Ć  votre `httpx2.AsyncClient` !!! info `httpx2` conserve l’API familiĆØre de `httpx` ; si vous connaissez `httpx`, vous savez dĆ©jĆ  comment gĆ©rer ici l’authentification, les proxys, les hooks d’évĆ©nements, les nouvelles tentatives et les limites de connexions. Le SDK n’ajoute rien par-dessus et ne retire - rien. C’est aussi lĆ  qu’OAuth se branche : + rien, Ć  l’exception de la [gestion des redirections](#redirects). C’est aussi lĆ  qu’OAuth se branche : `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Tout ce flux est dĆ©crit dans **[Clients OAuth](oauth-clients.md)**. +### Redirections {#redirects} + +Le transport se connecte Ć  l’URL que vous lui avez donnĆ©e, et Ć  cette origine uniquement. + +* Une redirection `307`/`308` qui reste sur le mĆŖme schĆ©ma, le mĆŖme hĆ“te et le mĆŖme port est suivie, tout comme `http://` → `https://` sur le mĆŖme hĆ“te. Cela couvre la redirection habituelle de barre oblique finale `/mcp` → `/mcp/`. +* Une redirection vers n’importe quel autre endroit n’est **pas** suivie. L’appel Ć©choue avec : + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Si cette URL est bien le serveur que vous visiez, mettez-la dans votre configuration. Sinon, le serveur ou un proxy placĆ© devant lui est mal configurĆ©. + +Cela vaut pour tout `httpx2.AsyncClient` que vous passez : son rĆ©glage `follow_redirects` n’est pas consultĆ© pour les requĆŖtes MCP, dans un sens comme dans l’autre. Les fournisseurs OAuth du SDK appliquent la mĆŖme rĆØgle Ć  leurs propres requĆŖtes. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` signifie que le + serveur se trouve derriĆØre un proxy Ć  terminaison TLS dont il ignore l’existence et qu’il Ć©met des redirections en `http://`. + Cela se corrige cĆ“tĆ© serveur (**[DĆ©ployer et passer Ć  l’échelle](../run/deploy.md#behind-a-tls-terminating-proxy)**), + ou en utilisant l’URL `https://…/` exacte que le message suggĆØre. + ## stdio {#stdio} Un serveur **stdio** est un sous-processus. Le client le lance, Ć©crit du JSON-RPC sur son stdin et lit du JSON-RPC depuis son stdout. C’est ainsi qu’un hĆ“te de bureau exĆ©cute un serveur sur votre machine : un hĆ“te *est* ce code plus une interface utilisateur, et **[Se connecter Ć  un vĆ©ritable hĆ“te](../get-started/real-host.md)** montre la mĆŖme relation vue du cĆ“tĆ© de l’hĆ“te, sous forme de fichier de configuration. @@ -105,6 +111,18 @@ Le stderr du processus enfant va vers le vĆ“tre. Pour l’envoyer ailleurs, cons Un serveur qui a besoin d’une clĆ© d’API ne l’y trouvera pas. Passez-la explicitement avec `env=` ; ces variables sont fusionnĆ©es par-dessus la liste d’autorisation. C’est ce que fait `BOOKSHOP_API_KEY` ci-dessus. +## En mĆ©moire {#in-memory} + +Dans un test, il n’y a rien Ć  dĆ©ployer ni rien Ć  lancer. Passez l’objet serveur lui-mĆŖme : + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le mĆŖme processus, et l’appel passe tout de mĆŖme par la vĆ©ritable couche protocolaire : `search_books` est listĆ©, validĆ© et invoquĆ© exactement comme il le serait via HTTP. La page **[Tests](../get-started/testing.md)** construit tout son modĆØle autour de lui. + +La mĆŖme forme sert aussi d’API d’intĆ©gration : une application qui construit elle-mĆŖme le serveur peut appeler ses outils sans saut rĆ©seau. + ## SSE {#sse} `sse_client(url)`, du module `mcp.client.sse`, est le transport HTTP que Streamable HTTP a remplacĆ©. Enveloppez-le de la mĆŖme maniĆØre, `Client(sse_client("http://localhost:8000/sse"))`, pour dialoguer avec un serveur qui le parle encore, et ne construisez rien de nouveau dessus. @@ -113,15 +131,16 @@ Le stderr du processus enfant va vers le vĆ“tre. Pour l’envoyer ailleurs, cons Pour `Client`, tout ce qui prĆ©cĆØde est une seule et mĆŖme chose. -Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` rĆ©sout son argument selon son type : un objet serveur se connecte dans le processus, une `str` devient `streamable_http_client(url)`, un `StdioServerParameters` devient `stdio_client(params)`, et tout le reste est ouvert directement comme transport. C’est cette derniĆØre rĆØgle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insĆØrent tous au mĆŖme emplacement, et pourquoi vous pouvez Ć©crire le vĆ“tre. +Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` rĆ©sout son argument selon son type : une `str` devient `streamable_http_client(url)`, un `StdioServerParameters` devient `stdio_client(params)`, un objet serveur se connecte dans le processus, et tout le reste est ouvert directement comme transport. C’est cette derniĆØre rĆØgle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insĆØrent tous au mĆŖme emplacement, et pourquoi vous pouvez Ć©crire le vĆ“tre. ## RĆ©capitulatif {#recap} -* `Client(mcp)` (l’objet serveur) se connecte en mĆ©moire. Utilisez-le pour les tests et pour l’intĆ©gration. * `Client("http://.../mcp")` (une URL) se connecte via Streamable HTTP, le transport de production. * Les en-tĆŖtes, l’authentification, les proxys et les dĆ©lais d’expiration vont sur un `httpx2.AsyncClient` que vous passez Ć  `streamable_http_client(url, http_client=...)`. Il n’y a pas de mot-clĆ© `headers=`. +* Les redirections ne sont suivies qu’à l’intĆ©rieur de l’origine de l’URL (une redirection `307`/`308` de barre oblique finale), plus `http`→`https` sur le mĆŖme hĆ“te. Tout le reste Ć©choue avec `Redirect to … not followed` ; configurez l’URL finale. * stdio s’écrit `Client(StdioServerParameters(...))`. Ne l’enveloppez vous-mĆŖme dans `stdio_client(...)` que pour rediriger le stderr du processus enfant. * Le sous-processus reƧoit un environnement sous liste d’autorisation, pas le vĆ“tre ; `env=` s’y ajoute. +* `Client(mcp)` (l’objet serveur) se connecte en mĆ©moire. Utilisez-le dans les tests, ou pour intĆ©grer un serveur dans l’application qui l’a construit. * Un transport est tout ce sur quoi vous pouvez faire `async with x as (read, write)`. `Client` transmet directement Ć  ce protocole tout ce qui n’est ni un objet serveur, ni une URL, ni un `StdioServerParameters`. * Construire un `Client` choisit le transport. `async with` l’ouvre. diff --git a/i18n/fr/pages/deprecated.md b/i18n/fr/pages/deprecated.md index 9a25f916..58facd98 100644 --- a/i18n/fr/pages/deprecated.md +++ b/i18n/fr/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # FonctionnalitĆ©s obsolĆØtes {#deprecated-features} -La spĆ©cification 2026-07-28 retire cinq Ć©lĆ©ments. Le SDK les implĆ©mente toujours tous, et chacun d’eux porte dĆ©sormais un **avertissement d’obsolescence**. Un utilitaire du SDK est obsolĆØte pour des raisons qui lui sont propres ; il figure [Ć  la fin](#deprecated-sdk-helpers). +La spĆ©cification 2026-07-28 retire cinq Ć©lĆ©ments. Le SDK les implĆ©mente toujours tous, et chacun d’eux porte dĆ©sormais un **avertissement d’obsolescence**. Quelques obsolescences propres au SDK existent pour des raisons qui leur sont propres ; elles figurent [Ć  la fin](#deprecated-sdk-helpers). Le tableau ci-dessous nomme chaque fonctionnalitĆ© obsolĆØte, la raison de sa disparition et le remplacement sur lequel vous appuyer. @@ -138,11 +138,13 @@ C’est toute l’API. Il n’y a pas d’interrupteur par mĆ©thode, et vous n ## Utilitaires du SDK obsolĆØtes {#deprecated-sdk-helpers} -Il ne s’agit pas de changements de la spĆ©cification, seulement de rouages internes du SDK qui ont un meilleur remplacement. Ils avertissent avec le mĆŖme `MCPDeprecationWarning` et seront supprimĆ©s dans la version 3.0. +Il ne s’agit pas de changements de la spĆ©cification, seulement d’usages du SDK qui ont un meilleur remplacement. Ils avertissent avec le mĆŖme `MCPDeprecationWarning`, et la version 3.0 supprime l’ancienne forme. | ObsolĆØte | Ce que vous faites Ć  la place | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` puis `FuncMetadata.call_fn()`. Seul du code qui pilote directement `FuncMetadata` (une sous-classe personnalisĆ©e de `Tool`, par exemple) l’a jamais appelĆ©e. | +| `AuthSettings(resource_server_url=...)` sans `validate_token_resource=` | DĆ©finissez-le : `True` fait refuser au serveur les jetons porteurs que votre vĆ©rificateur ne signale pas comme Ć©mis pour `resource_server_url`, `False` indique que votre vĆ©rificateur contrĆ“le lui-mĆŖme l’audience du jeton (voir **[Autorisation](run/authorization.md#a-token-verifier)**). Non dĆ©fini, il se comporte comme `False` ; la version 3.0 fait de `True` la valeur par dĆ©faut dĆØs que `resource_server_url` est dĆ©fini. | +| `ClientCredentialsOAuthProvider(...)` ou `PrivateKeyJWTOAuthProvider(...)` sans `issuer=` | Passez `issuer=` pour nommer le serveur d’autorisation qui a Ć©mis les identifiants (voir **[Ɖcrire des clients OAuth](client/oauth-clients.md#machine-to-machine)**). Sans lui, c’est le serveur MCP qui dĆ©cide quel serveur d’autorisation les reƧoit ; la version 3.0 rend ce paramĆØtre nommĆ© obligatoire. | ## RĆ©capitulatif {#recap} @@ -151,7 +153,7 @@ Il ne s’agit pas de changements de la spĆ©cification, seulement de rouages int * L’obsolescence est indicative : aucun changement sur la liaison, tout continue de fonctionner sur les sessions d’avant 2026, et vous obtenez un `MCPDeprecationWarning` visible (un `UserWarning`, donc actif par dĆ©faut). * L’échantillonnage et les racines ont en plus besoin d’un canal de retour qu’une session 2026-07-28 n’a pas. Sur une connexion moderne, ils avertissent puis lĆØvent une exception. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` fait taire toute la catĆ©gorie ; `"error::mcp.MCPDeprecationWarning"` dans pytest la transforme en Ć©chec de test. -* Un utilitaire du SDK, `FuncMetadata.call_fn_with_arg_validation()`, est obsolĆØte sĆ©parĆ©ment, pour suppression dans la version 3.0. +* Les [obsolescences propres au SDK](#deprecated-sdk-helpers) suivent la mĆŖme rĆØgle : elles avertissent dĆØs maintenant, et la version 3.0 abandonne l’ancienne forme. * Aucun nouveau code ne devrait s’appuyer sur l’une de ces fonctionnalitĆ©s. Toutes les autres pages de cette documentation enseignent l’API actuelle. diff --git a/i18n/fr/pages/get-started/first-steps.md b/i18n/fr/pages/get-started/first-steps.md index ac30ba9a..c5873f0c 100644 --- a/i18n/fr/pages/get-started/first-steps.md +++ b/i18n/fr/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Premiers pas {#first-steps} @@ -17,7 +17,7 @@ Trois mots que vous verrez sur chaque page Ć  partir d’ici : * Un **client** vit Ć  l’intĆ©rieur de l’hĆ“te et parle MCP. L’hĆ“te exĆ©cute un client par serveur auquel il est connectĆ©. * Un **serveur** est ce que vous construisez avec ce SDK. Il expose des choses aux clients. Il ne parle jamais directement au modĆØle. -Vous Ć©crivez le serveur. Les hĆ“tes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`. Vous l’utiliserez pour tester vos serveurs, et il apparaĆ®t plus loin sur cette page. +Vous Ć©crivez le serveur. Les hĆ“tes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`, la mĆŖme classe qu’un hĆ“te utiliserait pour joindre un serveur par son URL ou le lancer comme sous-processus. Il apparaĆ®t plus loin sur cette page, et c’est aussi avec lui que vous testerez vos serveurs. ## Les trois primitives {#the-three-primitives} @@ -83,22 +83,20 @@ Vous avez vu trois onglets dans l’Inspector. Comment savait-il qu’il y en av Lorsqu’un client se connecte, le serveur dĆ©clare ses **capacitĆ©s** (capabilities) : les familles de requĆŖtes auxquelles il rĆ©pondra. Le client utilise cette dĆ©claration pour dĆ©cider de ce qu’il peut mĆŖme demander. Vous ne l’avez jamais Ć©crite ; `MCPServer` la dĆ©clare pour vous. -Regardez par vous-mĆŖme. Le `Client` du SDK accepte directement l’objet serveur et s’y connecte **en mĆ©moire** (ni sous-processus, ni port) : +Regardez par vous-mĆŖme. Laissez `server.py` tourner en HTTP dans un terminal : -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +et pointez un client dessus depuis un autre : -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ Ce dictionnaire, ce sont les **capacitĆ©s** dĆ©clarĆ©es de votre serveur. C’es Remarquez ce qui n’y figure pas. `completions` (la complĆ©tion automatique des arguments pour les modĆØles de ressources et les prompts) nĆ©cessite un gestionnaire que vous Ć©crivez ; ce serveur n’en a pas, donc la capacitĆ© est absente et un client bien Ć©levĆ© ne demandera rien. C’est la rĆØgle pour tout ce qui est facultatif : enregistrez la chose et la capacitĆ© apparaĆ®t ; **[ComplĆ©tions](../servers/completions.md)** le prouve. !!! info - `Client(mcp)` est le mĆŖme client en mĆ©moire avec lequel chaque exemple de cette documentation est testĆ©, et - c’est ainsi que vous testerez les vĆ“tres. Il a droit Ć  une page entiĆØre : **[Tester](testing.md)**. + Ce `client.py` est un client MCP complet, et **[Le client](../client/index.md)** est sa page. + Dans un test, vous vous passez du terminal et du port et vous donnez Ć  `Client` l’objet serveur lui-mĆŖme, + `Client(mcp)`. Cela a aussi droit Ć  une page entiĆØre : **[Tester](testing.md)**. ## Ce que vous n’avez pas Ć©crit {#what-you-did-not-write} @@ -128,7 +127,7 @@ Reprenez cette page depuis le dĆ©but. Vous avez Ć©crit trois petites fonctions P * De JSON Schema. `a: int, b: int` *est* le schĆ©ma de `add`. * De gestionnaire de requĆŖtes. `tools/list`, `resources/read`, `prompts/get` : tous servis pour vous. * De dĆ©claration de capacitĆ©s. `MCPServer` l’a faite pour vous. -* Une seule ligne de protocole. La nĆ©gociation de version, l’encapsulation JSON-RPC, l’échange de capacitĆ©s : tout cela s’est passĆ© Ć  l’intĆ©rieur de `mcp dev` et de `Client(mcp)`, et vous n’en avez rien vu. +* Une seule ligne de protocole. La nĆ©gociation de version, l’encapsulation JSON-RPC, l’échange de capacitĆ©s : tout cela s’est passĆ© Ć  l’intĆ©rieur de `mcp dev` et de `client.py`, et vous n’en avez rien vu. Ce rapport est tout l’intĆ©rĆŖt du SDK. @@ -139,6 +138,6 @@ Ce rapport est tout l’intĆ©rĆŖt du SDK. * Un dĆ©corateur par primitive : `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Le nom, la description et le schĆ©ma viennent de la fonction. * Un URI avec un `{param}` crĆ©e un **modĆØle** de ressource, listĆ© sĆ©parĆ©ment des ressources concrĆØtes. * Les **capacitĆ©s** du serveur sont dĆ©clarĆ©es pour vous, et un client ne demande que ce qu’un serveur dĆ©clare. -* `Client(mcp)` se connecte Ć  l’objet serveur en mĆ©moire : votre banc d’essai dĆØs le premier jour. +* `Client("http://localhost:8000/mcp")` parle Ć  votre serveur en cours d’exĆ©cution. Donnez-lui plutĆ“t l’objet serveur, `Client(mcp)`, et c’est votre banc d’essai dĆØs le premier jour. La suite, c’est **[Se connecter Ć  un vrai hĆ“te](real-host.md)** : ce serveur dans Claude Desktop ou un IDE, pour de vrai. Puis **[Tester](testing.md)** : une page, un client en mĆ©moire, et vous n’aurez plus jamais Ć  deviner si cela fonctionne. Ensuite, chaque primitive a droit Ć  sa propre page, en commenƧant par celle que pilote le modĆØle : **[Outils](../servers/tools.md)**. diff --git a/i18n/fr/pages/get-started/testing.md b/i18n/fr/pages/get-started/testing.md index c6464add..ab0e7d6a 100644 --- a/i18n/fr/pages/get-started/testing.md +++ b/i18n/fr/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Tests {#testing} -Le SDK Python fournit une classe `Client` dotĆ©e d’un **transport en mĆ©moire** : passez-lui votre objet serveur et il s’y connecte directement. +La classe `Client` du SDK, celle-lĆ  mĆŖme qui se connecte Ć  une URL ou lance un sous-processus, se connecte aussi **en mĆ©moire** : passez-lui votre objet serveur et elle lui parle directement. -Pas de sous-processus. Pas de port. Pas de transport du tout. C’est la mĆŖme idĆ©e que le `TestClient` de FastAPI. +Pas de sous-processus. Pas de port. Rien sur la liaison. C’est la mĆŖme idĆ©e que le `TestClient` de FastAPI. ## Utilisation de base {#basic-usage} @@ -98,15 +98,15 @@ change `raise_exceptions=True` : votre test voit le vrai message au lieu de la v Laissez-le activĆ© dans les tests. Il n’a aucun sens dans du code de production. -## Dans le processus par dĆ©faut {#in-process-by-default} +## Neutre par dĆ©faut vis-Ć -vis de la gĆ©nĆ©ration du protocole {#era-neutral-by-default} !!! note `Client(mcp)` se connecte dans le processus et est **neutre vis-Ć -vis de la gĆ©nĆ©ration du protocole** par dĆ©faut : il sonde le serveur et choisit le chemin de protocole appropriĆ©. Fixez `mode="legacy"` si votre test exerce une sĆ©mantique propre aux connexions historiques (push d’échantillonnage (sampling) ou d’élicitation (elicitation), `message_handler`), et retirez alors - `raise_exceptions=True` : une connexion historique ne neutralise jamais rien, et l’indicateur - relĆØve l’échec dans la tĆ¢che du serveur plutĆ“t que dans votre test. + `raise_exceptions=True` : une connexion historique ne neutralise de toute faƧon jamais rien, et + l’indicateur relance l’échec Ć  l’intĆ©rieur de la tĆ¢che du serveur plutĆ“t que dans votre test. Cette unique ligne est aussi la raison pour laquelle cette documentation peut vous promettre que ses exemples fonctionnent : chaque fichier d’exemple est exercĆ© par la propre suite de tests du diff --git a/i18n/fr/pages/handlers/multi-round-trip.md b/i18n/fr/pages/handlers/multi-round-trip.md index 661c2785..7d8e8d35 100644 --- a/i18n/fr/pages/handlers/multi-round-trip.md +++ b/i18n/fr/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # RequĆŖtes Ć  plusieurs allers-retours (multi-round-trip) {#multi-round-trip-requests} @@ -164,7 +164,7 @@ Le `Server` bas niveau est le niveau sans rien de fourni d’office : contrairem ## Un rĆ©sultat de la version 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` n’existe qu’en version de protocole **2026-07-28**. Le `Client(server)` en mĆ©moire la nĆ©gocie pour vous ; sur la liaison, `mode="auto"` la dĆ©couvre. Une fois connectĆ©, `client.protocol_version` vous dit ce que vous avez obtenu. +`InputRequiredResult` n’existe qu’en version de protocole **2026-07-28**. Le `mode="auto"` par dĆ©faut de `Client` la dĆ©couvre sur n’importe quelle connexion. Une fois connectĆ©, `client.protocol_version` vous dit ce que vous avez obtenu. !!! warning Une session antĆ©rieure Ć  2026 n’a nulle part où mettre un `InputRequiredResult`. Renvoyez-en diff --git a/i18n/fr/pages/handlers/progress.md b/i18n/fr/pages/handlers/progress.md index 29182ac1..64b08fcf 100644 --- a/i18n/fr/pages/handlers/progress.md +++ b/i18n/fr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progression {#progress} @@ -29,19 +29,17 @@ Trois arguments, et c’est vous qui dĆ©cidez de leur sens : Le client active la fonctionnalitĆ© **appel par appel**, en passant `progress_callback=` Ć  `call_tool` : -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,23 +54,27 @@ anyio.run(main) La fonction de rappel (callback) est une fonction `async` qui prend exactement ce que le serveur a signalĆ© : `progress`, `total`, `message`. !!! info - `Client(mcp)` se connecte directement Ć  l’objet serveur, en mĆ©moire : c’est le mĆŖme client que celui sur lequel repose la page **[Tests](../get-started/testing.md)**. `progress_callback` est le mĆŖme paramĆØtre quel que soit le transport qu’utilise le `Client` ; le *timing* que vous allez observer est celui de la connexion en mĆ©moire. Elle exĆ©cute votre fonction de rappel de faƧon synchrone, si bien que chaque signalement arrive avant que `call_tool` ne renvoie. Sur un vrai transport, les notifications font la course avec le rĆ©sultat, et une fonction de rappel lente peut encore ĆŖtre en cours d’exĆ©cution aprĆØs le retour de `call_tool`. + `progress_callback` est le mĆŖme paramĆØtre quoi que vous ayez passĆ© Ć  `Client` : une URL comme ici, un `StdioServerParameters`, ou l’objet serveur dans un test. Attention toutefois au timing sur un vrai transport. Chaque notification est acheminĆ©e seule, Ć  cĆ“tĆ© de la rĆ©ponse, si bien qu’une fonction de rappel lente peut encore ĆŖtre en cours d’exĆ©cution aprĆØs le retour de `call_tool`. Seule la connexion de test en mĆ©moire exĆ©cute la fonction de rappel de faƧon synchrone et garantit que chaque signalement arrive d’abord. ### Essayer {#try-it} -Placez `client.py` Ć  cĆ“tĆ© de `server.py` et lancez-le : +Servez `server.py` en HTTP, puis lancez le client depuis un second terminal : + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Chaque `await ctx.report_progress(...)` cĆ“tĆ© serveur est devenu un appel Ć  `show` cĆ“tĆ© client, dans l’ordre, et les deux lignes se sont affichĆ©es **avant** que `call_tool` ne renvoie. La progression n’est pas empaquetĆ©e dans le rĆ©sultat ; elle est diffusĆ©e pendant que l’outil travaille encore. +Chaque `await ctx.report_progress(...)` cĆ“tĆ© serveur est devenu un appel Ć  `show` cĆ“tĆ© client, dans l’ordre. La progression n’est pas empaquetĆ©e dans le rĆ©sultat. Elle est diffusĆ©e pendant que l’outil travaille encore. !!! warning `progress_callback` appartient Ć  l’**appel**, pas au `Client`. Il n’existe aucun argument de constructeur pour cela, parce que des appels diffĆ©rents veulent des fonctions de rappel diffĆ©rentes : l’un pilote une barre de tĆ©lĆ©chargement, le suivant une ligne de journal. diff --git a/i18n/fr/pages/protocol-versions.md b/i18n/fr/pages/protocol-versions.md index a655ec8d..b9e120ce 100644 --- a/i18n/fr/pages/protocol-versions.md +++ b/i18n/fr/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Versions du protocole {#protocol-versions} @@ -11,9 +11,17 @@ Les serveurs publiĆ©s avant la version 2026-07-28 ouvrent chaque connexion par l Vous n’avez presque jamais Ć  vous en soucier, car `Client` nĆ©gocie pour vous. Cette page porte sur le seul argument du constructeur qui contrĆ“le cela, `mode=`, et sur les trois cas où vous le changez. +Chaque extrait de cette page est un `client.py` qui dialogue avec le `server.py` Bookshop de la page **[Le client](client/index.md)**. Lancez ce serveur dans un premier terminal : + +```console +uv run mcp run server.py --transport streamable-http +``` + +Puis exĆ©cutez chaque extrait dans un second terminal avec `python client.py`. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ Dans les deux cas, vous ressortez connectĆ©, et `client.protocol_version` vous i C’est toute la fonctionnalitĆ©. Un seul `Client`, un serveur de n’importe quelle gĆ©nĆ©ration, aucun branchement dans votre code. !!! info - `MCPServer` rĆ©pond Ć  `server/discover` sur tous les transports — en mĆ©moire, stdio, Streamable - HTTP — donc face Ć  votre propre serveur, `auto` aboutit toujours Ć  `2026-07-28`. Le repli ne - se dĆ©clenche que face Ć  un vrai serveur antĆ©rieur Ć  2026, c’est-Ć -dire exactement quand vous le souhaitez. + `MCPServer` rĆ©pond Ć  `server/discover` sur tous les transports — Streamable HTTP, stdio et la + connexion intra-processus qu’utilisent vos tests — donc face Ć  votre propre serveur, `auto` aboutit + toujours Ć  `2026-07-28`. Le repli ne se dĆ©clenche que face Ć  un vrai serveur antĆ©rieur Ć  2026, + c’est-Ć -dire exactement quand vous le souhaitez. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ En version 2026-07-28, il a disparu. Le serveur *renvoie* ses questions et vous `mode` accepte aussi une chaĆ®ne de version moderne du protocole. Aujourd’hui, cet ensemble est exactement `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ La sonde est peu coĆ»teuse, mais cela reste un aller-retour que vous payez Ć  ch Alors conservez-la. AprĆØs une connexion `auto`, `client.session.discover_result` contient le `DiscoverResult` exact que le serveur a envoyĆ© : ses `supported_versions`, ses `capabilities`, ses `instructions` et l’identitĆ© que le serveur a inscrite dans le `_meta` du rĆ©sultat. Repassez-le via `prior_discover=` la fois suivante : -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/fr/pages/run/asgi.md b/i18n/fr/pages/run/asgi.md index 288262c6..d2821f71 100644 --- a/i18n/fr/pages/run/asgi.md +++ b/i18n/fr/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # Ajouter Ć  une application existante {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Ce `/mcp` final, c’est `streamable_http_path`. DĆ©finissez-le Ć  `"/"` et le p --8<-- "docs_src/asgi/tutorial004.py" ``` -Les clients se connectent dĆ©sormais Ć  `/notes`, et non Ć  `/notes/mcp`. +Les clients se connectent dĆ©sormais Ć  `/notes/`, et non Ć  `/notes/mcp`. ## CORS pour les clients navigateur {#cors-for-browser-clients} @@ -142,4 +142,4 @@ Un client qui s’exĆ©cute dans un navigateur a besoin de deux permissions de vo * Les clients navigateur ont besoin de CORS : `allow_headers` pour les en-tĆŖtes de requĆŖte `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` pour la rĆ©ponse. * `@mcp.custom_route()` ajoute des points de terminaison HTTP ordinaires, non authentifiĆ©s, Ć  cĆ“tĆ© de `/mcp`. -Une fois le serveur joignable Ć  une vraie URL, **[Le client](../client/index.md)** s’y connecte avec cette URL plutĆ“t qu’avec un objet serveur. +Une fois le serveur joignable Ć  une vraie URL, **[Le client](../client/index.md)** s’y connecte avec cette URL. diff --git a/i18n/fr/pages/run/authorization.md b/i18n/fr/pages/run/authorization.md index 57e515a5..5726f506 100644 --- a/i18n/fr/pages/run/authorization.md +++ b/i18n/fr/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorisation {#authorization} @@ -23,12 +23,12 @@ C’est tout le triangle. Toute cette page porte sur le point du milieu. Le SDK n’a aucun avis sur ce Ć  quoi ressemble un jeton valide. C’est vous qui le lui dites, en implĆ©mentant **`TokenVerifier`** : -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` est un protocole avec une seule mĆ©thode asynchrone. `verify_token` reƧoit le jeton brut de l’en-tĆŖte `Authorization` et renvoie un **`AccessToken`** s’il est valide, `None` sinon. Il n’y a rien d’autre Ć  implĆ©menter. -* Celui-ci cherche le jeton dans une table. Un vĆ©rificateur rĆ©el vĆ©rifie la signature d’un JWT ou appelle le point de terminaison d’introspection de jetons du serveur d’autorisation. Ce code est le vĆ“tre ; le SDK ne fait que l’appeler. +* Celui-ci cherche le jeton dans une table ; chaque entrĆ©e consigne la ressource pour laquelle le jeton a Ć©tĆ© Ć©mis. Un vĆ©rificateur rĆ©el vĆ©rifie la signature d’un JWT ou appelle le point de terminaison d’introspection de jetons du serveur d’autorisation, et indique pour qui le jeton a Ć©tĆ© Ć©mis (son `aud`) dans `AccessToken.resource`. Ce code est le vĆ“tre ; le SDK ne fait que l’appeler. * `token_verifier=` et `auth=` vont toujours de pair. Passez l’un sans l’autre et `MCPServer(...)` lĆØve une `ValueError` avant mĆŖme de servir la moindre requĆŖte. `AuthSettings` est la face publique de votre serveur de ressources : @@ -36,6 +36,10 @@ Le SDK n’a aucun avis sur ce Ć  quoi ressemble un jeton valide. C’est vous q * `issuer_url` : le serveur d’autorisation qui Ć©met vos jetons. * `resource_server_url` : l’URL publique de ce point de terminaison MCP. Elle dĆ©signe *quelle* ressource un jeton vise, et c’est lĆ  que rĆ©side le document de dĆ©couverte. * `required_scopes` : chaque jeton doit tous les porter. +* `validate_token_resource` : refuser tout jeton dont `AccessToken.resource` n’est pas `resource_server_url`. Le laisser non dĆ©fini alors que `resource_server_url` est dĆ©fini produit un avertissement (`MCPDeprecationWarning`) et se comporte comme `False` ; la version 3.0 fait de `True` la valeur par dĆ©faut pour les serveurs de ressources. + * Activez-le lorsque votre serveur d’autorisation lie les jetons Ć  la `resource` demandĆ©e par le client, que les clients MCP envoient toujours. Gardez dans `resource_server_url` l’URL exacte Ć  laquelle les clients se connectent. + * Laissez-le dĆ©sactivĆ© lorsque votre serveur d’autorisation utilise ses propres identifiants d’audience (un identifiant d’API Auth0, un ID d’application Entra) et vĆ©rifiez plutĆ“t `aud` dans votre vĆ©rificateur, en renvoyant `None` pour un jeton qui n’est pas destinĆ© Ć  ce serveur. + * Si `aud` est une liste, placez dans `resource` l’entrĆ©e Ć©gale Ć  `resource_server_url`. !!! tip `examples/servers/simple-auth/` dans le dĆ©pĆ“t du SDK contient un `IntrospectionTokenVerifier` qui appelle @@ -91,7 +95,7 @@ C’est grĆ¢ce Ć  ce document qu’un client qui n’a jamais entendu parler de Dans n’importe quel gestionnaire (handler), **`get_access_token()`** est l’objet `AccessToken` que votre vĆ©rificateur a renvoyĆ© pour la requĆŖte en cours : -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Un serveur d’autorisation peut aussi accepter l’assertion signĆ©e d’un fou * `token_verifier=` et `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` vont toujours de pair. * Le SDK publie les mĆ©tadonnĆ©es de ressource protĆ©gĆ©e (Protected Resource Metadata) de la [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) sur `/.well-known/oauth-protected-resource/...` et rĆ©pond aux requĆŖtes non authentifiĆ©es par un 401 dont l’en-tĆŖte `WWW-Authenticate` pointe vers elles. C’est tout le mĆ©canisme de dĆ©couverte. * `get_access_token()` dans n’importe quel gestionnaire indique qui appelle. -* L’autorisation est une affaire de HTTP. `stdio` et le client en mĆ©moire ne la voient jamais. +* L’autorisation est une affaire de HTTP. `stdio` et le client de test en mĆ©moire ne la voient jamais. La moitiĆ© client (dĆ©couvrir votre serveur d’autorisation et rĆ©cupĆ©rer le jeton pour vous), c’est **[Clients OAuth](../client/oauth-clients.md)**. Et un client qui *affirme* une identitĆ© au lieu d’en demander une Ć  un utilisateur, c’est **[Assertion d’identitĆ©](../client/identity-assertion.md)**. diff --git a/i18n/fr/pages/run/deploy.md b/i18n/fr/pages/run/deploy.md index 14df6230..91ba48e4 100644 --- a/i18n/fr/pages/run/deploy.md +++ b/i18n/fr/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # DĆ©ployer et passer Ć  l’échelle {#deploy-scale} @@ -48,6 +48,22 @@ DĆ©ployĆ©e derriĆØre un vrai nom d’hĆ“te, cette mĆŖme valeur par dĆ©faut rejet de liste des hĆ“tes autorisĆ©s jusqu’à preuve du contraire. **[DĆ©pannage](../troubleshooting.md)** commence aussi par lĆ . +## DerriĆØre un proxy de terminaison TLS {#behind-a-tls-terminating-proxy} + +Si TLS se termine sur un proxy (un ingress, un rĆ©partiteur de charge, Caddy, nginx) et qu’uvicorn sert du HTTP en clair derriĆØre lui, dites Ć  uvicorn de faire confiance aux en-tĆŖtes `X-Forwarded-*` du proxy : + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sans cela, l’application croit ĆŖtre servie en `http://`, et toute redirection qu’elle Ć©met (la plus courante est `/mcp` → `/mcp/`) pointe vers `http://…`. Le client Python refuse de suivre une redirection d’un point de terminaison HTTPS vers du HTTP en clair, et le dit : + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Le palliatif cĆ“tĆ© client consiste Ć  configurer l’URL exacte que sert le serveur (`https://mcp.example.com/mcp/`, barre oblique finale comprise) pour qu’aucune redirection n’ait lieu. Le correctif, c’est l’option ci-dessus. `FORWARDED_ALLOW_IPS` en est la forme variable d’environnement ; `*` fait confiance Ć  chaque saut, ce qui n’est correct que si rien d’autre que le proxy ne peut atteindre uvicorn. + ## Les workers, et qui a besoin d’affinitĆ© {#workers-and-who-has-to-be-sticky} Une fois que le nom d’hĆ“te rĆ©pond, placez plus d’un worker derriĆØre lui. Le SDK n’a aucun rĆ©glage pour cela ; vous passez une application Starlette Ć  l’échelle comme n’importe quelle application ASGI, en confiant l’objet Ć  quelque chose qui sait crĆ©er des processus (fork) : @@ -173,6 +189,7 @@ Un `MCPServer` est une implĆ©mentation du protocole, pas un serveur d’applicat ## RĆ©capitulatif {#recap} * Par dĆ©faut, l’application ne rĆ©pond qu’aux requĆŖtes adressĆ©es Ć  localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` est le passage obligĆ© avant la mise en production : tant que vous ne le passez pas, chaque requĆŖte derriĆØre un vrai nom d’hĆ“te est un `421` et la raison n’est que dans le journal du serveur. +* DerriĆØre un proxy de terminaison TLS, lancez uvicorn avec `--proxy-headers --forwarded-allow-ips=...`, sans quoi ses redirections pointent vers `http://` et le client les refuse. * En version 2026-07-28, il n’y a pas de session et rien sur quoi un rĆ©partiteur de charge pourrait Ć©tablir une affinitĆ©. `stateless_http=True` est un rĆ©glage rĆ©servĆ© Ć  la branche historique, parce qu’une requĆŖte moderne est routĆ©e et traitĆ©e avant mĆŖme que cet indicateur soit lu. * La clĆ© `requestState` par dĆ©faut est `os.urandom(32)`, gĆ©nĆ©rĆ©e par processus. Une nouvelle tentative Ć  plusieurs allers-retours qui atteint un autre worker Ć©choue avec `-32602` *Ā« Invalid or expired requestState Ā»*. * Le correctif est `RequestStateSecurity(keys=[...])` **et** le mĆŖme nom de serveur sur chaque instance. Le nom est la revendication d’audience par dĆ©faut du jeton. MĆŖmes clĆ©s, mĆŖme nom. diff --git a/i18n/fr/pages/run/index.md b/i18n/fr/pages/run/index.md index 27adf135..d528c375 100644 --- a/i18n/fr/pages/run/index.md +++ b/i18n/fr/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # ExĆ©cuter votre serveur {#running-your-server} @@ -75,6 +75,11 @@ Chaque transport a ses propres arguments nommĆ©s, tous sur `run()` : * `max_request_body_size` : taille maximale acceptĆ©e pour le corps d’une requĆŖte, en octets. Vaut 4 Mio par dĆ©faut ; les requĆŖtes plus grandes reƧoivent un HTTP 413 avant toute analyse ou crĆ©ation de session. Ne l’augmentez que lorsque des messages MCP lĆ©gitimes dĆ©passent cette taille. +* `session_idle_timeout` : nombre de secondes pendant lesquelles une session historique peut rester sans rien en cours avant que le + serveur ne la ferme. Valeur par dĆ©faut 1800. `None` le dĆ©sactive. Voir + [DurĆ©e de vie des sessions et limites](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions` : nombre de sessions historiques qu’un mĆŖme processus conserve simultanĆ©ment. Valeur par dĆ©faut 10 000. `None` + supprime la limite. TraitĆ© dans la mĆŖme section. * `event_store`, `retry_interval`, `transport_security` : reprise aprĆØs coupure et protection contre le DNS rebinding. Ils peuvent attendre, jusqu’à ce que vous dĆ©ployiez ailleurs que sur localhost ; **[DĆ©ployer et passer Ć  l’échelle](deploy.md)** couvre `transport_security`. !!! warning diff --git a/i18n/fr/pages/run/legacy-clients.md b/i18n/fr/pages/run/legacy-clients.md index 80b1f796..c50f3763 100644 --- a/i18n/fr/pages/run/legacy-clients.md +++ b/i18n/fr/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Prendre en charge les clients historiques {#serving-legacy-clients} @@ -22,15 +22,25 @@ Un client historique n’est donc pas quelque chose *pour* lequel vous construis ## Un gestionnaire, deux gĆ©nĆ©rations {#one-handler-both-eras} -Voici un outil (tool) qui doit demander quelque chose Ć  l’utilisateur, et des clients des deux gĆ©nĆ©rations qui l’appellent : +Voici un outil (tool) qui doit demander quelque chose Ć  l’utilisateur : -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` a besoin d’une chose que le modĆØle n’a pas fournie : le nombre d’exemplaires. `Annotated[..., Resolve(ask_quantity)]` est la faƧon dont un outil le dĆ©clare (tous les dĆ©tails sont dans **[DĆ©pendances](../handlers/dependencies.md)**). Rien dans `reserve` ne nomme une version, ne vĆ©rifie une capacitĆ© ni ne bifurque. -Les deux clients sont ouverts **en mĆŖme temps**, sur le mĆŖme objet `mcp`. `mode="legacy"` exĆ©cute la poignĆ©e de main `initialize` : exactement la connexion qu’ouvre un client antĆ©rieur Ć  2026. L’autre prend la valeur par dĆ©faut et arrive en version `2026-07-28`. +Servez-le en HTTP, et voici des clients des deux gĆ©nĆ©rations qui l’appellent : + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Les deux clients sont ouverts **en mĆŖme temps**, sur le mĆŖme serveur en cours d’exĆ©cution. `mode="legacy"` exĆ©cute la poignĆ©e de main `initialize` : exactement la connexion qu’ouvre un client antĆ©rieur Ć  2026. L’autre prend la valeur par dĆ©faut et arrive en version `2026-07-28`. Lancez `python client.py` depuis un second terminal : ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -63,6 +73,41 @@ Sur un seul worker, c’est invisible. Sur deux, c’est tout le problĆØme : une Ć©vĆ©nements SSE manquĆ©s pour un client qui se reconnecte Ć  la *mĆŖme* session), pas un magasin de sessions. Il ne rend jamais une session accessible depuis un autre processus. +## DurĆ©e de vie et limites des sessions {#session-lifetime-and-limits} + +Une session historique ne vit pas Ć©ternellement, et un processus n’en conserve pas un nombre +illimitĆ©. Deux rĆ©glages contrĆ“lent cela. Tous deux sont des arguments nommĆ©s de `run()`, +`streamable_http_app()` et `Server.streamable_http_app()`. Les connexions modernes (`2026-07-28`) et +`stateless_http=True` n’ont pas de sessions, si bien qu’aucun des deux rĆ©glages ne s’y applique. + +| RĆ©glage | Valeur par dĆ©faut | Ce qu’il fait | Ce que voit le client | Pour le dĆ©sactiver | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Ferme une session qui n’a rien eu en cours pendant cette durĆ©e. | `404 Session not found`. Il doit refaire `initialize`. | `None` | +| `max_sessions` | `10_000` | Refuse d’ouvrir une session au-delĆ  de ce nombre. Les sessions existantes ne sont pas touchĆ©es et rien n’est Ć©vincĆ©. | `503 Too many open sessions` avec le code JSON-RPC `-32603`. | `None` | + +Ce qui compte comme Ā« en cours Ā» : + +* Un flux `GET` ouvert. Les clients du SDK en gardent un ouvert, si bien que la session d’un client + connectĆ© n’expire jamais. +* Une requĆŖte Ć  laquelle on est encore en train de rĆ©pondre. Un appel d’outil qui dure plus longtemps + que le dĆ©lai n’est pas interrompu, et le compte Ć  rebours ne dĆ©marre qu’une fois qu’il a terminĆ©. +* Rien d’autre. Entre les requĆŖtes, l’horloge tourne. N’importe quelle requĆŖte sur la session la + rĆ©initialise, `ping` compris. Une fois qu’une session a expirĆ©, rien ne la ranime. + +Un client qui met fin Ć  sa session par un `DELETE` la libĆØre immĆ©diatement. MĆŖme chose pour un client +dont la requĆŖte d’ouverture a Ć©tĆ© refusĆ©e. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Les deux Ć©vĆ©nements apparaissent dans le journal du serveur. Une expiration donne `Session idle timeout` +au niveau `INFO`. Une ouverture refusĆ©e donne `Refusing to open a new session: sessions are already open` +au niveau `WARNING`. + +Les limites s’entendent par processus. Avec quatre workers, le plafond est quatre fois `max_sessions`, et +chaque worker fait expirer ses propres sessions. + ## Le seul rĆ©glage : `stateless_http` {#the-one-knob-stateless_http} Si l’affinitĆ© est un coĆ»t que vous refusez de payer, il y a exactement une chose que vous pouvez changer. @@ -88,8 +133,8 @@ Deux choses Ć  son sujet comptent plus que ce qu’il fait. !!! check Faites la mauvaise chose. `reserve` est exactement l’outil qui vient de servir les deux clients. - DĆ©ployez-le avec `stateless_http=True`, connectez les deux mĆŖmes clients en HTTP et appelez-le - depuis chacun. + DĆ©ployez-le avec `stateless_http=True`, connectez les deux mĆŖmes clients et appelez-le depuis + chacun. Le client moderne obtient toujours `Reserved 2 of 'Dune'.` La voie moderne n’a pas changĆ©. diff --git a/i18n/fr/pages/troubleshooting.md b/i18n/fr/pages/troubleshooting.md index c928c6b7..1cfa29d8 100644 --- a/i18n/fr/pages/troubleshooting.md +++ b/i18n/fr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # DĆ©pannage {#troubleshooting} @@ -13,6 +13,12 @@ Plusieurs entrĆ©es s’appuient sur ce mĆŖme serveur. Un outil (tool) et une res --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Ces entrĆ©es le joignent Ć  l’adresse `http://localhost:8000/mcp`, laissez-le donc tourner en HTTP : + +```console +uv run mcp run server.py --transport streamable-http +``` + Les erreurs citĆ©es sur cette page sont rĆ©elles : la suite de tests du SDK reproduit chacune d’entre elles. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Ce n’est pas une erreur MCP. C’est du bruit produit par anyio, et votre vrai ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Deux choses Ć  faire avec cela : ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entrez-y. `__aenter__` est la connexion : ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Le serveur ne reconnaĆ®t pas le `Mcp-Session-Id` que votre client a envoyĆ©, presque toujours parce que le serveur a **redĆ©marrĆ©** (ou que vous avez Ć©tĆ© routĆ© vers une autre instance). Les sessions vivent dans la mĆ©moire de ce seul processus. +Le serveur ne reconnaĆ®t pas le `Mcp-Session-Id` que votre client a envoyĆ©. Soit le serveur a **redĆ©marrĆ©** (ou vous avez Ć©tĆ© routĆ© vers une autre instance), soit la session a **expirĆ©** parce que rien n’était en cours pendant `session_idle_timeout`, qui vaut 30 minutes par dĆ©faut. Voir [DurĆ©e de vie et limites des sessions](run/legacy-clients.md#session-lifetime-and-limits). Les sessions vivent dans la mĆ©moire de ce seul processus. Il n’y a pas de bogue serveur Ć  trouver. La rĆ©ponse HTTP est un `404` dont le corps *est* du JSON-RPC, donc, contrairement au `421` ci-dessus, le `Client` python vous montre celui-ci mot pour mot : @@ -262,9 +268,9 @@ Il n’y a pas de bogue serveur Ć  trouver. La rĆ©ponse HTTP est un `404` dont l Le correctif est de vous reconnecter : quittez le bloc `async with Client(...)` et entrez dans un nouveau, qui nĆ©gocie une session neuve. Pour un client de longue durĆ©e, cela signifie intercepter `MCPError` autour de vos appels et vous reconnecter sur ce message plutĆ“t que de rĆ©essayer dans une session morte. -Si cela arrive *sans* redĆ©marrage, vous exĆ©cutez plus d’un worker sans sessions persistantes (sticky sessions) : chaque worker dĆ©tient sa propre table de sessions, donc une requĆŖte routĆ©e vers le mauvais atterrit ici. **[DĆ©ployer et passer Ć  l’échelle](run/deploy.md)** et **[Prendre en charge les clients historiques](run/legacy-clients.md)** traitent ce sujet et ses deux correctifs (routage persistant, ou `stateless_http=True`). +Si cela arrive *sans* redĆ©marrage et sans que le client soit restĆ© silencieux aussi longtemps, vous exĆ©cutez plus d’un worker sans sessions persistantes (sticky sessions) : chaque worker dĆ©tient sa propre table de sessions, donc une requĆŖte routĆ©e vers le mauvais atterrit ici. **[DĆ©ployer et passer Ć  l’échelle](run/deploy.md)** et **[Prendre en charge les clients historiques](run/legacy-clients.md)** traitent ce sujet et ses deux correctifs (routage persistant, ou `stateless_http=True`). -Pour l’opĆ©rateur du serveur, la ligne de journal correspondante est `Rejected request with unknown or expired session ID: `. Elle est journalisĆ©e au niveau `INFO`, elle est donc invisible au seuil habituel `WARNING`. La voir par rafales juste aprĆØs un dĆ©ploiement est normal ; chaque client connectĆ© se reconnecte. +Pour l’opĆ©rateur du serveur, la ligne de journal correspondante est `Rejected request with unknown or expired session ID: `. Elle est journalisĆ©e au niveau `INFO`, elle est donc invisible au seuil habituel `WARNING`. La voir par rafales juste aprĆØs un dĆ©ploiement est normal ; chaque client connectĆ© se reconnecte. Lorsque la session a plutĆ“t expirĆ©, cette ligne est prĆ©cĆ©dĆ©e de `Session idle timeout`, Ć©galement au niveau `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ Une chose ne produit **pas** cette erreur, bien qu’il s’agisse d’une requ Votre serveur veut demander quelque chose Ć  l’utilisateur, et ce client n’a jamais dit qu’on pouvait l’interroger. -Un rĆ©solveur d’élicitation (elicitation) refuse d’emblĆ©e lorsque le client connectĆ© n’a pas dĆ©clarĆ© l’élicitation par formulaire, et `e.error.data` nomme exactement ce qui manque : +Ce Bistro pose la question avant de rĆ©server, via un rĆ©solveur : + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Servez-le Ć  la place du serveur Weather et appelez `book_table` depuis un client qui n’a passĆ© aucun `elicitation_callback`. Le rĆ©solveur refuse d’emblĆ©e, parce que le client connectĆ© n’a jamais dĆ©clarĆ© l’élicitation (elicitation) par formulaire, et `e.error.data` nomme exactement ce qui manque : ```json { @@ -290,7 +302,7 @@ Passez `elicitation_callback=` Ć  `Client(...)`. Enregistrer la fonction de rapp ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -316,14 +328,14 @@ Vous voyez celui-ci depuis `ctx.elicit()` sur une connexion historique, et sur n Votre gestionnaire a tentĆ© de joindre le client en cours de requĆŖte, sur une connexion dont l’appel n’a aucun canal capable de transporter une requĆŖte venant du serveur. Trois configurations de serveur placent un appel dans cette situation. -**Une connexion `2026-07-28` : n’importe quel transport, toujours.** Le protocole moderne n’a aucune requĆŖte Ć  l’initiative du serveur, si bien que le serveur refuse avant que quoi que ce soit ne soit envoyĆ©. `ctx.elicit()` dans un outil est la faƧon classique de la rencontrer (dĆØs le tout premier test en mĆ©moire, puisque `Client(server)` nĆ©gocie `2026-07-28` sans qu’on le lui demande), et passer `elicitation_callback=` ne change rien, parce qu’aucune requĆŖte n’atteint jamais le client pour qu’il y rĆ©ponde : +**Une connexion `2026-07-28` : n’importe quel transport, toujours.** Le protocole moderne n’a aucune requĆŖte Ć  l’initiative du serveur, si bien que le serveur refuse avant que quoi que ce soit ne soit envoyĆ©. `ctx.elicit()` dans un outil est la faƧon classique de la rencontrer, le plus souvent dĆØs le tout premier **[test](get-started/testing.md)** en mĆ©moire de cet outil, puisque `Client(mcp)` nĆ©gocie `2026-07-28` sans qu’on le lui demande. Passer `elicitation_callback=` ne change rien, parce qu’aucune requĆŖte n’atteint jamais le client pour qu’il y rĆ©ponde : ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -366,7 +378,7 @@ Le serveur n’a pas pu vĆ©rifier le jeton `requestState` que votre client a ren ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -419,7 +431,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` dans le journal du serveur est le seul signe que deux outils de mĆŖme nom se sont fondus en un seul. * Un 421, trois formulations : `Server returned an error response` (le `Client` python), `421 Misdirected Request` / `Invalid Host header` (tout le reste), `Invalid Host header: ` (le journal du serveur). Correctif : `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> une application montĆ©e dont le cycle de vie de l’hĆ“te n’est jamais entrĆ© dans `mcp.session_manager.run()`. -* `Session not found` -> le serveur a redĆ©marrĆ© ; reconnectez-vous. +* `Session not found` -> le serveur a redĆ©marrĆ© ou la session a expirĆ© (`session_idle_timeout`) ; reconnectez-vous. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` a besoin d’un canal serveur-vers-client : une connexion `2026-07-28` n’en a jamais, `stateless_http=True` retire celui des connexions historiques, et `json_response=True` retire celui attachĆ© Ć  la requĆŖte. Utilisez un rĆ©solveur (un client historique a aussi besoin d’un serveur qui conserve le canal). Son voisin `Method not found` est une requĆŖte pour une mĆ©thode que la rĆ©vision du protocole de l’autre cĆ“tĆ© ne possĆØde pas. * `Client did not declare the form elicitation capability ...` et `Elicitation not supported` -> il manque `elicitation_callback=` au client. * `Invalid or expired requestState` ne dit jamais pourquoi sur la liaison. Le journal du serveur, si ; `unknown key` signifie qu’il faut partager `RequestStateSecurity(keys=[...])` entre les workers. diff --git a/i18n/fr/pages/whats-new.md b/i18n/fr/pages/whats-new.md index e8a36aac..28a15ebb 100644 --- a/i18n/fr/pages/whats-new.md +++ b/i18n/fr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # NouveautĆ©s de la v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ Tout ce dont un outil a besoin ne devrait pas venir du modĆØle. NouveautĆ© de la La v1 vous donnait trois couches imbriquĆ©es : un gestionnaire de contexte de transport produisant des flux bruts, une `ClientSession` qui les enveloppait et un `await session.initialize()` appelĆ© Ć  la main. La v2 a un seul objet : -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` accepte un objet serveur (en mĆ©moire, sans transport : c’est la solution pour les tests), une URL (Streamable HTTP), un `StdioServerParameters` (un sous-processus stdio) ou n’importe quel autre gestionnaire de contexte de transport comme `sse_client(...)`. Entrer dans `async with` Ć©tablit la connexion et nĆ©gocie la version du protocole, quelle que soit la gĆ©nĆ©ration que parle le serveur ; `client.server_capabilities` et `client.protocol_version` sont simplement disponibles ensuite, et `client.server_info` aussi lorsque le serveur s’identifie (c’est dĆ©sormais `Implementation | None`, puisque l’identitĆ© est optionnelle dans la gĆ©nĆ©ration 2026). Les fonctions de rappel (callbacks) d’échantillonnage et d’élicitation que vous aviez enregistrĆ©es en v1 fonctionnent toujours (leur corps voit le mĆŖme renommage d’attributs en snake_case que tout le reste de cette page), elles rĆ©pondent dĆ©sormais aussi aux requĆŖtes-dans-les-rĆ©sultats de style 2026 (ci-dessous), et elles s’exĆ©cutent de faƧon concurrente plutĆ“t qu’une Ć  la fois. `ClientSession` reste en dessous pour qui veut la surface bas niveau, et `client.session` vous la donne ; elle a bougĆ© elle aussi (elle tourne sur le nouveau moteur de rĆ©partition, et certaines de ses propres signatures ont changĆ©), alors lisez le **[Guide de migration](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** avant de descendre Ć  ce niveau. +`Client` accepte une URL (Streamable HTTP), un `StdioServerParameters` (un sous-processus stdio), n’importe quel autre gestionnaire de contexte de transport comme `sse_client(...)` ou, dans les tests, l’objet serveur lui-mĆŖme (en mĆ©moire, sans transport). Entrer dans `async with` Ć©tablit la connexion et nĆ©gocie la version du protocole, quelle que soit la gĆ©nĆ©ration que parle le serveur ; `client.server_capabilities` et `client.protocol_version` sont simplement disponibles ensuite, et `client.server_info` aussi lorsque le serveur s’identifie (c’est dĆ©sormais `Implementation | None`, puisque l’identitĆ© est optionnelle dans la gĆ©nĆ©ration 2026). Les fonctions de rappel (callbacks) d’échantillonnage et d’élicitation que vous aviez enregistrĆ©es en v1 fonctionnent toujours (leur corps voit le mĆŖme renommage d’attributs en snake_case que tout le reste de cette page), elles rĆ©pondent dĆ©sormais aussi aux requĆŖtes-dans-les-rĆ©sultats de style 2026 (ci-dessous), et elles s’exĆ©cutent de faƧon concurrente plutĆ“t qu’une Ć  la fois. `ClientSession` reste en dessous pour qui veut la surface bas niveau, et `client.session` vous la donne ; elle a bougĆ© elle aussi (elle tourne sur le nouveau moteur de rĆ©partition, et certaines de ses propres signatures ont changĆ©), alors lisez le **[Guide de migration](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** avant de descendre Ć  ce niveau. **[Le Client](client/index.md)** le prĆ©sente, **[Transports du client](client/transports.md)** couvre les quatre formes de connexion, **[Fonctions de rappel du client](client/callbacks.md)** couvre les fonctions de rappel elles-mĆŖmes, et **[Tests](get-started/testing.md)** montre le modĆØle en mĆ©moire qui remplace l’utilitaire `create_connected_server_and_client_session()` de la v1. @@ -172,11 +172,15 @@ Toutes les requĆŖtes initiĆ©es par le serveur disparaissent en version 2026-07-2 Le remplacement inverse l’appel. Un outil qui a besoin de quelque chose de la part de l’utilisateur *renvoie* la question (`InputRequiredResult`), le client y rĆ©pond avec les mĆŖmes fonctions de rappel qu’il a toujours eues, et l’appel est relancĆ© avec les rĆ©ponses jointes. `Client` pilote cette boucle pour vous. CĆ“tĆ© serveur, vous construisez rarement le rĆ©sultat vous-mĆŖme, car une **[dĆ©pendance](handlers/dependencies.md)** le fait : annotez un paramĆØtre avec `Resolve(ask_quantity)`, où `ask_quantity` est une fonction ordinaire que vous Ć©crivez, et le SDK pose la question par le mĆ©canisme que la connexion prend en charge, une requĆŖte d’élicitation en direct sur une session historique ou une requĆŖte Ć  plusieurs allers-retours en 2026. Un seul corps d’outil, les deux gĆ©nĆ©rations : -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Ce fichier rĆ©sume tout l’argument en un seul endroit : un serveur, un outil adossĆ© Ć  `Resolve`, et un client historique plus un client moderne qui obtiennent tous deux leur rĆ©ponse, en mĆ©moire. **[RequĆŖtes Ć  plusieurs allers-retours](handlers/multi-round-trip.md)** explique le mĆ©canisme (y compris `request_state`, que le SDK scelle et vĆ©rifie pour vous) ; **[Ɖlicitation](handlers/elicitation.md)** couvre la faƧon de poser la question. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Ces deux fichiers rĆ©sument tout l’argument : un serveur, un outil adossĆ© Ć  `Resolve`, et un client historique plus un client moderne qui obtiennent tous deux leur rĆ©ponse du mĆŖme serveur en cours d’exĆ©cution (**[Prendre en charge les clients historiques](run/legacy-clients.md)** les passe en revue). **[RequĆŖtes Ć  plusieurs allers-retours](handlers/multi-round-trip.md)** explique le mĆ©canisme (y compris `request_state`, que le SDK scelle et vĆ©rifie pour vous) ; **[Ɖlicitation](handlers/elicitation.md)** couvre la faƧon de poser la question. !!! warning "C’est le seul endroit où un serveur v1 portĆ© change de comportement" Vos propres tests y butent en premier : `Client(mcp)` nĆ©gocie par dĆ©faut 2026-07-28 avec votre diff --git a/i18n/hi/pages/advanced/apps.md b/i18n/hi/pages/advanced/apps.md index 13361d90..efaea6c3 100644 --- a/i18n/hi/pages/advanced/apps.md +++ b/i18n/hi/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK ą¤‡ą¤øą„‡ built-in `Apps` extension (`io.modelcontextprotocol/ui`) ą¤•ą„‡ ## ą¤šą„‡ą¤¹ą¤°ą„‡ ą¤µą¤¾ą¤²ą„€ ą¤˜ą¤”ą¤¼ą„€ {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -49,15 +49,37 @@ model `content` पढ़ता ą¤¹ą„ˆ; iframe ą¤‡ą¤‚ą¤øą¤¾ą¤Øą„‹ą¤‚ ą¤•ą„‡ ल model ą¤•ą„‹ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, और text-only client ą¤•ą„‹ **ą¤øą¤æą¤°ą„ą¤«ą¤¼** ą¤µą¤¹ą„€ मिलता ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤²ą¤æą¤ मानक pattern ą¤¹ą„ˆ: ą¤ą¤• tool, ą¤¦ą„‹ ą¤œą¤µą¤¾ą¤¬ą„¤ `get_time` ą¤•ą„‹ फिर ą¤øą„‡ ą¤¦ą„‡ą¤–ą„‡ą¤‚: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` `client_supports_apps(ctx)` ą¤¤ą¤­ą„€ `True` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ जब client ą¤Øą„‡ `io.modelcontextprotocol/ui` extension declare किया ą¤¹ą„‹ **और** ą¤…ą¤Ŗą¤Øą„€ `mimeTypes` settings ą¤®ą„‡ą¤‚ `text/html;profile=mcp-app` ą¤øą„‚ą¤šą„€ą¤¬ą¤¦ą„ą¤§ किया ą¤¹ą„‹ą„¤ यह field ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„ˆ, -ą¤‡ą¤øą¤²ą¤æą¤ ą¤œą„‹ client ą¤‡ą¤øą„‡ ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ वह गिना ą¤Øą¤¹ą„€ą¤‚ ą¤œą¤¾ą¤¤ą¤¾ą„¤ ą¤‡ą¤øą„€ file ą¤®ą„‡ą¤‚ `main()` ą¤ ą„€ą¤• ą¤Æą¤¹ą„€ -declare करता ą¤¹ą„ˆ: negotiation का client वाला आधा ą¤¹ą¤æą¤øą„ą¤øą¤¾, और rich जवाब वापस आता ą¤¹ą„ˆą„¤ +ą¤‡ą¤øą¤²ą¤æą¤ ą¤œą„‹ client ą¤‡ą¤øą„‡ ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ वह गिना ą¤Øą¤¹ą„€ą¤‚ ą¤œą¤¾ą¤¤ą¤¾ą„¤ negotiation का client वाला +आधा ą¤¹ą¤æą¤øą„ą¤øą¤¾ यह रहा: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` ą¤•ą„‹ HTTP पर serve ą¤•ą¤°ą„‡ą¤‚, फिर ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ client ą¤šą¤²ą¤¾ą¤ą¤: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +rich जवाब वापस ą¤†ą¤Æą¤¾ą„¤ `Client` call ą¤øą„‡ `extensions=[APPS_SUPPORT]` हटा ą¤¦ą„‡ą¤‚, ą¤¤ą„‹ ą¤Æą¤¹ą„€ +program ą¤‡ą¤øą¤•ą„€ ą¤œą¤—ą¤¹ `The time is 2026-06-26T12:00:00Z.` print करता ą¤¹ą„ˆ, और text-only +client ą¤•ą„‹ ą¤¹ą¤®ą„‡ą¤¶ą¤¾ बस इतना ą¤¹ą„€ दिखता ą¤¹ą„ˆą„¤ !!! warning ą¤•ą¤­ą„€ ą¤­ą„€ `"[Rendered UI]"` ą¤œą„ˆą¤øą¤¾ placeholder ą¤…ą¤•ą„‡ą¤²ą„‡ content ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ न ą¤²ą„Œą¤Ÿą¤¾ą¤ą¤ą„¤ diff --git a/i18n/hi/pages/advanced/extensions.md b/i18n/hi/pages/advanced/extensions.md index 3617649c..3b4366f0 100644 --- a/i18n/hi/pages/advanced/extensions.md +++ b/i18n/hi/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Extensions {#extensions} @@ -49,7 +49,7 @@ prefix ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ऐसा domain ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² क ą¤øą¤¬ą¤øą„‡ ą¤›ą„‹ą¤Ÿą¤¾ काम का extension ą¤ą¤• tool और ą¤ą¤• settings map ą¤¹ą„ˆ: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ prefix ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ऐसा domain ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² क * `settings()` वह value ą¤¹ą„ˆ ą¤œą„‹ `capabilities.extensions["com.example/stamps"]` पर advertise ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą„¤ बिना settings ą¤•ą„‡ extension advertise ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ `{}` (default) ą¤²ą„Œą¤Ÿą¤¾ą¤ą¤ą„¤ * extension ą¤•ą„‹ server ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤®ą¤æą¤²ą¤¤ą¤¾ą„¤ यह ą¤…ą¤Ŗą¤Øą„‡ ą¤Æą„‹ą¤—ą¤¦ą¤¾ą¤Ø data ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ declare करता ą¤¹ą„ˆ; `MCPServer` ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ consume करता ą¤¹ą„ˆą„¤ mutate ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„‹ą¤ˆ `self.server` ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ -और `main()` इसका ą¤øą¤¬ą„‚ą¤¤ ą¤¹ą„ˆ, ą¤øą„€ą¤§ą„‡ `mcp` ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¾ ą¤ą¤• in-memory client: +ą¤‡ą¤øą„‡ HTTP पर serve ą¤•ą¤°ą„‡ą¤‚, और ą¤ą¤• client इसका ą¤øą¤¬ą„‚ą¤¤ ą¤¹ą„ˆ: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +इस page ą¤•ą„€ हर `server.py` ą¤‡ą¤øą„€ command ą¤øą„‡ serve ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, और हर `client.py` ą¤‰ą¤øą¤•ą„‡ साऄ-साऄ ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ `python client.py` ą¤øą„‡ ą¤šą¤²ą¤¤ą„€ ą¤¹ą„ˆą„¤ + ### ą¤…ą¤Ŗą¤Øą„‡ methods serve करना {#serving-your-own-methods} extension **ą¤Øą¤ request methods** register कर सकता ą¤¹ą„ˆ: ą¤‰ą¤øą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ verbs, ą¤œą„‹ spec ą¤•ą„‡ verbs ą¤•ą„‡ साऄ-साऄ serve ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ methods **ą¤øą¤–ą¤¼ą„ą¤¤ą„€ ą¤øą„‡ additive** ą¤¹ą„ˆą¤‚ą„¤ SDK ą¤‡ą¤øą„‡ constr ### Client side {#the-client-side} -ą¤‰ą¤øą„€ file का `main()` ą¤¹ą„€ client ą¤•ą„€ ą¤Ŗą„‚ą¤°ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ ą¤¹ą„ˆ, ą¤‰ą¤øą¤•ą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤¹ą¤æą¤øą„ą¤øą„‡: +client अपना अलग program ą¤¹ą„ˆ, और client ą¤•ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ ą¤•ą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤¹ą¤æą¤øą„ą¤øą„‡ ą¤‡ą¤øą„€ ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` extension declare करता ą¤¹ą„ˆą„¤ ą¤Æą„‡ declarations `ClientCapabilities.extensions` बन ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą¤‚: 2026-07-28 connection पर यह map हर request ą¤•ą„‡ `_meta` envelope ą¤®ą„‡ą¤‚ जाता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ server ą¤‡ą¤øą„‡ **हर** request पर ą¤¦ą„‡ą¤–ą¤¤ą¤¾ ą¤¹ą„ˆ; legacy connection पर यह `initialize` handshake ą¤•ą„‡ साऄ जाता ą¤¹ą„ˆą„¤ server code ą¤•ą„‹ ą¤«ą¤¼ą¤°ą„ą¤• ą¤Øą¤¹ą„€ą¤‚ पऔ़ता कि ą¤•ą„Œą¤Ø सा: `require_client_extension(ctx, ...)` और `ctx.session.check_client_capability(...)` ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤°ą¤¾ą¤øą„ą¤¤ą„‹ą¤‚ पर ą¤øą¤¹ą„€ ą¤øą„ą¤°ą„‹ą¤¤ ą¤Ŗą¤¢ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * vendor methods ą¤ą¤• परत ą¤Øą„€ą¤šą„‡ `client.session.send_request(...)` पर ą¤‰ą¤¤ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; `Client` ą¤øą¤æą¤°ą„ą¤«ą¤¼ spec verbs ą¤•ą„‡ ą¤²ą¤æą¤ first-class methods ą¤œą„‹ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ `send_request` ą¤•ą„‹ą¤ˆ ą¤­ą„€ `Request` subclass ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° करता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ vendor request ą¤œą„ˆą¤øą„€ ą¤¹ą„ˆ ą¤µą„ˆą¤øą„€ ą¤¹ą„€ ą¤šą¤²ą„€ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą„¤ +* `SearchRequest` और ą¤‰ą¤øą¤•ą„‡ साऄ ą¤œą¤¾ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ ą¤¦ą„‹ models extension का wire contract ą¤¹ą„ˆą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ client ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ ą¤…ą¤Ŗą¤Øą„‡ ą¤²ą¤æą¤ ą¤–ą„ą¤¦ declare करता ą¤¹ą„ˆą„¤ published extension ą¤‡ą¤Øą„ą¤¹ą„‡ą¤‚ ą¤ą¤øą„‡ package ą¤®ą„‡ą¤‚ ą¤¦ą„‡ą¤—ą¤¾ ą¤œą¤æą¤øą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą¤•ą„ą¤· import ą¤•ą¤°ą„‡ą¤‚ą„¤ ### `tools/call` ą¤•ą„‹ intercept करना {#intercepting-toolscall} @@ -109,12 +116,18 @@ hook `tools/call` ą¤•ą„‹ wrap करता ą¤¹ą„ˆ, और ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ## client extension ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² करना {#using-a-client-extension} -**client extension** ą¤µą¤¹ą„€ contract ą¤¹ą„ˆ, ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ ą¤Ŗą¤•ą„ą¤· ą¤øą„‡: ą¤ą¤• identifier ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ client-side behaviour का bundleą„¤ instances ą¤•ą„‹ `Client(extensions=[...])` ą¤®ą„‡ą¤‚ पास ą¤•ą¤°ą„‡ą¤‚ और tools ą¤øą¤¾ą¤®ą¤¾ą¤Øą„ą¤Æ ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤øą„‡ call ą¤•ą¤°ą„‡ą¤‚: +**client extension** ą¤µą¤¹ą„€ contract ą¤¹ą„ˆ, ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ ą¤Ŗą¤•ą„ą¤· ą¤øą„‡: ą¤ą¤• identifier ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ client-side behaviour का bundleą„¤ यहाँ server `buy` का जवाब सामान ą¤•ą„‡ बजाय redeem ą¤•ą¤°ą¤Øą„‡ लायक receipt ą¤øą„‡ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, और ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤‰ą¤øą„€ client ą¤•ą„‹ ą¤œą¤æą¤øą¤Øą„‡ extension declare किया ą¤¹ą„‹: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +client पर, instances ą¤•ą„‹ `Client(extensions=[...])` ą¤®ą„‡ą¤‚ पास ą¤•ą¤°ą„‡ą¤‚ और tools ą¤øą¤¾ą¤®ą¤¾ą¤Øą„ą¤Æ ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤øą„‡ call ą¤•ą¤°ą„‡ą¤‚: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` हर ą¤¦ą„‚ą¤øą¤°ą„‡ call ą¤•ą„€ तरह सादा `CallToolResult` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ extension ą¤Øą„‡ ą¤œą„‹ बदला: server अब `buy` का जवाब final result ą¤•ą„‡ बजाय `receipt` **result shape** ą¤øą„‡ ą¤¦ą„‡ सकता ą¤¹ą„ˆ, और `call_tool` ą¤•ą„‡ ą¤²ą„Œą¤Ÿą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ `Receipts` ą¤‰ą¤øą„‡ ą¤Ŗą„‚ą¤°ą¤¾ कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ (यहाँ follow-up call ą¤øą„‡ receipt redeem ą¤•ą¤°ą¤•ą„‡)ą„¤ call site ą¤®ą„‡ą¤‚ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą¤æą¤²ą¤¤ą¤¾ą„¤ extension हटा ą¤¦ą„‡ą¤‚ ą¤¤ą„‹ ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą„ą¤› ą¤­ą„€ ą¤®ą„Œą¤œą„‚ą¤¦ ą¤Øą¤¹ą„€ą¤‚: server का gate उस client ą¤•ą„‹ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤Øą„‡ ą¤‡ą¤øą„‡ declare ą¤Øą¤¹ą„€ą¤‚ किया (error -32021), और gate ą¤›ą„‹ą¤”ą¤¼ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ server ą¤øą„‡ आया claimed shape validation ą¤®ą„‡ą¤‚ fail ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤œą„ˆą¤øą„‡ spec ą¤…ą¤Øą¤œą¤¾ą¤Ø `resultType` ą¤•ą„‡ ą¤²ą¤æą¤ माँगता ą¤¹ą„ˆą„¤ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ बंद, wire ą¤•ą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤øą¤æą¤°ą„‹ą¤‚ ą¤Ŗą¤°ą„¤ @@ -124,15 +137,15 @@ extension हटा ą¤¦ą„‡ą¤‚ ą¤¤ą„‹ ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą„ą¤› ą¤­ą„€ म ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## client extension लिखना {#writing-a-client-extension} `ClientExtension` ą¤•ą„‹ subclass ą¤•ą¤°ą„‡ą¤‚ और ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤µą¤¹ą„€ override ą¤•ą¤°ą„‡ą¤‚ ą¤œą¤æą¤øą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤¹ą„‹ą„¤ ą¤Æą„‹ą¤—ą¤¦ą¤¾ą¤Ø ą¤•ą„‡ ą¤¤ą„€ą¤Ø ą¤Ŗą„ą¤°ą¤•ą¤¾ą¤°, हर ą¤ą¤• का default: `settings()`, `claims()` और `notifications()`ą„¤ -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * identifier ą¤µą¤¹ą„€ grammar मानता ą¤¹ą„ˆ ą¤œą„‹ server का, और class define ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„€ validate ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ @@ -153,12 +166,18 @@ handler ą¤•ą„‹ validated params ą¤ą¤•-ą¤ą¤• ą¤•ą¤°ą¤•ą„‡, dispatch ą¤•ą„‡ क ### Extension verbs {#extension-verbs} -extension ą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ request methods ą¤•ą„‹ client-side registration ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤Øą¤¹ą„€ą¤‚ą„¤ vendor request type `mcp.types.Request` ą¤•ą„‹ subclass करता ą¤¹ą„ˆ और `client.session.send_request` ą¤øą„‡ जाता ą¤¹ą„ˆ, ą¤œą„ˆą¤øą¤¾ [ą¤…ą¤Ŗą¤Øą„‡ methods serve करना](#serving-your-own-methods) ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ ą¤ą¤• बात और: जब ą¤•ą¤æą¤øą„€ params key का `Mcp-Name` header ą¤®ą„‡ą¤‚ जाना ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„‹ (tasks ą¤œą„ˆą¤øą„‡ extension specs ą¤…ą¤Ŗą¤Øą„‡ verbs ą¤•ą„‡ ą¤²ą¤æą¤ यह ą¤®ą¤¾ą¤ą¤—ą¤¤ą„‡ ą¤¹ą„ˆą¤‚), ą¤¤ą„‹ request type `name_param` declare करता ą¤¹ą„ˆ: +extension ą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ request methods ą¤•ą„‹ client-side registration ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤Øą¤¹ą„€ą¤‚ą„¤ vendor request type `mcp.types.Request` ą¤•ą„‹ subclass करता ą¤¹ą„ˆ और `client.session.send_request` ą¤øą„‡ जाता ą¤¹ą„ˆ, ą¤œą„ˆą¤øą¤¾ [ą¤…ą¤Ŗą¤Øą„‡ methods serve करना](#serving-your-own-methods) ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ ऐसा server ą¤²ą„‡ą¤‚ ą¤œą¤æą¤øą¤•ą¤¾ extension ą¤ą¤• named job ą¤•ą„‡ ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ ą¤ą¤• verb serve करता ą¤¹ą„ˆ: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +client पर ą¤ą¤• बात और ą¤œą„ą¤”ą¤¼ą¤¤ą„€ ą¤¹ą„ˆ: जब ą¤•ą¤æą¤øą„€ params key का `Mcp-Name` header ą¤®ą„‡ą¤‚ जाना ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„‹ (tasks ą¤œą„ˆą¤øą„‡ extension specs ą¤…ą¤Ŗą¤Øą„‡ verbs ą¤•ą„‡ ą¤²ą¤æą¤ यह ą¤®ą¤¾ą¤ą¤—ą¤¤ą„‡ ą¤¹ą„ˆą¤‚), ą¤¤ą„‹ request type `name_param` declare करता ą¤¹ą„ˆ: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + session हर send path पर `params["jobId"]` ą¤•ą„‹ `Mcp-Name` ą¤®ą„‡ą¤‚ mirror करता ą¤¹ą„ˆ, और value न ą¤¹ą„‹ą¤Øą„‡ पर ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ header ą¤šą„ą¤Ŗą¤šą¤¾ą¤Ŗ ą¤›ą„‹ą¤”ą¤¼ą¤Øą„‡ ą¤•ą„‡ बजाय साफ़ ą¤¤ą„Œą¤° पर fail ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ## extension ą¤•ą„ą¤Æą¤¾ ą¤Øą¤¹ą„€ą¤‚ कर सकता {#what-an-extension-cannot-do} diff --git a/i18n/hi/pages/advanced/low-level-server.md b/i18n/hi/pages/advanced/low-level-server.md index d2032b78..90f0b3a1 100644 --- a/i18n/hi/pages/advanced/low-level-server.md +++ b/i18n/hi/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # Low-level Server {#the-low-level-server} @@ -25,9 +25,9 @@ translation: ą¤¤ą„€ą¤Ø ą¤šą„€ą¤œą¤¼ą„‡ą¤‚ ą¤¬ą¤¦ą¤²ą„€ą¤‚, और ą¤Ŗą„‚ą¤°ą¤¾ low-level API बस ą¤Æą¤¹ą„€ ą¤¹ą„ˆ: -* **Handlers constructor parameters ą¤¹ą„ˆą¤‚ą„¤** `on_list_tools=` और `on_call_tool=` `Server(...)` ą¤®ą„‡ą¤‚ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ यहाँ ą¤Øą„€ą¤šą„‡ ą¤•ą„‹ą¤ˆ decorator ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, और हर handler का आकार ą¤ą¤• ą¤¹ą„€ ą¤¹ą„ˆ: `async (ctx, params) -> result`ą„¤ -* **Input schema आप ą¤²ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** `Tool.input_schema` ą¤ą¤• सादा JSON Schema `dict` ą¤¹ą„ˆą„¤ ą¤•ą„‹ą¤ˆ ą¤‡ą¤øą„‡ type hints ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ निकालता, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤Øą¤æą¤•ą¤¾ą¤²ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ type hints ą¤¹ą„ˆą¤‚ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ -* **Result आप ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** `CallToolResult(content=[TextContent(...)])`, हाऄ ą¤øą„‡ą„¤ न ą¤•ą„ą¤› wrap ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, न convert, न return annotation ą¤øą„‡ ą¤…ą¤Øą„ą¤®ą¤¾ą¤Ø लगाया जाता ą¤¹ą„ˆą„¤ +* **handlers constructor parameters ą¤¹ą„ˆą¤‚ą„¤** `on_list_tools=` और `on_call_tool=` `Server(...)` ą¤®ą„‡ą¤‚ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ यहाँ ą¤Øą„€ą¤šą„‡ ą¤•ą„‹ą¤ˆ decorator ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, और हर handler का आकार ą¤ą¤• ą¤¹ą„€ ą¤¹ą„ˆ: `async (ctx, params) -> result`ą„¤ +* **input schema आप ą¤²ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** `Tool.input_schema` ą¤ą¤• सादा JSON Schema `dict` ą¤¹ą„ˆą„¤ ą¤•ą„‹ą¤ˆ ą¤‡ą¤øą„‡ type hints ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ निकालता, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤Øą¤æą¤•ą¤¾ą¤²ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ type hints ą¤¹ą„ˆą¤‚ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ +* **result आप ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** `CallToolResult(content=[TextContent(...)])`, हाऄ ą¤øą„‡ą„¤ न ą¤•ą„ą¤› wrap ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, न convert, न return annotation ą¤øą„‡ ą¤…ą¤Øą„ą¤®ą¤¾ą¤Ø लगाया जाता ą¤¹ą„ˆą„¤ `params` parse ą¤•ą„€ ą¤¹ą„ą¤ˆ request ą¤¹ą„ˆ: `CallToolRequestParams` ą¤†ą¤Ŗą¤•ą„‹ `.name` और `.arguments` ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ `ctx` ą¤ą¤• `ServerRequestContext` ą¤¹ą„ˆ: client ą¤øą„‡ वापस बात ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ `ctx.session`, `ctx.lifespan_context`, `ctx.request_id`, और `ctx.meta`, ą¤Æą¤¾ą¤Øą„€ request का ą¤†ą¤Øą„‡ वाला `_meta`ą„¤ @@ -36,18 +36,22 @@ translation: ### ą¤‡ą¤øą„‡ ą¤†ą¤œą¤¼ą¤®ą¤¾ą¤ą¤ {#try-it} -ą¤‡ą¤øą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„‹ą¤ˆ Inspector ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ: `mcp dev` और `mcp run` ą¤øą¤æą¤°ą„ą¤«ą¤¼ `MCPServer` ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ In-memory `Client` ą¤•ą„‹ ą¤•ą„‹ą¤ˆ ą¤«ą¤¼ą¤°ą„ą¤• ą¤Øą¤¹ą„€ą¤‚ पऔ़ता; वह low-level `Server` ą¤•ą„‹ ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ `MCPServer` ą¤•ą„‹: +`mcp dev` और `mcp run` ą¤øą¤æą¤°ą„ą¤«ą¤¼ `MCPServer` ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ ą¤‡ą¤øą„‡ serve आप ą¤–ą„ą¤¦ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ `server.py` ą¤•ą„€ ą¤†ą¤–ą¤¼ą¤æą¤°ą„€ line ą¤‡ą¤øą¤øą„‡ ą¤ą¤• साधारण ASGI app ą¤¬ą¤Øą¤¾ą¤¤ą„€ ą¤¹ą„ˆ, और uvicorn ą¤‰ą¤øą„‡ चलाता ą¤¹ą„ˆ: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector ą¤•ą„‹, या ą¤•ą¤æą¤øą„€ ą¤­ą„€ client ą¤•ą„‹, `http://localhost:8000/mcp` पर point ą¤•ą¤°ą„‡ą¤‚: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -61,8 +65,10 @@ asyncio.run(main()) ą¤µą¤¹ą„€ text ą¤œą„‹ `@mcp.tool()` ą¤µą¤¾ą¤²ą„‡ version ą¤Øą„‡ दिया ą¤„ą¤¾ą„¤ ą¤¦ą„‹ ą¤…ą¤øą¤²ą„€ अंतर: -* `result.structured_content` `None` ą¤¹ą„ˆą„¤ High-level server ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ `-> str` ą¤•ą„‹ `{"result": ...}` ą¤®ą„‡ą¤‚ wrap कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; यहाँ ą¤œą„‹ ą¤†ą¤Ŗą¤Øą„‡ ą¤Øą¤¹ą„€ą¤‚ बनाया, ą¤‰ą¤øą„‡ ą¤•ą„‹ą¤ˆ ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤Øą¤¾ą¤¤ą¤¾ą„¤ -* `list_tools` ą¤µą¤¹ą„€ schema ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ **ą¤†ą¤Ŗą¤Øą„‡** type किया, ą¤…ą¤•ą„ą¤·ą¤°-दर-ą¤…ą¤•ą„ą¤·ą¤°ą„¤ High-level version ą¤®ą„‡ą¤‚ हर property पर `"title": "Query"` ऄा और root पर `"title": "search_booksArguments"`: Pydantic ą¤•ą„€ ą¤¦ą„‡ą¤Øą„¤ यहाँ ą¤Øą„€ą¤šą„‡, अगर ą¤•ą„ą¤› wire पर ą¤¹ą„ˆ, ą¤¤ą„‹ ą¤‰ą¤øą„‡ वहाँ ą¤†ą¤Ŗą¤Øą„‡ रखा ą¤¹ą„ˆą„¤ +* `result.structured_content` `None` ą¤¹ą„ˆą„¤ high-level server ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ `-> str` ą¤•ą„‹ `{"result": ...}` ą¤®ą„‡ą¤‚ wrap कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; यहाँ ą¤œą„‹ ą¤†ą¤Ŗą¤Øą„‡ ą¤Øą¤¹ą„€ą¤‚ बनाया, ą¤‰ą¤øą„‡ ą¤•ą„‹ą¤ˆ ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤Øą¤¾ą¤¤ą¤¾ą„¤ +* `list_tools` ą¤µą¤¹ą„€ schema ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ **ą¤†ą¤Ŗą¤Øą„‡** type किया, ą¤…ą¤•ą„ą¤·ą¤°-दर-ą¤…ą¤•ą„ą¤·ą¤°ą„¤ high-level version ą¤®ą„‡ą¤‚ हर property पर `"title": "Query"` ऄा और root पर `"title": "search_booksArguments"`: Pydantic ą¤•ą„€ ą¤¦ą„‡ą¤Øą„¤ यहाँ ą¤Øą„€ą¤šą„‡, अगर ą¤•ą„ą¤› wire पर ą¤¹ą„ˆ, ą¤¤ą„‹ ą¤‰ą¤øą„‡ वहाँ ą¤†ą¤Ŗą¤Øą„‡ रखा ą¤¹ą„ˆą„¤ + +test ą¤®ą„‡ą¤‚ आप uvicorn और port ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: `Client(server)` low-level `Server` ą¤•ą„‹ in-process ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ `MCPServer` ą¤•ą„‹, और **[Testing](../get-started/testing.md)** ą¤Æą¤¹ą„€ pattern ą¤¹ą„ˆą„¤ ## ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„ą¤› जाँचा ą¤Øą¤¹ą„€ą¤‚ जाता {#nothing-is-checked-for-you} @@ -207,12 +213,12 @@ Handshake runner का ą¤¹ą„ˆą„¤ `server/discover`, `ping`, और ą¤¬ą¤¾ą¤•ą„€ ## सारांश {#recap} -* Low-level `Server` ą¤…ą¤Ŗą¤Øą„‡ handlers `on_*` **constructor parameters** ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; हर handler `async (ctx, params) -> result` ą¤¹ą„ˆą„¤ +* low-level `Server` ą¤…ą¤Ŗą¤Øą„‡ handlers `on_*` **constructor parameters** ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; हर handler `async (ctx, params) -> result` ą¤¹ą„ˆą„¤ * `input_schema` dict आप ą¤²ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और `CallToolResult` आप ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ न ą¤•ą„ą¤› derive ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, न wrap, न validateą„¤ -* Handler ą¤®ą„‡ą¤‚ exception `-32603` protocol error ą¤¹ą„ˆą„¤ जिस tool error ą¤•ą„‹ model पढ़ ą¤øą¤•ą„‡, वह `is_error=True` वाला `CallToolResult` ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ **आप** ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ -* Result पर `_meta` client application ą¤•ą„‡ नाम ą¤¹ą„ˆ, model ą¤•ą„‡ ą¤Øą¤¹ą„€ą¤‚ą„¤ +* handler ą¤®ą„‡ą¤‚ exception `-32603` protocol error ą¤¹ą„ˆą„¤ जिस tool error ą¤•ą„‹ model पढ़ ą¤øą¤•ą„‡, वह `is_error=True` वाला `CallToolResult` ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ **आप** ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +* result पर `_meta` client application ą¤•ą„‡ नाम ą¤¹ą„ˆ, model ą¤•ą„‡ ą¤Øą¤¹ą„€ą¤‚ą„¤ * `Server[T]` उस ą¤šą„€ą¤œą¤¼ ą¤®ą„‡ą¤‚ generic ą¤¹ą„ˆ ą¤œą„‹ उसका lifespan yield करता ą¤¹ą„ˆ; `ctx.lifespan_context` ą¤ą¤• typed `T` ą¤¹ą„ˆą„¤ * `add_request_handler(method, params_type, handler)` ą¤•ą„‹ą¤ˆ ą¤­ą„€ method serve करता ą¤¹ą„ˆą„¤ `initialize` reserved ą¤¹ą„ˆą„¤ * `Server` ą¤œą„‹ capabilities advertise करता ą¤¹ą„ˆ, ą¤µą„‡ ą¤‡ą¤øą¤øą„‡ ą¤Øą¤æą¤•ą¤²ą¤¤ą„€ ą¤¹ą„ˆą¤‚ कि ą¤†ą¤Ŗą¤Øą„‡ ą¤•ą„Œą¤Ø ą¤øą„‡ handlers register ą¤•ą¤æą¤ą„¤ -`Client(server)` ą¤Øą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ servers ą¤•ą„‡ साऄ ą¤ą¤• ą¤œą„ˆą¤øą¤¾ ą¤¬ą¤°ą„ą¤¤ą¤¾ą¤µ किया ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤µą„‡ ą¤ą¤• ą¤¹ą„€ protocol **ą¤¹ą„ˆą¤‚**, और ą¤Æą¤¹ą„€ ą¤…ą¤øą¤²ą„€ बात ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤øą„‡ ą¤Øą„€ą¤šą„‡ ą¤•ą„€ ą¤…ą¤—ą¤²ą„€ layer ą¤•ą„‹ą¤ˆ class ą¤¹ą„ˆ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚: वह **[Middleware](middleware.md)** ą¤¹ą„ˆą„¤ +client ą¤Øą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ servers ą¤•ą„‡ साऄ ą¤ą¤• ą¤œą„ˆą¤øą¤¾ ą¤¬ą¤°ą„ą¤¤ą¤¾ą¤µ किया ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤µą„‡ ą¤ą¤• ą¤¹ą„€ protocol **ą¤¹ą„ˆą¤‚**, और ą¤Æą¤¹ą„€ ą¤…ą¤øą¤²ą„€ बात ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤øą„‡ ą¤Øą„€ą¤šą„‡ ą¤•ą„€ ą¤…ą¤—ą¤²ą„€ layer ą¤•ą„‹ą¤ˆ class ą¤¹ą„ˆ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚: वह **[Middleware](middleware.md)** ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/advanced/pagination.md b/i18n/hi/pages/advanced/pagination.md index 31096406..6bd3fa88 100644 --- a/i18n/hi/pages/advanced/pagination.md +++ b/i18n/hi/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Pagination {#pagination} @@ -31,9 +31,13 @@ Pagination उस server ą¤•ą„‡ ą¤²ą¤æą¤ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„€ resource list ### ą¤‡ą¤øą„‡ ą¤†ą¤œą¤¼ą¤®ą¤¾ą¤ą¤ {#try-it} -`Client(server)` memory ą¤®ą„‡ą¤‚ low-level `Server` ą¤øą„‡ ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ `MCPServer` ą¤øą„‡ą„¤ +`mcp run` ą¤øą¤æą¤°ą„ą¤«ą¤¼ `MCPServer` ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° करता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ ą¤‡ą¤øą„‡ आप ą¤–ą¤¼ą„ą¤¦ serve ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ `server.py` ą¤•ą„€ ą¤†ą¤–ą¤¼ą¤æą¤°ą„€ line `Server` ą¤øą„‡ ą¤ą¤• साधारण ASGI app ą¤¬ą¤Øą¤¾ą¤¤ą„€ ą¤¹ą„ˆ, और uvicorn ą¤‰ą¤øą„€ ą¤•ą„‹ चलाता ą¤¹ą„ˆ: -बिना arguments ą¤•ą„‡ `list_resources()` call ą¤•ą¤°ą„‡ą¤‚ą„¤ ą¤†ą¤Ŗą¤•ą„‹ दस resources ą¤®ą¤æą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `book-1` ą¤øą„‡ `book-10` तक, और `next_cursor` string `"10"` ą¤¹ą„ˆą„¤ +```console +uvicorn server:app --port 8000 +``` + +ą¤•ą¤æą¤øą„€ ą¤­ą„€ client (**[The Client](../client/index.md)**, या Inspector) ą¤•ą„‹ `http://localhost:8000/mcp` ą¤•ą„€ ओर point ą¤•ą¤°ą„‡ą¤‚ और बिना arguments ą¤•ą„‡ `list_resources()` call ą¤•ą¤°ą„‡ą¤‚ą„¤ ą¤†ą¤Ŗą¤•ą„‹ दस resources ą¤®ą¤æą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `book-1` ą¤øą„‡ `book-10` तक, और `next_cursor` string `"10"` ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ `list_resources(cursor="10")` ą¤øą„‡ वापस ą¤¦ą„‡ą¤‚, ą¤¤ą„‹ पहला resource `book-11` ą¤¹ą„ˆ और नया `next_cursor` `"20"` ą¤¹ą„ˆą„¤ @@ -43,7 +47,7 @@ Pagination उस server ą¤•ą„‡ ą¤²ą¤æą¤ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„€ resource list `Client` का हर `list_*` method (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) `cursor=` keyword ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ Paged list ą¤•ą„‹ ą¤Ŗą„‚ą¤°ą¤¾ ą¤–ą„€ą¤‚ą¤šą¤Øą¤¾ ą¤ą¤• `while True` ą¤¹ą„ˆ: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Pagination उस server ą¤•ą„‡ ą¤²ą¤æą¤ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„€ resource list * `next_cursor` ą¤¦ą„‡ą¤–ą¤Øą„‡ ą¤øą„‡ **ą¤Ŗą¤¹ą¤²ą„‡** extend ą¤•ą¤°ą„‡ą¤‚: ą¤†ą¤–ą¤¼ą¤æą¤°ą„€ page ą¤®ą„‡ą¤‚ ą¤­ą„€ resources ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * `next_cursor is None` ą¤¹ą„€ बाहर ą¤Øą¤æą¤•ą¤²ą¤Øą„‡ का ą¤°ą¤¾ą¤øą„ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤¬ą¤¾ą¤•ą„€ ą¤•ą„ą¤› ą¤­ą„€ ą¤øą„€ą¤§ą„‡ `cursor=` ą¤®ą„‡ą¤‚ वापस जाता ą¤¹ą„ˆ, बिना ą¤›ą„‡ą¤”ą¤¼ą„‡ą„¤ -इसका `main()` ą¤šą¤²ą¤¾ą¤ą¤ और यह `100 resources` print करता ą¤¹ą„ˆ: दस-दस ą¤•ą„‡ दस pages, ą¤ą¤• ą¤ą¤øą„‡ loop ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą„‡ ą¤¹ą„ą¤ ą¤œą¤æą¤øą„‡ ą¤•ą¤­ą„€ पता ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ ऄा कि दस pages ą¤„ą„‡ą„¤ +uvicorn अब ą¤­ą„€ `server.py` serve कर रहा ą¤¹ą„‹, तब ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤®ą„‡ą¤‚ `python client.py` ą¤šą¤²ą¤¾ą¤ą¤ą„¤ यह `100 resources` print करता ą¤¹ą„ˆ: दस-दस ą¤•ą„‡ दस pages, ą¤ą¤• ą¤ą¤øą„‡ loop ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą„‡ ą¤¹ą„ą¤ ą¤œą¤æą¤øą„‡ ą¤•ą¤­ą„€ पता ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ ऄा कि दस pages ą¤„ą„‡ą„¤ यह ą¤µą¤¹ą„€ loop ą¤¹ą„ˆ ą¤œą„‹ **[The Client](../client/index.md)** हर `list_*` verb ą¤•ą„‡ ą¤²ą¤æą¤ दिखाता ą¤¹ą„ˆ, और paging न ą¤•ą¤°ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ server पर ą¤‡ą¤øą¤•ą„€ ą¤•ą„‹ą¤ˆ ą¤•ą¤¼ą„€ą¤®ą¤¤ ą¤Øą¤¹ą„€ą¤‚: ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ response ą¤®ą„‡ą¤‚ `next_cursor` `None` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और loop ą¤ą¤• बार चलता ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/client/caching.md b/i18n/hi/pages/client/caching.md index dd239cb2..e22005c4 100644 --- a/i18n/hi/pages/client/caching.md +++ b/i18n/hi/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Caching hints {#caching-hints} @@ -30,7 +30,7 @@ server ą¤–ą„ą¤¦ ą¤•ą„ą¤› ą¤­ą„€ cache ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ ą¤Æą„‡ fiel low-level `Server` पर handlers ą¤…ą¤Ŗą¤Øą„‡ results ą¤–ą„ą¤¦ ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और `ttl_ms` / `cache_scope` result models पर बस fields ą¤¹ą„ˆą¤‚ą„¤ ą¤œą„‹ handler ą¤‡ą¤Øą„ą¤¹ą„‡ą¤‚ explicitly set करता ą¤¹ą„ˆ, वह constructor map पर ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤­ą¤¾ą¤°ą„€ पऔ़ता ą¤¹ą„ˆ, field दर field: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ paginated lists पर ą¤ą¤• ą¤øą¤¾ą¤µą¤§ą¤¾ą¤Øą„€: protocol ą¤•ą„€ माँ 2026-07-28 session पर `Client` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ hints का पालन करता ą¤¹ą„ˆ: ą¤‡ą¤øą¤®ą„‡ą¤‚ built-in response cache ą¤¹ą„ˆ, ą¤œą„‹ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ ą¤šą¤¾ą¤²ą„‚ रहता ą¤¹ą„ˆą„¤ ą¤œą„‹ result `ttlMs` ą¤•ą„‡ साऄ आता ą¤¹ą„ˆ, वह store ą¤¹ą„‹ जाता ą¤¹ą„ˆ, और उस TTL ą¤•ą„‡ ą¤­ą„€ą¤¤ą¤° ą¤µą„ˆą¤øą¤¾ ą¤¹ą„€ call cache ą¤øą„‡ serve ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, बिना round trip ą¤•ą„‡ą„¤ जिस result ą¤®ą„‡ą¤‚ **ą¤•ą„‹ą¤ˆ** hint ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾, वह cache ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾: बिना hint ą¤µą¤¾ą¤²ą„‡ results ą¤•ą„‹ `CacheConfig.default_ttl_ms` मिलता ą¤¹ą„ˆ, ą¤œą¤æą¤øą¤•ą¤¾ default `0` ą¤¹ą„ˆ (ą¤¤ą„ą¤°ą¤‚ą¤¤ stale), ą¤‡ą¤øą¤²ą¤æą¤ ą¤œą„‹ server ą¤•ą„ą¤› ą¤­ą„€ declare ą¤Øą¤¹ą„€ą¤‚ करता, ą¤‰ą¤øą„‡ ą¤ ą„€ą¤• ą¤µą„ˆą¤øą¤¾ ą¤¹ą„€ call-दर-call traffic दिखता ą¤¹ą„ˆ ą¤œą„ˆą¤øą¤¾ ą¤¹ą¤®ą„‡ą¤¶ą¤¾ दिखता ą¤„ą¤¾ą„¤ -```python title="client.py" hl_lines="33 35 38" +ą¤‡ą¤øą„‡ ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ą¤ ą¤¦ą„‡ą¤–ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤, ą¤Ŗą¤æą¤›ą¤²ą„‡ section ą¤•ą„€ `server.py` ą¤•ą„‹ uvicorn ą¤øą„‡ serve ą¤•ą¤°ą„‡ą¤‚ (ą¤‰ą¤øą¤•ą„€ ą¤†ą¤–ą¤æą¤°ą„€ line ASGI app ą¤¬ą¤Øą¤¾ą¤¤ą„€ ą¤¹ą„ˆ)ą„¤ handler जब ą¤­ą„€ सच ą¤®ą„‡ą¤‚ चलता ą¤¹ą„ˆ, ą¤ą¤• line print करता ą¤¹ą„ˆ: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ `python client.py` ą¤šą¤²ą¤¾ą¤ą¤ą„¤ यह ą¤µą„‡ hints print करता ą¤¹ą„ˆ ą¤œą„‹ ą¤Ŗą¤¹ą¤²ą„‡ result ą¤®ą„‡ą¤‚ ą¤†ą¤ ą¤„ą„‡, handler का `ttlMs` और ą¤‰ą¤øą¤•ą„‡ बगल ą¤®ą„‡ą¤‚ map का `cacheScope`: + +```text +1000 public +``` + +ą¤¬ą¤¾ą¤•ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ server का terminal बताता ą¤¹ą„ˆ: uvicorn ą¤•ą„‡ request logs ą¤•ą„‡ ą¤¬ą„€ą¤š `tools/list served` ą¤¤ą„€ą¤Ø बार दिखता ą¤¹ą„ˆą„¤ + चार calls, ą¤¤ą„€ą¤Ø fetchesą„¤ ą¤¦ą„‚ą¤øą¤°ą„‡ call ą¤•ą„‹ fresh entry ą¤®ą¤æą¤²ą„€ और वह server तक ą¤Ŗą¤¹ą„ą¤ą¤šą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚; (inject ą¤•ą„€ ą¤—ą¤ˆ) clock ą¤•ą„‹ TTL ą¤øą„‡ ą¤†ą¤—ą„‡ ą¤¬ą¤¢ą¤¼ą¤¾ą¤Øą„‡ पर ą¤¤ą„€ą¤øą¤°ą„‡ ą¤Øą„‡ फिर ą¤øą„‡ fetch किया; ą¤šą„Œą¤„ą„‡ ą¤Øą„‡ `cache_mode="refresh"` ą¤•ą¤¹ą¤¾ą„¤ यह kwarg ą¤Ŗą¤¾ą¤ą¤šą„‹ą¤‚ caching verbs पर ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆ (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (default) fresh entry ą¤¹ą„‹ ą¤¤ą„‹ ą¤‰ą¤øą„‡ serve करता ą¤¹ą„ˆ, और न ą¤¹ą„‹ ą¤¤ą„‹ fetch ą¤•ą¤°ą¤•ą„‡ store करता ą¤¹ą„ˆą„¤ @@ -56,7 +70,7 @@ paginated lists पर ą¤ą¤• ą¤øą¤¾ą¤µą¤§ą¤¾ą¤Øą„€: protocol ą¤•ą„€ माँ ą¤ą¤• नियम `"use"` ą¤øą„‡ ऊपर ą¤¹ą„ˆ: **`meta` ą¤µą¤¾ą¤²ą„‡ calls ą¤¹ą¤®ą„‡ą¤¶ą¤¾ server तक ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** जिस request ą¤®ą„‡ą¤‚ `meta` set ą¤¹ą„‹ (progress token, tracing fields), ą¤‰ą¤øą„‡ wire request ą¤•ą„€ ą¤‰ą¤®ą„ą¤®ą„€ą¤¦ ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ `cache_mode="use"` ą¤®ą„‡ą¤‚ ą¤‰ą¤øą„‡ `"refresh"` माना जाता ą¤¹ą„ˆ: cache read ą¤›ą„‹ą¤”ą¤¼ दिया जाता ą¤¹ą„ˆ, और fetch किया गया result फिर ą¤­ą„€ cache ą¤•ą„€ entry ą¤•ą„€ ą¤œą¤—ą¤¹ ą¤²ą„‡ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ `"bypass"` और explicit `"refresh"` ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤•ą„€ तरह ą¤¹ą„€ ą¤¬ą¤°ą„ą¤¤ą¤¾ą¤µ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ -caching ą¤Ŗą„‚ą¤°ą„€ तरह बंद ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ `Client(server, cache=None)` ą¤øą„‡ construct ą¤•ą¤°ą„‡ą¤‚: हर call फिर ą¤øą„‡ round trip ą¤¹ą„ˆ, और `cache_mode`, ą¤­ą¤²ą„‡ ą¤¹ą„€ अब ą¤­ą„€ ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ +caching ą¤Ŗą„‚ą¤°ą„€ तरह बंद ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ `Client` construct ą¤•ą¤°ą¤¤ą„‡ समय `cache=None` pass ą¤•ą¤°ą„‡ą¤‚: हर call फिर ą¤øą„‡ round trip ą¤¹ą„ˆ, और `cache_mode`, ą¤­ą¤²ą„‡ ą¤¹ą„€ अब ą¤­ą„€ ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ scope का पालन ą¤­ą„€ ą¤…ą¤Ŗą¤Øą„‡ आप ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ: `"private"` entries cache ą¤•ą„‡ *partition* (ą¤Øą„€ą¤šą„‡ ą¤¦ą„‡ą¤–ą„‡ą¤‚) ą¤øą„‡ ą¤¬ą¤ą¤§ą„€ ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą¤‚, ą¤œą¤¬ą¤•ą¤æ `"public"` ą¤µą¤¾ą¤²ą„€ ą¤šą¤¾ą¤¹ą„‡ą¤‚ ą¤¤ą„‹ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ ą¤µą„ą¤Æą¤¾ą¤Ŗą¤• sharing ą¤šą„ą¤Ø ą¤øą¤•ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤ और जिन entries का नाम notifications ą¤²ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤ ą„€ą¤• ą¤‰ą¤Øą¤•ą„‡ ą¤²ą¤æą¤ **notifications TTL पर ą¤­ą¤¾ą¤°ą„€ ą¤Ŗą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚**: `list_changed` notification ą¤®ą„‡ą¤² ą¤–ą¤¾ą¤¤ą„€ cached listing ą¤•ą„‹ evict कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, और `resources/updated` ą¤ ą„€ą¤• ą¤‰ą¤øą„€ URI ą¤•ą„‡ तहत store ą¤•ą¤æą¤ ą¤—ą¤ cached read ą¤•ą„‹ evict करता ą¤¹ą„ˆ, ą¤šą¤¾ą¤¹ą„‡ ą¤µą„‡ ą¤•ą¤æą¤¤ą¤Øą„‡ ą¤­ą„€ fresh ą¤°ą¤¹ą„‡ ą¤¹ą„‹ą¤‚ą„¤ 2026-07-28 connection पर ą¤Æą„‡ notifications `subscriptions/listen` stream पर ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ą¤œą¤æą¤øą„‡ आप `client.listen(...)` ą¤øą„‡ ą¤–ą„‹ą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और eviction ą¤†ą¤Ŗą¤•ą„‡ watcher ą¤•ą„‹ event ą¤¦ą¤æą¤–ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤Ŗą„‚ą¤°ą¤¾ ą¤¹ą„‹ जाता ą¤¹ą„ˆ; **[Subscriptions](subscriptions.md)** ą¤µą¤¹ą„€ page ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/client/callbacks.md b/i18n/hi/pages/client/callbacks.md index c7f70ccf..6d3ff282 100644 --- a/i18n/hi/pages/client/callbacks.md +++ b/i18n/hi/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Client callbacks {#client-callbacks} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `Client(...)` call पर `mode="legacy"` असल ą¤®ą„‡ą¤‚ काम कर रहा ą¤¹ą„ˆą„¤ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ `Client(...)` modern protocol path negotiate करता ą¤¹ą„ˆ, और उस path ą¤®ą„‡ą¤‚ server-ą¤øą„‡-client requests ą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„‹ą¤ˆ back-channel ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ: आपका callback ą¤šą¤²ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ `ctx.elicit` fail ą¤¹ą„‹ जाता ą¤¹ą„ˆą„¤ यह transport तय ą¤Øą¤¹ą„€ą¤‚ करता; negotiated - protocol तय करता ą¤¹ą„ˆ, in-memory ą¤®ą„‡ą¤‚ ą¤­ą„€ और URL पर ą¤­ą„€ą„¤ जब ą¤­ą„€ ą¤†ą¤Ŗą¤•ą„‡ client ą¤•ą„‹ ą¤ą¤øą„€ ą¤•ą¤æą¤øą„€ request का जवाब ą¤¦ą„‡ą¤Øą¤¾ ą¤¹ą„‹, + protocol तय करता ą¤¹ą„ˆą„¤ जब ą¤­ą„€ ą¤†ą¤Ŗą¤•ą„‡ client ą¤•ą„‹ ą¤ą¤øą„€ ą¤•ą¤æą¤øą„€ request का जवाब ą¤¦ą„‡ą¤Øą¤¾ ą¤¹ą„‹, `mode="legacy"` तय ą¤•ą¤°ą„‡ą¤‚; इस page ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ का हर test ą¤Æą¤¹ą„€ करता ą¤¹ą„ˆą„¤ ą¤Ŗą„‚ą¤°ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ **[Protocol versions](../protocol-versions.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ 2026-07-28 session पर callback ą¤¬ą„‡ą¤•ą¤¾ą¤° ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾, ą¤‰ą¤øą„‡ input अलग ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤øą„‡ मिलता ą¤¹ą„ˆ: जब ą¤•ą„‹ą¤ˆ tool @@ -149,6 +149,6 @@ result.structured_content # {'result': ['elicitation']} * **callback register करना ą¤¹ą„€ capability ą¤˜ą„‹ą¤·ą¤æą¤¤ करना ą¤¹ą„ˆą„¤** ą¤‡ą¤øą¤•ą„‡ बिना SDK ą¤†ą¤Ŗą¤•ą„€ ओर ą¤øą„‡ server ą¤•ą„€ request ą¤ ą„ą¤•ą¤°ą¤¾ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और ą¤Ŗą„‚ą¤°ą¤¾ call `MCPError` ą¤•ą„‡ साऄ fail ą¤¹ą„‹ जाता ą¤¹ą„ˆą„¤ * server ą¤Ŗą„‚ą¤›ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ `ctx.session.check_client_capability(...)` ą¤øą„‡ पता कर ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * `sampling_callback` और `list_roots_callback` ą¤‡ą¤øą„€ तरह काम ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ą¤²ą„‡ą¤•ą¤æą¤Ø deprecated features ą¤•ą„‹ serve ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; modern servers ą¤‡ą¤Øą¤•ą„€ ą¤œą¤—ą¤¹ multi-round-trip requests ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ -* `logging_callback` और `message_handler` ą¤•ą„‹ notifications ą¤®ą¤æą¤²ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤ ą¤µą„‡ ą¤•ą„ą¤› ą¤˜ą„‹ą¤·ą¤æą¤¤ ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą„‡ą„¤ +* `logging_callback` और `message_handler` ą¤•ą„‹ notifications ą¤®ą¤æą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤µą„‡ ą¤•ą„ą¤› ą¤˜ą„‹ą¤·ą¤æą¤¤ ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą„‡ą„¤ -`Client(...)` का पहला argument transport object ą¤¹ą„ˆą„¤ हर ą¤Ŗą„ą¤°ą¤•ą¤¾ą¤° ą¤•ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ **[Client transports](transports.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ +`Client(...)` का पहला argument transport ą¤šą„ą¤Øą¤¤ą¤¾ ą¤¹ą„ˆą„¤ हर ą¤Ŗą„ą¤°ą¤•ą¤¾ą¤° ą¤•ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ **[Client transports](transports.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/client/index.md b/i18n/hi/pages/client/index.md index bc2a393f..eb85cb86 100644 --- a/i18n/hi/pages/client/index.md +++ b/i18n/hi/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## आपका पहला client {#your-first-client} -```python title="client.py" hl_lines="14-18" +client ą¤•ą„‹ बात ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ server ą¤šą¤¾ą¤¹ą¤æą¤ą„¤ इस page का हर उदाहरण ą¤‡ą¤øą„€ Bookshop ą¤øą„‡ connect करता ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ `server.py` ą¤•ą„‡ नाम ą¤øą„‡ save ą¤•ą¤°ą„‡ą¤‚ और HTTP पर चलता ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤‚: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -ऊपर वाला server ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤‡ą¤øą¤²ą¤æą¤ ą¤¹ą„ˆ ताकि connect ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„ą¤› ą¤¹ą„‹ą„¤ client ą¤µą„‡ पाँच highlighted lines ą¤¹ą„ˆą¤‚ą„¤ +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` ą¤•ą„‹ **server object ą¤¹ą„€** दिया गया ą¤¹ą„ˆą„¤ ą¤Æą¤¹ą„€ in-memory transport ą¤¹ą„ˆ: न subprocess, न port, न HTTPą„¤ इस page का हर उदाहरण, और आपका लिखा हर test, ą¤‡ą¤øą„€ तरह connect करता ą¤¹ą„ˆą„¤ +ą¤‡ą¤øą¤øą„‡ server `http://localhost:8000/mcp` पर serve ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ client अपना अलग program ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ `client.py` ą¤•ą„‡ नाम ą¤øą„‡ save ą¤•ą¤°ą„‡ą¤‚ और ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤®ą„‡ą¤‚ `python client.py` ą¤šą¤²ą¤¾ą¤ą¤: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` ą¤•ą„‹ **URL** दिया गया ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ यह ą¤…ą¤­ą„€ ą¤¶ą„ą¤°ą„‚ ą¤•ą¤æą¤ ą¤—ą¤ server ą¤øą„‡ Streamable HTTP पर connect करता ą¤¹ą„ˆą„¤ * `async with` ą¤¹ą„€ **lifecycle** ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤®ą„‡ą¤‚ enter ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„€ connect और negotiate ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ; बाहर ą¤Øą¤æą¤•ą¤²ą¤¤ą„‡ ą¤¹ą„€ disconnectą„¤ ą¤•ą„‹ą¤ˆ `connect()` / `close()` ą¤œą„‹ą¤”ą¤¼ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, और block ą¤–ą¤¤ą„ą¤® ą¤¹ą„‹ą¤Øą„‡ ą¤•ą„‡ बाद `Client` ą¤¦ą„‹ą¤¬ą¤¾ą¤°ą¤¾ ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ ą¤øą¤•ą¤¤ą¤¾ą„¤ * block ą¤•ą„‡ अंदर connection ą¤•ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ ą¤Ŗą¤¹ą¤²ą„‡ ą¤øą„‡ ą¤øą¤¾ą¤¦ą„€ properties ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ @@ -25,10 +35,10 @@ translation: `Client` ą¤ą¤• positional argument ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और ą¤‰ą¤øą¤•ą„‡ type ą¤øą„‡ transport तय करता ą¤¹ą„ˆ: -* `MCPServer` (या low-level `Server`) instance: **in-process** connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ -* URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, production वाला ą¤°ą¤¾ą¤øą„ą¤¤ą¤¾ą„¤ -* `StdioServerParameters`: वह command ą¤œą„‹ **subprocess** ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ launch ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, और ą¤œą¤æą¤øą¤øą„‡ ą¤‰ą¤øą¤•ą„‡ stdin और stdout ą¤•ą„‡ ą¤œą¤¼ą¤°ą¤æą¤ बात ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą„¤ +* URL string (`Client("http://localhost:8000/mcp")`): Streamable HTTP, वह transport ą¤œą¤æą¤øą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ आप deploy ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +* `StdioServerParameters`: वह command ą¤œą„‹ local **subprocess** ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ launch ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, और ą¤œą¤æą¤øą¤øą„‡ ą¤‰ą¤øą¤•ą„‡ stdin और stdout ą¤•ą„‡ ą¤œą¤¼ą¤°ą¤æą¤ बात ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą„¤ * **transport**: ą¤•ą„‹ą¤ˆ ą¤­ą„€ ą¤šą„€ą¤œą¤¼ ą¤œą¤æą¤øą„‡ आप `async with ... as (read, write)` कर ą¤øą¤•ą„‡ą¤‚, ą¤œą„ˆą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ HTTP client ą¤•ą„‡ ऊपर `streamable_http_client(url, http_client=...)`ą„¤ +* `MCPServer` (या low-level `Server`) instance: **in-process** connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, न subprocess, न portą„¤ यह tests ą¤•ą„‡ ą¤²ą¤æą¤ ą¤¹ą„ˆ, और **[Testing](../get-started/testing.md)** ą¤‡ą¤øą„€ पर ą¤†ą¤—ą„‡ बढ़ता ą¤¹ą„ˆą„¤ इस page ą¤•ą„€ ą¤¬ą¤¾ą¤•ą„€ हर ą¤šą„€ą¤œą¤¼ ą¤šą¤¾ą¤°ą„‹ą¤‚ ą¤®ą„‡ą¤‚ ą¤ą¤• ą¤œą„ˆą¤øą„€ ą¤¹ą„ˆą„¤ Headers, subprocesses, timeouts और `Transport` protocol का अपना अलग page ą¤¹ą„ˆ: **[Client transports](transports.md)**ą„¤ @@ -49,11 +59,11 @@ translation: ## tools ą¤•ą„€ ą¤øą„‚ą¤šą„€ ą¤²ą„‡ą¤Øą¤¾ {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` ą¤ą¤• `ListToolsResult` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ; tools `.tools` ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ हर ą¤ą¤• वह ą¤Ŗą„‚ą¤°ą„€ definition ą¤¹ą„ˆ ą¤œą„‹ host ą¤•ą¤æą¤øą„€ model ą¤•ą„‹ ą¤¦ą„‡ą¤—ą¤¾: +`list_tools()` ą¤ą¤• `ListToolsResult` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ; tools `.tools` ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ हर ą¤ą¤• वह ą¤Ŗą„‚ą¤°ą„€ definition ą¤¹ą„ˆ ą¤œą„‹ host ą¤•ą¤æą¤øą„€ model ą¤•ą„‹ ą¤¦ą„‡ą¤—ą¤¾ą„¤ यह पहला ą¤¹ą„ˆ: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' UI ą¤•ą„‹ argument form ą¤¦ą¤æą¤–ą¤¾ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤, और model ą¤•ą„‹ valid arguments ą¤¬ą¤Øą¤¾ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤, ą¤œą„‹ ą¤•ą„ą¤› ą¤šą¤¾ą¤¹ą¤æą¤ वह सब ą¤‡ą¤øą„€ schema ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ +ą¤¦ą„‚ą¤øą¤°ą¤¾ tool, `lookup_book`, बिना `title=` ą¤•ą„‡ register ą¤¹ą„ą¤† ऄा, ą¤‡ą¤øą¤²ą¤æą¤ उसका `tool.title` `None` ą¤¹ą„ˆą„¤ + !!! tip `title` optional ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ ą¤•ą¤æą¤øą„€ इंसान ą¤•ą„‹ tools ą¤¦ą¤æą¤–ą¤¾ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ UI ą¤•ą„‹ ą¤šą„ą¤Øą¤Øą¤¾ पऔ़ता ą¤¹ą„ˆ: `title` ą¤¹ą„‹ ą¤¤ą„‹ ą¤µą¤¹ą„€, ą¤Øą¤¹ą„€ą¤‚ ą¤¤ą„‹ `name`ą„¤ `from mcp.shared.metadata_utils import get_display_name` ą¤ ą„€ą¤• ą¤Æą¤¹ą„€ करता ą¤¹ą„ˆ, @@ -86,7 +98,7 @@ UI ą¤•ą„‹ argument form ą¤¦ą¤æą¤–ą¤¾ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤, और model ą¤•ą„‹ `call_tool(name, arguments)` tool चलाता ą¤¹ą„ˆ और ą¤†ą¤Ŗą¤•ą„‹ `CallToolResult` वापस ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ result.is_error # False resource verbs ą¤œą„‹ą¤”ą¤¼ą¤æą¤Æą„‹ą¤‚ ą¤®ą„‡ą¤‚ ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: ą¤øą„‚ą¤šą„€ ą¤²ą„‡ą¤Øą„‡ ą¤•ą„‡ ą¤¦ą„‹ ą¤¤ą¤°ą„€ą¤•ą„‡, ą¤Ŗą¤¢ą¤¼ą¤Øą„‡ का ą¤ą¤•ą„¤ -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ client ą¤•ą„‹ यह ą¤­ą„€ बताया जा सकता ą¤¹ą„ˆ कि ## Prompts {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ host ą¤Æą„‡ messages ą¤øą„€ą¤§ą„‡ model ą¤•ą„‹ ą¤¦ą„‡ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ प जिस server ą¤®ą„‡ą¤‚ completion handler ą¤¹ą„‹ वह user ą¤•ą„‡ type ą¤•ą¤°ą¤¤ą„‡-ą¤•ą¤°ą¤¤ą„‡ prompt और resource-template arguments autocomplete कर सकता ą¤¹ą„ˆą„¤ -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ host ą¤Æą„‡ messages ą¤øą„€ą¤§ą„‡ model ą¤•ą„‹ ą¤¦ą„‡ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ प हर `list_*` method ą¤ą¤• `cursor=` keyword ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और हर result ą¤®ą„‡ą¤‚ `next_cursor` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ जब `next_cursor` `None` ą¤¹ą„‹, ą¤†ą¤Ŗą¤•ą„‡ पास सब ą¤•ą„ą¤› ą¤¹ą„ˆą„¤ -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -यह loop हर server ą¤•ą„‡ साऄ ą¤øą¤¹ą„€ ą¤¹ą„ˆą„¤ `MCPServer` सब ą¤•ą„ą¤› ą¤ą¤• ą¤¹ą„€ page ą¤®ą„‡ą¤‚ ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ `next_cursor` `None` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और loop ą¤ą¤• बार चलता ą¤¹ą„ˆ, ą¤Æą¤¹ą„€ वजह ą¤¹ą„ˆ कि ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ą¤¤ą¤° code ą¤‡ą¤øą„‡ ą¤•ą¤­ą„€ लिखता ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤œą„‹ servers सच ą¤®ą„‡ą¤‚ page ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और cursors जिन ą¤Øą¤æą¤Æą¤®ą„‹ą¤‚ का पालन ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤µą„‡ **[Pagination](../advanced/pagination.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ +`list_all_tools` हर server ą¤•ą„‡ साऄ ą¤øą¤¹ą„€ ą¤¹ą„ˆą„¤ `MCPServer` सब ą¤•ą„ą¤› ą¤ą¤• ą¤¹ą„€ page ą¤®ą„‡ą¤‚ ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ `next_cursor` `None` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और loop ą¤ą¤• बार चलता ą¤¹ą„ˆ, ą¤Æą¤¹ą„€ वजह ą¤¹ą„ˆ कि ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ą¤¤ą¤° code ą¤‡ą¤øą„‡ ą¤•ą¤­ą„€ लिखता ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤œą„‹ servers सच ą¤®ą„‡ą¤‚ page ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और cursors जिन ą¤Øą¤æą¤Æą¤®ą„‹ą¤‚ का पालन ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤µą„‡ **[Pagination](../advanced/pagination.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ ## tests ą¤®ą„‡ą¤‚ {#in-tests} -बिना process और बिना port वाला `Client(mcp)` ą¤…ą¤Ŗą¤Øą„‡ आप ą¤®ą„‡ą¤‚ server ą¤•ą„‡ ą¤²ą¤æą¤ test harness ą¤¹ą„ˆą„¤ +इस page ą¤•ą„€ हर `client.py` HTTP ą¤•ą„‡ ą¤œą¤¼ą¤°ą¤æą¤ `server.py` तक ą¤Ŗą¤¹ą„ą¤ą¤šą„€ą„¤ test ą¤®ą„‡ą¤‚ आप network ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और `Client` ą¤•ą„‹ server object ą¤¹ą„€ ą¤¦ą„‡ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: `from server import mcp`, फिर `Client(mcp)`ą„¤ न process, न port, और ऊपर का हर method ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ काम करता ą¤¹ą„ˆą„¤ -ą¤‡ą¤øą„€ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤ą¤• constructor flag बना ą¤¹ą„ˆ: `Client(mcp, raise_exceptions=True)`ą„¤ इसका असर ą¤øą¤æą¤°ą„ą¤«ą¤¼ in-memory connections पर ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, और **[Testing](../get-started/testing.md)** वह page ą¤¹ą„ˆ ą¤œą„‹ ą¤‡ą¤øą„‡ ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ और ą¤‡ą¤øą¤•ą„‡ ą¤šą¤¾ą¤°ą„‹ą¤‚ ओर ą¤Ŗą„‚ą¤°ą¤¾ pattern बनाता ą¤¹ą„ˆą„¤ +ą¤‡ą¤øą„€ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤ą¤• constructor flag बना ą¤¹ą„ˆ: `Client(mcp, raise_exceptions=True)`ą„¤ इसका असर ą¤øą¤æą¤°ą„ą¤«ą¤¼ in-process connections पर ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, और **[Testing](../get-started/testing.md)** वह page ą¤¹ą„ˆ ą¤œą„‹ ą¤‡ą¤øą„‡ ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ और ą¤‡ą¤øą¤•ą„‡ ą¤šą¤¾ą¤°ą„‹ą¤‚ ओर ą¤Ŗą„‚ą¤°ą¤¾ pattern बनाता ą¤¹ą„ˆą„¤ ## सारांश {#recap} -* `Client(x)` server object ą¤øą„‡ in-memory connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, URL string ą¤øą„‡ Streamable HTTP पर, और ą¤¬ą¤¾ą¤•ą„€ ą¤•ą¤æą¤øą„€ ą¤­ą„€ ą¤šą„€ą¤œą¤¼ ą¤øą„‡ transport ą¤•ą„‡ ą¤œą¤¼ą¤°ą¤æą¤ą„¤ +* `Client(x)` URL string ą¤øą„‡ Streamable HTTP पर connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, `StdioServerParameters` ą¤•ą„‡ ą¤²ą¤æą¤ subprocess launch करता ą¤¹ą„ˆ, transport ą¤®ą„‡ą¤‚ ą¤øą„€ą¤§ą„‡ enter करता ą¤¹ą„ˆ, और tests ą¤®ą„‡ą¤‚ server object ą¤¹ą„€ ą¤²ą„‡ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * `async with` ą¤¹ą„€ ą¤Ŗą„‚ą¤°ą¤¾ lifecycle ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤•ą„‡ अंदर `server_capabilities` और `protocol_version` ą¤Ŗą¤¹ą¤²ą„‡ ą¤øą„‡ ą¤­ą¤°ą„‡ ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; server ą¤¦ą„‡ ą¤¤ą„‹ `server_info` और `instructions` ą¤­ą„€ą„¤ * `list_tools()` ą¤†ą¤Ŗą¤•ą„‹ हर tool का `name`, `title`, `description` और `input_schema` ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * `call_tool()` model ą¤•ą„‡ ą¤²ą¤æą¤ `content`, ą¤†ą¤Ŗą¤•ą„‡ code ą¤•ą„‡ ą¤²ą¤æą¤ `structured_content`, और `is_error` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ raise ą¤•ą¤°ą¤Øą„‡ वाला tool ą¤ą¤• result ą¤¹ą„ˆ, exception ą¤Øą¤¹ą„€ą¤‚ą„¤ diff --git a/i18n/hi/pages/client/oauth-clients.md b/i18n/hi/pages/client/oauth-clients.md index 51526eb2..bc31d4ce 100644 --- a/i18n/hi/pages/client/oauth-clients.md +++ b/i18n/hi/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth clients {#oauth-clients} @@ -81,18 +81,20 @@ authorization code flow ą¤•ą„‹ इंसान ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤ ą„€ जब `Client` ą¤Ŗą¤¹ą¤²ą„€ बार request ą¤­ą„‡ą¤œą¤¤ą¤¾ ą¤¹ą„ˆ, server `401` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ provider कमान संभाल ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ: -1. **Discovery.** यह `WWW-Authenticate` header पढ़ता ą¤¹ą„ˆ, `/.well-known/oauth-protected-resource` ą¤øą„‡ server का Protected Resource Metadata लाता ą¤¹ą„ˆ, पता करता ą¤¹ą„ˆ कि ą¤•ą„Œą¤Ø सा authorization server इस resource ą¤•ą„€ ą¤°ą¤•ą„ą¤·ą¤¾ करता ą¤¹ą„ˆ, और **उस** server का metadata लाता ą¤¹ą„ˆą„¤ +1. **Discovery.** यह `WWW-Authenticate` header पढ़ता ą¤¹ą„ˆ, `/.well-known/oauth-protected-resource` ą¤øą„‡ server का Protected Resource Metadata लाता ą¤¹ą„ˆ, पता करता ą¤¹ą„ˆ कि ą¤•ą„Œą¤Ø सा authorization server इस resource ą¤•ą„€ ą¤°ą¤•ą„ą¤·ą¤¾ करता ą¤¹ą„ˆ, और **उस** server का metadata लाता ą¤¹ą„ˆą„¤ (ą¤•ą„‹ą¤ˆ ą¤Ŗą„ą¤°ą¤¾ą¤Øą¤¾ server ą¤œą„‹ resource metadata publish ą¤Øą¤¹ą„€ą¤‚ करता, ą¤‰ą¤øą¤øą„‡ ą¤‡ą¤øą¤•ą„‡ बजाय ą¤‰ą¤øą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ origin पर authorization server metadata माँगा जाता ą¤¹ą„ˆą„¤) ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤¹ą„€ ą¤øą„‚ą¤°ą¤¤ą„‹ą¤‚ ą¤®ą„‡ą¤‚ metadata ą¤•ą„‹ ą¤…ą¤Ŗą¤Øą„‡ `issuer` ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤‰ą¤øą„€ server का नाम ą¤¦ą„‡ą¤Øą¤¾ ą¤¹ą„‹ą¤—ą¤¾ ą¤œą¤æą¤øą¤•ą„‡ ą¤²ą¤æą¤ ą¤‰ą¤øą„‡ लाया गया ऄा; ą¤‡ą¤øą¤•ą„‡ अलावा ą¤•ą„ą¤› ą¤­ą„€ ą¤¹ą„‹ ą¤¤ą„‹ मना कर दिया जाता ą¤¹ą„ˆą„¤ 2. **Registration.** storage ą¤®ą„‡ą¤‚ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ? यह ą¤†ą¤Ŗą¤•ą„‡ `OAuthClientMetadata` ą¤•ą„‡ साऄ ą¤†ą¤Ŗą¤•ą„‹ dynamically register करता ą¤¹ą„ˆ और ą¤Øą¤¤ą„€ą¤œą¤¾ store कर ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ 3. **Authorization.** यह PKCE pair और `state` बनाता ą¤¹ą„ˆ, authorization URL ą¤¤ą„ˆą¤Æą¤¾ą¤° करता ą¤¹ą„ˆ, ą¤†ą¤Ŗą¤•ą„‡ `redirect_handler` ą¤•ą„‹ await करता ą¤¹ą„ˆ, फिर code ą¤•ą„‡ ą¤²ą¤æą¤ ą¤†ą¤Ŗą¤•ą„‡ `callback_handler` ą¤•ą„‹ await करता ą¤¹ą„ˆą„¤ 4. **Exchange.** यह code ą¤•ą„‡ ą¤¬ą¤¦ą¤²ą„‡ `OAuthToken` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤‰ą¤øą„‡ store करता ą¤¹ą„ˆ, और ą¤†ą¤Ŗą¤•ą„€ ą¤®ą„‚ą¤² request ą¤•ą„‹ `Authorization: Bearer ...` ą¤•ą„‡ साऄ ą¤¦ą„‹ą¤¬ą¤¾ą¤°ą¤¾ ą¤­ą„‡ą¤œą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‰ą¤øą¤•ą„‡ बाद यह शांत रहता ą¤¹ą„ˆą„¤ tokens storage ą¤øą„‡ ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, expire ą¤¹ą„ą¤† access token refresh token ą¤øą„‡ refresh ą¤¹ą„‹ जाता ą¤¹ą„ˆ, और ą¤øą¤æą¤°ą„ą¤«ą¤¼ तब जब ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą„ą¤› काम ą¤Øą¤¹ą„€ą¤‚ करता, यह flow फिर ą¤øą„‡ चलाता ą¤¹ą„ˆą„¤ +इन ą¤øą¤­ą„€ requests पर transport का ą¤ą¤• नियम ą¤²ą¤¾ą¤—ą„‚ ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ: जिस MCP request ą¤•ą„‡ अंदर ą¤Æą„‡ ą¤šą¤²ą¤¤ą„€ ą¤¹ą„ˆą¤‚ ą¤‰ą¤øą„€ ą¤•ą„€ तरह, ą¤Æą„‡ ą¤•ą¤æą¤øą„€ redirect ą¤•ą„‹ ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤¤ą¤­ą„€ follow ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆą¤‚ जब वह ą¤‰ą¤øą„€ origin पर ą¤°ą¤¹ą„‡ और method ą¤µą¤¹ą„€ ą¤°ą¤–ą„‡ (ą¤œą„ˆą¤øą„‡ trailing-slash वाला 307/308), और ą¤•ą¤æą¤øą„€ ą¤­ą„€ ą¤¦ą„‚ą¤øą¤°ą„‡ redirect ą¤•ą„‹ ą¤ą¤øą„‡ ą¤®ą¤¾ą¤Øą¤¤ą„€ ą¤¹ą„ˆą¤‚ ą¤œą„ˆą¤øą„‡ उस URL ą¤Øą„‡ जवाब ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą¤æą¤Æą¤¾ą„¤ + ą¤†ą¤Ŗą¤Øą„‡ ą¤‡ą¤øą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤²ą¤æą¤–ą¤¾ą„¤ ą¤¦ą„‹ keyword arguments ą¤¬ą¤šą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (`client_metadata_url` और `validate_resource_url`), और इस file ą¤•ą„‹ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą¤æą¤øą„€ ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤Øą¤¹ą„€ą¤‚ą„¤ `client_metadata_url` ą¤œą¤¾ą¤Øą¤Øą„‡ लायक ą¤¹ą„ˆ; इसका अपना section ą¤Øą„€ą¤šą„‡ ą¤¹ą„ˆą„¤ ### ą¤‡ą¤øą„‡ ą¤†ą¤œą¤¼ą¤®ą¤¾ą¤ą¤ {#try-it} -इन docs ą¤•ą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ą¤¤ą¤° उदाहरण आप in-memory `Client(server)` ą¤øą„‡ जाँच ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ यह ą¤Øą¤¹ą„€ą¤‚: इस flow का ą¤Ŗą„‚ą¤°ą¤¾ मतलब ą¤¹ą„€ HTTP `401` ą¤¹ą„ˆ, और in-memory client व ą¤‰ą¤øą¤•ą„‡ server ą¤•ą„‡ ą¤¬ą„€ą¤š ą¤•ą„‹ą¤ˆ HTTP ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ +in-memory `Client(server)`, ą¤œą¤æą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‡ tests ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, यहाँ ą¤•ą¤æą¤øą„€ काम का ą¤Øą¤¹ą„€ą¤‚: इस flow का ą¤Ŗą„‚ą¤°ą¤¾ मतलब ą¤¹ą„€ HTTP `401` ą¤¹ą„ˆ, और in-memory client व ą¤‰ą¤øą¤•ą„‡ server ą¤•ą„‡ ą¤¬ą„€ą¤š ą¤•ą„‹ą¤ˆ HTTP ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ repository ą¤®ą„‡ą¤‚ live version ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ `examples/servers/simple-auth/` ą¤ą¤• standalone authorization server और ą¤ą¤• protected MCP server चलाता ą¤¹ą„ˆ; `examples/clients/simple-auth-client/` ą¤‡ą¤øą„€ page का client ą¤¹ą„ˆ, ą¤›ą„‹ą¤Ÿą„€ CLI ą¤®ą„‡ą¤‚ बढ़ा ą¤¹ą„ą¤†ą„¤ ą¤‰ą¤øą¤•ą„€ README ą¤®ą„‡ą¤‚ ą¤¦ą„‹ commands ą¤¹ą„ˆą¤‚: servers ą¤¶ą„ą¤°ą„‚ ą¤•ą¤°ą„‡ą¤‚, client ą¤•ą„‹ ą¤‰ą¤Øą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ ą¤šą¤²ą¤¾ą¤ą¤, और ą¤šą¤¾ą¤°ą„‹ą¤‚ चरण ą¤…ą¤Ŗą¤Øą„€ ą¤†ą¤ą¤–ą„‹ą¤‚ ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ ą¤¹ą„‹ą¤¤ą„‡ ą¤¦ą„‡ą¤–ą„‡ą¤‚ą„¤ @@ -110,13 +112,14 @@ URL HTTPS ą¤¹ą„‹ą¤Øą¤¾ ą¤šą¤¾ą¤¹ą¤æą¤ और उसका path root न ą¤¹ą„‹; `ClientCredentialsOAuthProvider` ą¤µą¤¹ą„€ `httpx2.Auth` ą¤¹ą„ˆ, बस इंसान ą¤•ą„‡ बिना: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` ą¤•ą„ą¤Æą¤¾ बदला: * न `OAuthClientMetadata`, न handlersą„¤ आप `client_id` और `client_secret` ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; provider ą¤‰ą¤Øą¤•ą„‡ ą¤‡ą¤°ą„ą¤¦-ą¤—ą¤æą¤°ą„ą¤¦ ą¤ą¤• ą¤Øą„ą¤Æą„‚ą¤Øą¤¤ą¤® `client_credentials` registration बनाता ą¤¹ą„ˆ और dynamic registration ą¤Ŗą„‚ą¤°ą„€ तरह ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +* `issuer` उस authorization server का नाम बताता ą¤¹ą„ˆ ą¤œą¤æą¤øą¤Øą„‡ ą¤µą„‡ credentials ą¤œą¤¾ą¤°ą„€ ą¤•ą¤æą¤; ą¤µą¤¹ą„€ `issuer` value ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą„‡ą¤‚ ą¤œą„‹ उसका `/.well-known/oauth-authorization-server` document ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ discovery अब ą¤­ą„€ ऊपर ą¤¬ą¤¤ą¤¾ą¤ ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤øą„‡ ą¤šą¤²ą¤¤ą„€ ą¤¹ą„ˆ, ą¤²ą„‡ą¤•ą¤æą¤Ø token requests ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤øą¤æą¤°ą„ą¤«ą¤¼ **ą¤‰ą¤øą„€** issuer ą¤•ą„‡ metadata ą¤øą„‡ ą¤¬ą¤Øą¤¤ą„€ ą¤¹ą„ˆą¤‚; अगर MCP server ą¤•ą¤¹ą„€ą¤‚ और इशारा करता ą¤¹ą„ˆ, ą¤¤ą„‹ flow ą¤‡ą¤øą¤•ą„‡ बजाय `OAuthFlowError` ą¤•ą„‡ साऄ ą¤°ą„ą¤• जाता ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤Øą¤¾ deprecated ą¤¹ą„ˆ और 3.0 ą¤®ą„‡ą¤‚ यह ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„‹ ą¤œą¤¾ą¤ą¤—ą¤¾ (**[Deprecated features](../deprecated.md#deprecated-sdk-helpers)** ą¤¦ą„‡ą¤–ą„‡ą¤‚); तब तक provider warning ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और discovery ą¤•ą„‹ ą¤œą„‹ ą¤­ą„€ authorization server मिलता ą¤¹ą„ˆ, ą¤‰ą¤øą„‡ ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² करता ą¤¹ą„ˆą„¤ * `scope` space ą¤øą„‡ अलग ą¤•ą„€ ą¤—ą¤ˆ string ą¤¹ą„ˆ, OAuth का wire formatą„¤ * ą¤†ą¤—ą„‡ का सब ą¤•ą„ą¤› ą¤¬ą¤æą¤²ą„ą¤•ą„ą¤² ą¤µą¤¹ą„€ ą¤¹ą„ˆ: ą¤µą¤¹ą„€ `TokenStorage`, ą¤µą¤¹ą„€ `httpx2.AsyncClient(auth=...)`, ą¤µą¤¹ą„€ `streamable_http_client`ą„¤ @@ -129,7 +132,7 @@ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ secret token request पर HTTP Basic auth ą¤•ą„‡ र ą¤ą¤• और provider `mcp.client.auth.extensions.client_credentials` ą¤®ą„‡ą¤‚ रहता ą¤¹ą„ˆ: **`PrivateKeyJWTOAuthProvider`**, उन clients ą¤•ą„‡ ą¤²ą¤æą¤ ą¤œą„‹ shared secret ą¤•ą„‡ बजाय JWT ą¤øą„‡ authenticate ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (`private_key_jwt`, key-pair और workload-identity वाला ą¤°ą„‚ą¤Ŗ)ą„¤ यह ą¤‰ą¤øą„€ - pattern पर चलता ą¤¹ą„ˆ: ą¤ą¤• ą¤¬ą¤Øą¤¾ą¤ą¤, `auth=` पर ą¤²ą¤—ą¤¾ą¤ą¤ą„¤ ą¤‰ą¤øą„€ module ą¤®ą„‡ą¤‚ + pattern पर चलता ą¤¹ą„ˆ: ą¤ą¤• ą¤¬ą¤Øą¤¾ą¤ą¤ (यह ą¤­ą„€ ą¤µą¤¹ą„€ optional `issuer` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ), `auth=` पर ą¤²ą¤—ą¤¾ą¤ą¤ą„¤ ą¤‰ą¤øą„€ module ą¤®ą„‡ą¤‚ `SignedJWTParameters` और `static_assertion_provider` ą¤­ą„€ ą¤¹ą„ˆą¤‚, ą¤¦ą„‹ helpers ą¤œą„‹ इसका assertion ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ बिना इंसान ą¤µą¤¾ą¤²ą„€ ą¤ą¤• और ą¤øą„ą¤„ą¤æą¤¤ą¤æ ą¤¹ą„ˆ: client ą¤•ą¤æą¤øą„€ enterprise का ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą¤¾ identity provider, न कि user, तय करता ą¤¹ą„ˆ कि वह किन MCP servers तक ą¤Ŗą¤¹ą„ą¤ą¤š सकता ą¤¹ą„ˆą„¤ वह अलग grant ą¤¹ą„ˆ, ą¤…ą¤Ŗą¤Øą„‡ trust model और ą¤…ą¤Ŗą¤Øą„‡ page ą¤•ą„‡ साऄ, **[Identity assertion](identity-assertion.md)**ą„¤ diff --git a/i18n/hi/pages/client/session-groups.md b/i18n/hi/pages/client/session-groups.md index f06c315b..f3e7eb5a 100644 --- a/i18n/hi/pages/client/session-groups.md +++ b/i18n/hi/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Session groups {#session-groups} @@ -78,7 +78,7 @@ translation: ## सारांश {#recap} * `ClientSessionGroup` ą¤•ą¤ˆ server connections रखता ą¤¹ą„ˆ और ą¤‰ą¤Øą¤•ą„‡ tools, resources और prompts ą¤•ą„‹ ą¤ą¤•-ą¤ą¤• `dict` ą¤®ą„‡ą¤‚ मिला ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ -* हर server ą¤•ą„‡ ą¤²ą¤æą¤ `connect_to_server(params)`ą„¤ यह transport parameters ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤•ą¤­ą„€ वह server object या URL ą¤Øą¤¹ą„€ą¤‚ ą¤œą„‹ `Client` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +* हर server ą¤•ą„‡ ą¤²ą¤æą¤ `connect_to_server(params)`ą„¤ यह transport parameters ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤•ą¤­ą„€ वह URL या `Transport` ą¤Øą¤¹ą„€ą¤‚ ą¤œą„‹ `Client` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * `group.call_tool(name, arguments)` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ call ą¤•ą„‹ उस server तक ą¤Ŗą¤¹ą„ą¤ą¤šą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą¤¾ वह tool ą¤¹ą„ˆą„¤ * नाम ą¤Ŗą„‚ą¤°ą„‡ group ą¤®ą„‡ą¤‚ unique ą¤¹ą„‹ą¤Øą„‡ ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„ˆą¤‚; `search` tool ą¤µą¤¾ą¤²ą„‡ ą¤¦ą„‹ servers ą¤…ą¤Ŗą¤Øą„‡ आप साऄ ą¤Øą¤¹ą„€ą¤‚ रह ą¤øą¤•ą¤¤ą„‡ą„¤ * `component_name_hook=` हर register ą¤•ą¤æą¤ ą¤—ą¤ नाम ą¤•ą„‹ फिर ą¤øą„‡ लिखता ą¤¹ą„ˆą„¤ dict key ą¤¬ą¤¦ą¤²ą¤¤ą„€ ą¤¹ą„ˆ, wire पर ą¤œą¤¾ą¤Øą„‡ वाला नाम ą¤Øą¤¹ą„€ą¤‚ą„¤ diff --git a/i18n/hi/pages/client/transports.md b/i18n/hi/pages/client/transports.md index e6cb756b..d049588d 100644 --- a/i18n/hi/pages/client/transports.md +++ b/i18n/hi/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Client transports {#client-transports} @@ -11,30 +11,15 @@ translation: हर transport का **server** वाला ą¤Ŗą¤•ą„ą¤· (`mcp.run()` ą¤•ą„ą¤Æą¤¾ करता ą¤¹ą„ˆ और आप ą¤•ą„ą¤Æą¤¾ deploy ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚) **[अपना server चलाना](../run/index.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ -## Memory ą¤®ą„‡ą¤‚ {#in-memory} - -server object ą¤¹ą„€ पास ą¤•ą¤°ą„‡ą¤‚: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -न ą¤•ą„‹ą¤ˆ subprocess, न ą¤•ą„‹ą¤ˆ port, न wire पर ą¤•ą„‹ą¤ˆ bytesą„¤ client और server ą¤ą¤• ą¤¹ą„€ process ą¤®ą„‡ą¤‚ ą¤¦ą„‹ objects ą¤¹ą„ˆą¤‚, और call फिर ą¤­ą„€ ą¤…ą¤øą¤²ą„€ protocol layer ą¤øą„‡ ą¤¹ą„‹ą¤•ą¤° ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆ: `search_books` ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ list, validate और invoke ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ HTTP पर ą¤¹ą„‹ą¤¤ą¤¾ą„¤ - -ą¤‡ą¤øą¤øą„‡ यह ą¤ą¤• साऄ ą¤¦ą„‹ काम करता ą¤¹ą„ˆ: - -* **Test harnessą„¤** इस documentation का हर उदाहरण ą¤‡ą¤øą„€ ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤øą„‡ चलाया जाता ą¤¹ą„ˆ, और **[Testing](../get-started/testing.md)** page ą¤Ŗą„‚ą¤°ą¤¾ pattern ą¤‡ą¤øą„€ ą¤•ą„‡ ą¤‡ą¤°ą„ą¤¦-ą¤—ą¤æą¤°ą„ą¤¦ बनाता ą¤¹ą„ˆą„¤ -* **Embedding APIą„¤** ą¤œą„‹ application ą¤–ą„ą¤¦ server बनाता ą¤¹ą„ˆ, ą¤‰ą¤øą„‡ ą¤‰ą¤øą¤•ą„‡ tools call ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ network hop ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤Øą¤¹ą„€ą¤‚ą„¤ - ## Streamable HTTP {#streamable-http} -URL string पास ą¤•ą¤°ą„‡ą¤‚ और ą¤†ą¤Ŗą¤•ą„‹ **Streamable HTTP** मिलता ą¤¹ą„ˆ, वह transport ą¤œą¤æą¤øą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ आप deploy ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: +URL string पास ą¤•ą¤°ą„‡ą¤‚ और ą¤†ą¤Ŗą¤•ą„‹ **Streamable HTTP** मिलता ą¤¹ą„ˆ, वह transport ą¤œą¤æą¤øą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ आप deploy ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और ą¤œą¤æą¤øą„‡ ą¤øą¤¬ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤šą„ą¤Øą¤Øą¤¾ ą¤šą¤¾ą¤¹ą¤æą¤: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -ą¤Ŗą„‚ą¤°ą¤¾ production client बस इतना ą¤¹ą„€ ą¤¹ą„ˆą„¤ `Client` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ URL ą¤•ą„‹ `streamable_http_client(...)` ą¤®ą„‡ą¤‚ ą¤²ą¤Ŗą„‡ą¤Ÿ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤ą¤• `httpx2.AsyncClient` ą¤•ą„‡ ऊपर ą¤œą„‹ MCP ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤•ą„‡ हिसाब ą¤øą„‡ configure किया गया ą¤¹ą„ˆ: `follow_redirects=True`, connect/write/pool ą¤•ą„‡ ą¤²ą¤æą¤ 30 ą¤øą„‡ą¤•ą¤‚ą¤” का timeout, और 300 ą¤øą„‡ą¤•ą¤‚ą¤” का read timeout, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ server response stream ą¤•ą„‹ ą¤–ą„ą¤²ą¤¾ रख सकता ą¤¹ą„ˆą„¤ +ą¤Ŗą„‚ą¤°ą¤¾ production client बस इतना ą¤¹ą„€ ą¤¹ą„ˆą„¤ `Client` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ URL ą¤•ą„‹ `streamable_http_client(...)` ą¤®ą„‡ą¤‚ ą¤²ą¤Ŗą„‡ą¤Ÿ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤ą¤• `httpx2.AsyncClient` ą¤•ą„‡ ऊपर ą¤œą„‹ MCP ą¤•ą„€ ą¤œą¤¼ą¤°ą„‚ą¤°ą¤¤ ą¤•ą„‡ हिसाब ą¤øą„‡ configure किया गया ą¤¹ą„ˆ: connect/write/pool ą¤•ą„‡ ą¤²ą¤æą¤ 30 ą¤øą„‡ą¤•ą¤‚ą¤” का timeout, और 300 ą¤øą„‡ą¤•ą¤‚ą¤” का read timeout, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ server response stream ą¤•ą„‹ ą¤–ą„ą¤²ą¤¾ रख सकता ą¤¹ą„ˆą„¤ !!! check ą¤œą„‹ `Client` ą¤†ą¤Ŗą¤Øą„‡ बनाया ą¤¹ą„ˆ वह connected **ą¤Øą¤¹ą„€ą¤‚** ą¤¹ą„ˆą„¤ ą¤¬ą¤Øą¤¾ą¤Øą„‡ ą¤øą„‡ ą¤øą¤æą¤°ą„ą¤«ą¤¼ transport ą¤šą„ą¤Øą¤¾ जाता ą¤¹ą„ˆ; @@ -50,7 +35,7 @@ URL string पास ą¤•ą¤°ą„‡ą¤‚ और ą¤†ą¤Ŗą¤•ą„‹ **Streamable HTTP** मि ą¤œą„ˆą¤øą„‡ ą¤¹ą„€ ą¤†ą¤Ŗą¤•ą„‹ `Authorization` header, cookie, proxy, mTLS या ą¤•ą„‹ą¤ˆ अलग timeout ą¤šą¤¾ą¤¹ą¤æą¤, `httpx2.AsyncClient` ą¤–ą„ą¤¦ ą¤¬ą¤Øą¤¾ą¤ą¤ और ą¤‰ą¤øą„‡ `streamable_http_client` ą¤•ą„‹ ą¤¦ą„‡ą¤‚: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ environment variables set ą¤•ą¤°ą„‡ą¤‚ या ą¤…ą¤Ŗą¤Øą„‡ `httpx2.AsyncClient` !!! info `httpx2` जाना-पहचाना `httpx` API ą¤¹ą„€ रखता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ अगर आप `httpx` ą¤œą¤¾ą¤Øą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ą¤¤ą„‹ यहाँ auth, proxies, event hooks, retries और connection limits ą¤•ą„ˆą¤øą„‡ ą¤•ą¤°ą¤Øą„‡ ą¤¹ą„ˆą¤‚, यह आप ą¤Ŗą¤¹ą¤²ą„‡ ą¤øą„‡ ą¤œą¤¾ą¤Øą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ SDK न ऊपर ą¤øą„‡ ą¤•ą„ą¤› ą¤œą„‹ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, न ą¤•ą„ą¤› - हटाता ą¤¹ą„ˆą„¤ OAuth ą¤­ą„€ ą¤Æą¤¹ą„€ą¤‚ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ: + हटाता ą¤¹ą„ˆ, सिवाय [redirect handling](#redirects) ą¤•ą„‡ą„¤ OAuth ą¤­ą„€ ą¤Æą¤¹ą„€ą¤‚ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`ą„¤ वह ą¤Ŗą„‚ą¤°ą¤¾ flow **[OAuth clients](oauth-clients.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ +### Redirects {#redirects} + +transport ą¤‰ą¤øą„€ URL ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ ą¤†ą¤Ŗą¤Øą„‡ दिया, और ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤‰ą¤øą„€ origin ą¤øą„‡ą„¤ + +* ą¤œą„‹ `307`/`308` redirect ą¤‰ą¤øą„€ scheme, host और port पर रहता ą¤¹ą„ˆ, ą¤‰ą¤øą„‡ follow किया जाता ą¤¹ą„ˆ, और ą¤‰ą¤øą„€ host पर `http://` → `https://` ą¤•ą„‹ ą¤­ą„€ą„¤ आम `/mcp` → `/mcp/` वाला trailing-slash redirect ą¤‡ą¤øą„€ ą¤®ą„‡ą¤‚ आ जाता ą¤¹ą„ˆą„¤ +* ą¤•ą¤¹ą„€ą¤‚ और ą¤œą¤¾ą¤Øą„‡ वाला redirect follow **ą¤Øą¤¹ą„€ą¤‚** किया ą¤œą¤¾ą¤¤ą¤¾ą„¤ call इस error ą¤•ą„‡ साऄ fail ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + अगर वह URL ą¤µą¤¹ą„€ server ą¤¹ą„ˆ ą¤œą„‹ आप ą¤šą¤¾ą¤¹ą¤¤ą„‡ ą¤„ą„‡, ą¤¤ą„‹ ą¤‰ą¤øą„‡ ą¤…ą¤Ŗą¤Øą„‡ config ą¤®ą„‡ą¤‚ ą¤”ą¤¾ą¤²ą„‡ą¤‚ą„¤ अगर ą¤Øą¤¹ą„€ą¤‚, ą¤¤ą„‹ server या ą¤‰ą¤øą¤•ą„‡ ą¤†ą¤—ą„‡ लगा ą¤•ą„‹ą¤ˆ proxy गलत configure ą¤¹ą„ˆą„¤ + +यह ą¤†ą¤Ŗą¤•ą„‡ पास ą¤•ą¤æą¤ ą¤—ą¤ ą¤•ą¤æą¤øą„€ ą¤­ą„€ `httpx2.AsyncClient` पर ą¤²ą¤¾ą¤—ą„‚ ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ: MCP requests ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‰ą¤øą¤•ą„€ `follow_redirects` setting ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą„‡ą¤–ą„€ ą¤œą¤¾ą¤¤ą„€, ą¤•ą¤æą¤øą„€ ą¤­ą„€ दिशा ą¤®ą„‡ą¤‚ą„¤ SDK ą¤•ą„‡ OAuth providers ą¤…ą¤Ŗą¤Øą„€ requests पर ą¤Æą¤¹ą„€ नियम ą¤²ą¤¾ą¤—ą„‚ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` का मतलब ą¤¹ą„ˆ कि + server ą¤•ą¤æą¤øą„€ ą¤ą¤øą„‡ TLS-terminating proxy ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„‡ ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ ą¤‰ą¤øą„‡ पता ą¤Øą¤¹ą„€ą¤‚, और वह `http://` redirects ą¤œą¤¾ą¤°ą„€ कर रहा ą¤¹ą„ˆą„¤ + ą¤‡ą¤øą„‡ server पर ą¤ ą„€ą¤• किया जाता ą¤¹ą„ˆ (**[Deploy & scale](../run/deploy.md#behind-a-tls-terminating-proxy)**), + या ą¤ ą„€ą¤• ą¤µą¤¹ą„€ `https://…/` URL ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤•ą„‡ ą¤œą„‹ message ą¤øą„ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ + ## stdio {#stdio} **stdio** server ą¤ą¤• subprocess ą¤¹ą„ˆą„¤ client ą¤‰ą¤øą„‡ launch करता ą¤¹ą„ˆ, ą¤‰ą¤øą¤•ą„‡ stdin पर JSON-RPC लिखता ą¤¹ą„ˆ और ą¤‰ą¤øą¤•ą„‡ stdout ą¤øą„‡ JSON-RPC पढ़ता ą¤¹ą„ˆą„¤ desktop host ą¤†ą¤Ŗą¤•ą„€ machine पर server ą¤‡ą¤øą„€ तरह चलाता ą¤¹ą„ˆ: host ą¤Æą¤¹ą„€ code **ą¤¹ą„ˆ**, बस ऊपर ą¤ą¤• UI ą¤•ą„‡ साऄ, और **[ą¤…ą¤øą¤²ą„€ host ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą„‡ą¤‚](../get-started/real-host.md)** ą¤Æą¤¹ą„€ ą¤°ą¤æą¤¶ą„ą¤¤ą¤¾ host ą¤•ą„€ तरफ़ ą¤øą„‡, ą¤ą¤• config file ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ दिखाता ą¤¹ą„ˆą„¤ @@ -105,6 +111,18 @@ child का stderr ą¤†ą¤Ŗą¤•ą„‡ stderr पर जाता ą¤¹ą„ˆą„¤ ą¤‰ą¤øą„‡ जिस server ą¤•ą„‹ API key ą¤šą¤¾ą¤¹ą¤æą¤, ą¤‰ą¤øą„‡ वह वहाँ ą¤Øą¤¹ą„€ą¤‚ ą¤®ą¤æą¤²ą„‡ą¤—ą„€ą„¤ ą¤‰ą¤øą„‡ `env=` ą¤øą„‡ explicitly पास ą¤•ą¤°ą„‡ą¤‚; ą¤µą„‡ variables allow-list ą¤•ą„‡ ऊपर merge ą¤¹ą„‹ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ऊपर `BOOKSHOP_API_KEY` ą¤Æą¤¹ą„€ कर रहा ą¤¹ą„ˆą„¤ +## Memory ą¤®ą„‡ą¤‚ {#in-memory} + +test ą¤®ą„‡ą¤‚ न ą¤•ą„ą¤› deploy करना ą¤¹ą„ˆ, न ą¤•ą„ą¤› launch ą¤•ą¤°ą¤Øą¤¾ą„¤ server object ą¤¹ą„€ पास ą¤•ą¤°ą„‡ą¤‚: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +न ą¤•ą„‹ą¤ˆ subprocess, न ą¤•ą„‹ą¤ˆ port, न wire पर ą¤•ą„‹ą¤ˆ bytesą„¤ client और server ą¤ą¤• ą¤¹ą„€ process ą¤®ą„‡ą¤‚ ą¤¦ą„‹ objects ą¤¹ą„ˆą¤‚, और call फिर ą¤­ą„€ ą¤…ą¤øą¤²ą„€ protocol layer ą¤øą„‡ ą¤¹ą„‹ą¤•ą¤° ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆ: `search_books` ą¤ ą„€ą¤• ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ list, validate और invoke ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ HTTP पर ą¤¹ą„‹ą¤¤ą¤¾ą„¤ **[Testing](../get-started/testing.md)** ą¤Ŗą„‚ą¤°ą¤¾ pattern ą¤‡ą¤øą„€ ą¤•ą„‡ ą¤‡ą¤°ą„ą¤¦-ą¤—ą¤æą¤°ą„ą¤¦ बनाता ą¤¹ą„ˆą„¤ + +ą¤Æą¤¹ą„€ ą¤°ą„‚ą¤Ŗ embedding API का काम ą¤­ą„€ करता ą¤¹ą„ˆ: ą¤œą„‹ application ą¤–ą„ą¤¦ server बनाता ą¤¹ą„ˆ, वह बिना network hop ą¤•ą„‡ ą¤‰ą¤øą¤•ą„‡ tools call कर सकता ą¤¹ą„ˆą„¤ + ## SSE {#sse} `mcp.client.sse` का `sse_client(url)` वह HTTP transport ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„€ ą¤œą¤—ą¤¹ Streamable HTTP ą¤Øą„‡ ą¤²ą„€ą„¤ ą¤œą„‹ server अब ą¤­ą„€ ą¤‡ą¤øą„‡ ą¤¬ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤‰ą¤øą¤øą„‡ बात ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą„‡ ą¤‰ą¤øą„€ तरह wrap ą¤•ą¤°ą„‡ą¤‚, `Client(sse_client("http://localhost:8000/sse"))`, और इस पर ą¤•ą„ą¤› नया न ą¤¬ą¤Øą¤¾ą¤ą¤ą„¤ @@ -113,15 +131,16 @@ child का stderr ą¤†ą¤Ŗą¤•ą„‡ stderr पर जाता ą¤¹ą„ˆą„¤ ą¤‰ą¤øą„‡ `Client` ą¤•ą„‡ ą¤²ą¤æą¤ ऊपर ą¤•ą„€ ą¤øą¤­ą„€ ą¤šą„€ą¤œą¤¼ą„‡ą¤‚ ą¤ą¤• ą¤¹ą„€ ą¤¹ą„ˆą¤‚ą„¤ -**transport** ą¤•ą„‹ą¤ˆ ą¤­ą„€ async context manager ą¤¹ą„ˆ ą¤œą„‹ message streams का `(read, write)` ą¤œą„‹ą¤”ą¤¼ą¤¾ yield करता ą¤¹ą„ˆ: ą¤”ą¤Ŗą¤šą¤¾ą¤°ą¤æą¤• ą¤°ą„‚ą¤Ŗ ą¤øą„‡, `mcp.client` का `Transport` protocolą„¤ `Client` ą¤…ą¤Ŗą¤Øą„‡ argument ą¤•ą„‹ type ą¤øą„‡ resolve करता ą¤¹ą„ˆ: server object in-process ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, `str` `streamable_http_client(url)` बन जाता ą¤¹ą„ˆ, `StdioServerParameters` `stdio_client(params)` बन जाता ą¤¹ą„ˆ, और ą¤¬ą¤¾ą¤•ą„€ सब ą¤•ą„ą¤› ą¤øą„€ą¤§ą„‡ transport ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ enter किया जाता ą¤¹ą„ˆą„¤ ą¤Æą¤¹ą„€ ą¤†ą¤–ą¤¼ą¤æą¤°ą„€ नियम वजह ą¤¹ą„ˆ कि `stdio_client(...)`, `streamable_http_client(...)` और `sse_client(...)` सब ą¤‰ą¤øą„€ ą¤ą¤• slot ą¤®ą„‡ą¤‚ ą¤¬ą„ˆą¤ ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और ą¤Æą¤¹ą„€ वजह ą¤¹ą„ˆ कि आप अपना ą¤–ą„ą¤¦ का ą¤­ą„€ लिख ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +**transport** ą¤•ą„‹ą¤ˆ ą¤­ą„€ async context manager ą¤¹ą„ˆ ą¤œą„‹ message streams का `(read, write)` ą¤œą„‹ą¤”ą¤¼ą¤¾ yield करता ą¤¹ą„ˆ: ą¤”ą¤Ŗą¤šą¤¾ą¤°ą¤æą¤• ą¤°ą„‚ą¤Ŗ ą¤øą„‡, `mcp.client` का `Transport` protocolą„¤ `Client` ą¤…ą¤Ŗą¤Øą„‡ argument ą¤•ą„‹ type ą¤øą„‡ resolve करता ą¤¹ą„ˆ: `str` `streamable_http_client(url)` बन जाता ą¤¹ą„ˆ, `StdioServerParameters` `stdio_client(params)` बन जाता ą¤¹ą„ˆ, server object in-process ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, और ą¤¬ą¤¾ą¤•ą„€ सब ą¤•ą„ą¤› ą¤øą„€ą¤§ą„‡ transport ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ enter किया जाता ą¤¹ą„ˆą„¤ ą¤Æą¤¹ą„€ ą¤†ą¤–ą¤¼ą¤æą¤°ą„€ नियम वजह ą¤¹ą„ˆ कि `stdio_client(...)`, `streamable_http_client(...)` और `sse_client(...)` सब ą¤‰ą¤øą„€ ą¤ą¤• slot ą¤®ą„‡ą¤‚ ą¤¬ą„ˆą¤ ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और ą¤Æą¤¹ą„€ वजह ą¤¹ą„ˆ कि आप अपना ą¤–ą„ą¤¦ का ą¤­ą„€ लिख ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ## सारांश {#recap} -* `Client(mcp)` (server object) memory ą¤®ą„‡ą¤‚ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ tests और embedding ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą„‡ą¤‚ą„¤ * `Client("http://.../mcp")` (URL) Streamable HTTP पर ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤œą„‹ production transport ą¤¹ą„ˆą„¤ * Headers, auth, proxies और timeouts उस `httpx2.AsyncClient` पर ą¤¹ą„‹ą¤Øą„‡ ą¤šą¤¾ą¤¹ą¤æą¤ ą¤œą„‹ आप `streamable_http_client(url, http_client=...)` ą¤•ą„‹ पास ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤•ą„‹ą¤ˆ `headers=` keyword ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ +* Redirects ą¤øą¤æą¤°ą„ą¤«ą¤¼ URL ą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ origin ą¤•ą„‡ ą¤­ą„€ą¤¤ą¤° follow ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (trailing-slash वाला `307`/`308`), और ą¤‰ą¤øą„€ host पर `http`→`https`ą„¤ ą¤¬ą¤¾ą¤•ą„€ सब `Redirect to … not followed` ą¤•ą„‡ साऄ fail ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ; final URL configure ą¤•ą¤°ą„‡ą¤‚ą„¤ * stdio ą¤¹ą„ˆ `Client(StdioServerParameters(...))`ą„¤ ą¤‡ą¤øą„‡ ą¤–ą„ą¤¦ `stdio_client(...)` ą¤®ą„‡ą¤‚ ą¤øą¤æą¤°ą„ą¤«ą¤¼ तब wrap ą¤•ą¤°ą„‡ą¤‚ जब child का stderr ą¤•ą¤¹ą„€ą¤‚ और ą¤­ą„‡ą¤œą¤Øą¤¾ ą¤¹ą„‹ą„¤ * subprocess ą¤•ą„‹ allow-list वाला environment मिलता ą¤¹ą„ˆ, आपका ą¤Øą¤¹ą„€ą¤‚; `env=` ą¤‰ą¤øą¤®ą„‡ą¤‚ ą¤œą„‹ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +* `Client(mcp)` (server object) memory ą¤®ą„‡ą¤‚ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ tests ą¤®ą„‡ą¤‚ ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą„‡ą¤‚, या server ą¤•ą„‹ ą¤‰ą¤øą„€ application ą¤®ą„‡ą¤‚ embed ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤œą¤æą¤øą¤Øą„‡ ą¤‰ą¤øą„‡ ą¤¬ą¤Øą¤¾ą¤Æą¤¾ą„¤ * transport वह हर ą¤šą„€ą¤œą¤¼ ą¤¹ą„ˆ जिस पर आप `async with x as (read, write)` कर ą¤øą¤•ą„‡ą¤‚ą„¤ ą¤œą„‹ ą¤•ą„ą¤› server object, URL या `StdioServerParameters` ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, `Client` ą¤‰ą¤øą„‡ ą¤øą„€ą¤§ą„‡ ą¤‰ą¤øą„€ protocol ą¤•ą„‹ ą¤øą„Œą¤‚ą¤Ŗ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * `Client` ą¤¬ą¤Øą¤¾ą¤Øą„‡ ą¤øą„‡ transport ą¤šą„ą¤Øą¤¾ जाता ą¤¹ą„ˆą„¤ `async with` ą¤‰ą¤øą„‡ ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/deprecated.md b/i18n/hi/pages/deprecated.md index d986c75a..db2b439e 100644 --- a/i18n/hi/pages/deprecated.md +++ b/i18n/hi/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Deprecated features {#deprecated-features} -2026-07-28 spec पाँच ą¤šą„€ą¤œą¤¼ą„‹ą¤‚ ą¤•ą„‹ retire करता ą¤¹ą„ˆą„¤ SDK अब ą¤­ą„€ ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ हर ą¤ą¤• ą¤•ą„‹ implement करता ą¤¹ą„ˆ, और अब हर ą¤ą¤• पर **deprecation warning** ą¤²ą¤—ą„€ ą¤¹ą„ˆą„¤ ą¤ą¤• SDK helper ą¤…ą¤Ŗą¤Øą„€ अलग वजह ą¤øą„‡ deprecated ą¤¹ą„ˆ और [आख़िर ą¤®ą„‡ą¤‚](#deprecated-sdk-helpers) दिया गया ą¤¹ą„ˆą„¤ +2026-07-28 spec पाँच ą¤šą„€ą¤œą¤¼ą„‹ą¤‚ ą¤•ą„‹ retire करता ą¤¹ą„ˆą„¤ SDK अब ą¤­ą„€ ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ हर ą¤ą¤• ą¤•ą„‹ implement करता ą¤¹ą„ˆ, और अब हर ą¤ą¤• पर **deprecation warning** ą¤²ą¤—ą„€ ą¤¹ą„ˆą„¤ ą¤•ą„ą¤› SDK-level deprecations ą¤…ą¤Ŗą¤Øą„€ अलग वजह ą¤øą„‡ ą¤¹ą„ˆą¤‚ और [आख़िर ą¤®ą„‡ą¤‚](#deprecated-sdk-helpers) ą¤¦ą„€ ą¤—ą¤ˆ ą¤¹ą„ˆą¤‚ą„¤ ą¤Øą„€ą¤šą„‡ ą¤¦ą„€ ą¤—ą¤ˆ table हर deprecated feature का नाम, ą¤‰ą¤øą¤•ą„‡ ą¤¹ą¤Ÿą¤Øą„‡ ą¤•ą„€ वजह, और ą¤‰ą¤øą¤•ą„€ ą¤œą¤—ą¤¹ किस replacement पर build करना ą¤¹ą„ˆ, यह ą¤¬ą¤¤ą¤¾ą¤¤ą„€ ą¤¹ą„ˆą„¤ @@ -137,11 +137,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## Deprecated SDK helpers {#deprecated-sdk-helpers} -ą¤Æą„‡ spec ą¤•ą„‡ बदलाव ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą¤‚, ą¤øą¤æą¤°ą„ą¤«ą¤¼ SDK ą¤•ą„‡ ą¤…ą¤‚ą¤¦ą¤°ą„‚ą¤Øą„€ ą¤¹ą¤æą¤øą„ą¤øą„‡ ą¤¹ą„ˆą¤‚ ą¤œą¤æą¤Øą¤•ą¤¾ ą¤¬ą„‡ą¤¹ą¤¤ą¤° replacement ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ ą¤Æą„‡ ą¤‰ą¤øą„€ `MCPDeprecationWarning` ą¤•ą„‡ साऄ warn ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और 3.0 ą¤®ą„‡ą¤‚ हटा ą¤¦ą¤æą¤ ą¤œą¤¾ą¤ą¤ą¤—ą„‡ą„¤ +ą¤Æą„‡ spec ą¤•ą„‡ बदलाव ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą¤‚, ą¤øą¤æą¤°ą„ą¤«ą¤¼ SDK ą¤•ą„‡ ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą„‡ ą¤µą„‡ ą¤¤ą¤°ą„€ą¤•ą„‡ ą¤¹ą„ˆą¤‚ ą¤œą¤æą¤Øą¤•ą¤¾ ą¤¬ą„‡ą¤¹ą¤¤ą¤° replacement ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ ą¤Æą„‡ ą¤‰ą¤øą„€ `MCPDeprecationWarning` ą¤•ą„‡ साऄ warn ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और 3.0 ą¤Ŗą„ą¤°ą¤¾ą¤Øą¤¾ ą¤°ą„‚ą¤Ŗ हटा ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ | Deprecated | ą¤‡ą¤øą¤•ą„‡ बजाय ą¤•ą„ą¤Æą¤¾ ą¤•ą¤°ą„‡ą¤‚ | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` और फिर `FuncMetadata.call_fn()`ą„¤ ą¤‡ą¤øą„‡ ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤µą¤¹ą„€ code call करता ऄा ą¤œą„‹ `FuncMetadata` ą¤•ą„‹ ą¤øą„€ą¤§ą„‡ चलाता ą¤¹ą„ˆ (ą¤œą„ˆą¤øą„‡ ą¤•ą„‹ą¤ˆ custom `Tool` subclass)ą„¤ | +| `validate_token_resource=` ą¤•ą„‡ बिना `AuthSettings(resource_server_url=...)` | ą¤‡ą¤øą„‡ set ą¤•ą¤°ą„‡ą¤‚: `True` पर server ą¤µą„‡ bearer tokens ą¤ ą„ą¤•ą¤°ą¤¾ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤Øą„ą¤¹ą„‡ą¤‚ आपका verifier `resource_server_url` ą¤•ą„‡ ą¤²ą¤æą¤ ą¤œą¤¾ą¤°ą„€ ą¤¹ą„ą¤† ą¤Øą¤¹ą„€ą¤‚ बताता, `False` का मतलब ą¤¹ą„ˆ कि आपका verifier token का audience ą¤–ą¤¼ą„ą¤¦ जाँचता ą¤¹ą„ˆ (**[Authorization](run/authorization.md#a-token-verifier)** ą¤¦ą„‡ą¤–ą„‡ą¤‚)ą„¤ set न ą¤¹ą„‹ą¤Øą„‡ पर ą¤µą„ą¤Æą¤µą¤¹ą¤¾ą¤° `False` ą¤œą„ˆą¤øą¤¾ ą¤¹ą„ˆ; 3.0 ą¤®ą„‡ą¤‚ जब ą¤­ą„€ `resource_server_url` set ą¤¹ą„‹, default `True` ą¤¹ą„‹ą¤—ą¤¾ą„¤ | +| `issuer=` ą¤•ą„‡ बिना `ClientCredentialsOAuthProvider(...)` या `PrivateKeyJWTOAuthProvider(...)` | `issuer=` ą¤¦ą„‡ą¤‚, ą¤œą¤æą¤øą¤®ą„‡ą¤‚ credentials ą¤œą¤¾ą¤°ą„€ ą¤•ą¤°ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ authorization server का नाम ą¤¹ą„‹ (**[OAuth clients लिखना](client/oauth-clients.md#machine-to-machine)** ą¤¦ą„‡ą¤–ą„‡ą¤‚)ą„¤ ą¤‡ą¤øą¤•ą„‡ बिना MCP server तय करता ą¤¹ą„ˆ कि credentials किस authorization server ą¤•ą„‹ ą¤®ą¤æą¤²ą„‡ą¤‚; 3.0 ą¤®ą„‡ą¤‚ यह keyword ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤¹ą„‹ ą¤œą¤¾ą¤ą¤—ą¤¾ą„¤ | ## सारांश {#recap} @@ -150,7 +152,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Deprecated ą¤¹ą„‹ą¤Øą¤¾ बस सलाह भर ą¤¹ą„ˆ: wire ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ बदलाव ą¤Øą¤¹ą„€ą¤‚, 2026 ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤•ą„‡ sessions पर सब ą¤•ą„ą¤› काम करता रहता ą¤¹ą„ˆ, और ą¤†ą¤Ŗą¤•ą„‹ साफ़ ą¤¦ą¤æą¤–ą¤Øą„‡ ą¤µą¤¾ą¤²ą„€ `MCPDeprecationWarning` ą¤®ą¤æą¤²ą¤¤ą„€ ą¤¹ą„ˆ (यह `UserWarning` ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ ą¤šą¤¾ą¤²ą„‚ ą¤¹ą„ˆ)ą„¤ * sampling और roots ą¤•ą„‹ ą¤‡ą¤øą¤•ą„‡ अलावा back-channel ą¤šą¤¾ą¤¹ą¤æą¤ ą¤œą„‹ 2026-07-28 session ą¤•ą„‡ पास ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ modern connection पर ą¤Æą„‡ warn ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और फिर raise ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` ą¤Ŗą„‚ą¤°ą„€ category ą¤•ą„‹ ą¤šą„ą¤Ŗ कराता ą¤¹ą„ˆ; pytest ą¤®ą„‡ą¤‚ `"error::mcp.MCPDeprecationWarning"` ą¤‡ą¤øą„‡ test failure ą¤®ą„‡ą¤‚ बदल ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ -* ą¤ą¤• SDK helper, `FuncMetadata.call_fn_with_arg_validation()`, अलग ą¤øą„‡ deprecated ą¤¹ą„ˆ और 3.0 ą¤®ą„‡ą¤‚ हटाया ą¤œą¤¾ą¤ą¤—ą¤¾ą„¤ +* [SDK-level deprecations](#deprecated-sdk-helpers) पर ą¤­ą„€ ą¤Æą¤¹ą„€ नियम ą¤²ą¤¾ą¤—ą„‚ ą¤¹ą„ˆ: ą¤…ą¤­ą„€ ą¤Æą„‡ warn ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और 3.0 ą¤Ŗą„ą¤°ą¤¾ą¤Øą¤¾ ą¤°ą„‚ą¤Ŗ हटा ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * नया code ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą¤æą¤øą„€ पर ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ बनना ą¤šą¤¾ą¤¹ą¤æą¤ą„¤ इन docs का ą¤¬ą¤¾ą¤•ą„€ हर page ą¤®ą„Œą¤œą„‚ą¤¦ą¤¾ API सिखाता ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/get-started/first-steps.md b/i18n/hi/pages/get-started/first-steps.md index 89dffcdc..3d00d05f 100644 --- a/i18n/hi/pages/get-started/first-steps.md +++ b/i18n/hi/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # ą¤Ŗą¤¹ą¤²ą„‡ कदम {#first-steps} @@ -17,7 +17,7 @@ translation: * **client** host ą¤•ą„‡ अंदर रहता ą¤¹ą„ˆ और MCP ą¤¬ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ host ą¤œą¤æą¤¤ą¤Øą„‡ servers ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¾ ą¤¹ą„ˆ, हर ą¤ą¤• ą¤•ą„‡ ą¤²ą¤æą¤ ą¤ą¤• client चलाता ą¤¹ą„ˆą„¤ * **server** वह ą¤¹ą„ˆ ą¤œą„‹ आप इस SDK ą¤øą„‡ ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ यह clients ą¤•ą„‹ ą¤šą„€ą¤œą¤¼ą„‡ą¤‚ expose करता ą¤¹ą„ˆą„¤ यह model ą¤øą„‡ ą¤øą„€ą¤§ą„‡ ą¤•ą¤­ą„€ बात ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ -server आप ą¤²ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ hosts ą¤•ą¤æą¤øą„€ और का product ą¤¹ą„ˆą¤‚ą„¤ SDK ą¤†ą¤Ŗą¤•ą„‹ ą¤ą¤• `Client` ą¤­ą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤øą„‡ आप ą¤…ą¤Ŗą¤Øą„‡ servers test ą¤•ą¤°ą„‡ą¤‚ą¤—ą„‡, और यह ą¤‡ą¤øą„€ page पर ą¤†ą¤—ą„‡ दिखता ą¤¹ą„ˆą„¤ +server आप ą¤²ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ hosts ą¤•ą¤æą¤øą„€ और का product ą¤¹ą„ˆą¤‚ą„¤ SDK ą¤†ą¤Ŗą¤•ą„‹ `Client` ą¤­ą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤µą¤¹ą„€ class ą¤œą¤æą¤øą¤øą„‡ ą¤•ą„‹ą¤ˆ host URL ą¤øą„‡ server तक ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆ या ą¤‰ą¤øą„‡ subprocess ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ launch करता ą¤¹ą„ˆą„¤ यह ą¤‡ą¤øą„€ page पर ą¤†ą¤—ą„‡ दिखता ą¤¹ą„ˆ, और ą¤‡ą¤øą„€ ą¤øą„‡ आप ą¤…ą¤Ŗą¤Øą„‡ servers test ą¤­ą„€ ą¤•ą¤°ą„‡ą¤‚ą¤—ą„‡ą„¤ ## ą¤¤ą„€ą¤Ø primitives {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector ą¤®ą„‡ą¤‚ ą¤†ą¤Ŗą¤Øą„‡ ą¤¤ą„€ą¤Ø tabs ą¤¦ą„‡ą¤–ą„‡ą„¤ ą¤‰ą¤øą„‡ ą¤•ą„ˆ जब client ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, server ą¤…ą¤Ŗą¤Øą„€ **capabilities** declare करता ą¤¹ą„ˆ: requests ą¤•ą„‡ ą¤•ą„Œą¤Ø-ą¤øą„‡ ą¤Ŗą¤°ą¤æą¤µą¤¾ą¤°ą„‹ą¤‚ का वह जवाब ą¤¦ą„‡ą¤—ą¤¾ą„¤ client ą¤‰ą¤øą„€ declaration ą¤øą„‡ तय करता ą¤¹ą„ˆ कि ą¤®ą¤¾ą¤ą¤—ą„‡ ą¤­ą„€ ą¤¤ą„‹ ą¤•ą„ą¤Æą¤¾ą„¤ ą¤†ą¤Ŗą¤Øą„‡ यह ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ लिखा; `MCPServer` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą„‡ declare करता ą¤¹ą„ˆą„¤ -ą¤–ą„ą¤¦ ą¤¦ą„‡ą¤–ą„‡ą¤‚ą„¤ SDK का `Client` ą¤øą„€ą¤§ą„‡ server object ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और ą¤‰ą¤øą¤øą„‡ **in memory** ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ (न subprocess, न port): +ą¤–ą„ą¤¦ ą¤¦ą„‡ą¤–ą„‡ą¤‚ą„¤ ą¤ą¤• terminal ą¤®ą„‡ą¤‚ `server.py` ą¤•ą„‹ HTTP पर चलता ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤‚: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +और ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ client ą¤•ą„‹ ą¤‰ą¤øą¤øą„‡ ą¤œą„‹ą¤”ą¤¼ą„‡ą¤‚: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ asyncio.run(main()) ą¤§ą„ą¤Æą¤¾ą¤Ø ą¤¦ą„‡ą¤‚ कि ą¤•ą„ą¤Æą¤¾ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ `completions` (resource templates और prompts ą¤•ą„‡ ą¤²ą¤æą¤ argument autocomplete) ą¤•ą„‹ आपका लिखा handler ą¤šą¤¾ą¤¹ą¤æą¤, इस server ą¤®ą„‡ą¤‚ वह ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ capability ą¤®ą„Œą¤œą„‚ą¤¦ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ और ą¤øą¤²ą„€ą¤•ą„‡ वाला client ą¤Ŗą„‚ą¤›ą„‡ą¤—ą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ हर optional ą¤šą„€ą¤œą¤¼ का ą¤Æą¤¹ą„€ नियम ą¤¹ą„ˆ: ą¤šą„€ą¤œą¤¼ register ą¤•ą¤°ą„‡ą¤‚ और capability आ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆ; **[Completions](../servers/completions.md)** ą¤‡ą¤øą„‡ साबित करता ą¤¹ą„ˆą„¤ !!! info - `Client(mcp)` ą¤µą¤¹ą„€ in-memory client ą¤¹ą„ˆ ą¤œą¤æą¤øą¤øą„‡ इन docs का हर उदाहरण test ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, और - ą¤‡ą¤øą„€ ą¤øą„‡ आप ą¤…ą¤Ŗą¤Øą„‡ servers test ą¤•ą¤°ą„‡ą¤‚ą¤—ą„‡ą„¤ ą¤‡ą¤øą„‡ ą¤Ŗą„‚ą¤°ą¤¾ ą¤ą¤• page मिलता ą¤¹ą„ˆ: **[Testing](testing.md)**ą„¤ + वह `client.py` ą¤Ŗą„‚ą¤°ą¤¾ MCP client ą¤¹ą„ˆ, और **[Client](../client/index.md)** उसका page ą¤¹ą„ˆą„¤ + test ą¤®ą„‡ą¤‚ आप terminal और port ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और `Client` ą¤•ą„‹ ą¤øą„€ą¤§ą„‡ server object ą¤¹ą„€ ऄमा ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, + `Client(mcp)`ą„¤ ą¤‰ą¤øą„‡ ą¤­ą„€ ą¤Ŗą„‚ą¤°ą¤¾ ą¤ą¤• page मिलता ą¤¹ą„ˆ: **[Testing](testing.md)**ą„¤ ## ą¤œą„‹ ą¤†ą¤Ŗą¤Øą„‡ ą¤Øą¤¹ą„€ą¤‚ लिखा {#what-you-did-not-write} @@ -128,7 +127,7 @@ asyncio.run(main()) * JSON Schemaą„¤ `a: int, b: int` **ą¤¹ą„€** `add` का schema ą¤¹ą„ˆą„¤ * request handlerą„¤ `tools/list`, `resources/read`, `prompts/get`: सब ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ serve ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * capability declarationą„¤ `MCPServer` ą¤Øą„‡ ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ बना ą¤¦ą¤æą¤Æą¤¾ą„¤ -* protocol ą¤•ą„€ ą¤ą¤• ą¤­ą„€ lineą„¤ version negotiation, JSON-RPC framing, capability exchange: यह सब `mcp dev` और `Client(mcp)` ą¤•ą„‡ अंदर ą¤¹ą„ą¤†, और ą¤†ą¤Ŗą¤Øą„‡ ą¤•ą¤­ą„€ ą¤¦ą„‡ą¤–ą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ +* protocol ą¤•ą„€ ą¤ą¤• ą¤­ą„€ lineą„¤ version negotiation, JSON-RPC framing, capability exchange: यह सब `mcp dev` और `client.py` ą¤•ą„‡ अंदर ą¤¹ą„ą¤†, और ą¤†ą¤Ŗą¤Øą„‡ ą¤•ą¤­ą„€ ą¤¦ą„‡ą¤–ą¤¾ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤Æą¤¹ą„€ ą¤…ą¤Øą„ą¤Ŗą¤¾ą¤¤ SDK का ą¤Ŗą„‚ą¤°ą¤¾ मतलब ą¤¹ą„ˆą„¤ @@ -139,6 +138,6 @@ asyncio.run(main()) * हर primitive ą¤•ą„‡ ą¤²ą¤æą¤ ą¤ą¤• decorator: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`ą„¤ नाम, description और schema function ą¤øą„‡ ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * `{param}` वाला URI resource **template** बनाता ą¤¹ą„ˆ, ą¤œą„‹ concrete resources ą¤øą„‡ अलग ą¤øą„‚ą¤šą„€ ą¤®ą„‡ą¤‚ दिखता ą¤¹ą„ˆą„¤ * server ą¤•ą„€ **capabilities** ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ declare ą¤¹ą„‹ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą¤‚, और client ą¤µą¤¹ą„€ माँगता ą¤¹ą„ˆ ą¤œą„‹ server declare करता ą¤¹ą„ˆą„¤ -* `Client(mcp)` server object ą¤øą„‡ in memory ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ: ą¤Ŗą¤¹ą¤²ą„‡ दिन ą¤øą„‡ आपका test harnessą„¤ +* `Client("http://localhost:8000/mcp")` ą¤†ą¤Ŗą¤•ą„‡ ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ą¤ server ą¤øą„‡ बात करता ą¤¹ą„ˆą„¤ ą¤‰ą¤øą¤•ą„€ ą¤œą¤—ą¤¹ ą¤‡ą¤øą„‡ server object ऄमा ą¤¦ą„‡ą¤‚, `Client(mcp)`, और ą¤Ŗą¤¹ą¤²ą„‡ दिन ą¤øą„‡ ą¤Æą¤¹ą„€ आपका test harness ą¤¹ą„ˆą„¤ ą¤†ą¤—ą„‡ ą¤¹ą„ˆ **[ą¤…ą¤øą¤²ą„€ host ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą„‡ą¤‚](real-host.md)**: ą¤Æą¤¹ą„€ server Claude Desktop या ą¤•ą¤æą¤øą„€ IDE ą¤•ą„‡ अंदर, सच ą¤®ą„‡ą¤‚ą„¤ फिर **[Testing](testing.md)**: ą¤ą¤• page, ą¤ą¤• in-memory client, और ą¤†ą¤Ŗą¤•ą„‹ ą¤•ą¤­ą„€ ą¤…ą¤‚ą¤¦ą¤¾ą¤œą¤¼ą¤¾ ą¤Øą¤¹ą„€ą¤‚ लगाना ą¤Ŗą¤”ą¤¼ą„‡ą¤—ą¤¾ कि यह काम करता ą¤¹ą„ˆ या ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤‰ą¤øą¤•ą„‡ बाद हर primitive ą¤•ą„‹ अपना page मिलता ą¤¹ą„ˆ, ą¤¶ą„ą¤°ą„ą¤†ą¤¤ ą¤‰ą¤øą¤øą„‡ ą¤œą¤æą¤øą„‡ model चलाता ą¤¹ą„ˆ: **[Tools](../servers/tools.md)**ą„¤ diff --git a/i18n/hi/pages/get-started/testing.md b/i18n/hi/pages/get-started/testing.md index 2e72aa19..c7cdf9b8 100644 --- a/i18n/hi/pages/get-started/testing.md +++ b/i18n/hi/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Testing {#testing} -Python SDK ą¤®ą„‡ą¤‚ `Client` class ą¤†ą¤¤ą„€ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„‡ साऄ **in-memory transport** मिलता ą¤¹ą„ˆ: ą¤‡ą¤øą„‡ अपना server object ą¤¦ą„‡ą¤‚ और यह ą¤‰ą¤øą¤øą„‡ ą¤øą„€ą¤§ą„‡ ą¤œą„ą¤”ą¤¼ जाता ą¤¹ą„ˆą„¤ +SDK ą¤•ą„€ `Client` class, ą¤µą¤¹ą„€ ą¤œą„‹ ą¤•ą¤æą¤øą„€ URL ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą„€ ą¤¹ą„ˆ या subprocess ą¤¶ą„ą¤°ą„‚ ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆ, **in memory** ą¤­ą„€ ą¤œą„ą¤”ą¤¼ą¤¤ą„€ ą¤¹ą„ˆ: ą¤‡ą¤øą„‡ अपना server object ą¤¦ą„‡ą¤‚ और यह ą¤‰ą¤øą¤øą„‡ ą¤øą„€ą¤§ą„‡ बात ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆą„¤ -ą¤•ą„‹ą¤ˆ subprocess ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤•ą„‹ą¤ˆ port ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤•ą„‹ą¤ˆ transport ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ यह ą¤µą¤¹ą„€ विचार ą¤¹ą„ˆ ą¤œą„‹ FastAPI ą¤•ą„‡ `TestClient` का ą¤¹ą„ˆą„¤ +ą¤•ą„‹ą¤ˆ subprocess ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤•ą„‹ą¤ˆ port ą¤Øą¤¹ą„€ą¤‚ą„¤ wire पर ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ą„¤ यह ą¤µą¤¹ą„€ विचार ą¤¹ą„ˆ ą¤œą„‹ FastAPI ą¤•ą„‡ `TestClient` का ą¤¹ą„ˆą„¤ ## Basic usage {#basic-usage} @@ -91,7 +91,7 @@ tool body ą¤•ą„‡ **बाहर** का failure अलग ą¤¹ą„ˆą„¤ `Client(mcp tests ą¤®ą„‡ą¤‚ ą¤‡ą¤øą„‡ ą¤šą¤¾ą¤²ą„‚ ą¤°ą¤¹ą¤Øą„‡ ą¤¦ą„‡ą¤‚ą„¤ production code ą¤®ą„‡ą¤‚ इसका ą¤•ą„‹ą¤ˆ मतलब ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ -## Default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ in-process {#in-process-by-default} +## Default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ ą¤Ŗą„€ą¤¢ą¤¼ą„€-ą¤Øą¤æą¤°ą¤Ŗą„‡ą¤•ą„ą¤· {#era-neutral-by-default} !!! note `Client(mcp)` in-process ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ और default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ **ą¤Ŗą„€ą¤¢ą¤¼ą„€-ą¤Øą¤æą¤°ą¤Ŗą„‡ą¤•ą„ą¤·** ą¤¹ą„ˆ: यह server ą¤•ą„‹ probe करता ą¤¹ą„ˆ और diff --git a/i18n/hi/pages/handlers/multi-round-trip.md b/i18n/hi/pages/handlers/multi-round-trip.md index 251145ad..4928269c 100644 --- a/i18n/hi/pages/handlers/multi-round-trip.md +++ b/i18n/hi/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # Multi-round-trip requests {#multi-round-trip-requests} @@ -164,7 +164,7 @@ low-level `Server` बिना-batteries वाला ą¤øą„ą¤¤ą¤° ą¤¹ą„ˆ: `MCP ## ą¤ą¤• 2026-07-28 result {#a-2026-07-28-result} -`InputRequiredResult` ą¤øą¤æą¤°ą„ą¤«ą¤¼ protocol version **2026-07-28** पर ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ in-memory `Client(server)` ą¤‡ą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ negotiate करता ą¤¹ą„ˆ; wire पर, `mode="auto"` ą¤‡ą¤øą„‡ ą¤–ą„‹ą¤œ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ connect ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ बाद `client.protocol_version` बताता ą¤¹ą„ˆ कि ą¤†ą¤Ŗą¤•ą„‹ ą¤•ą„ą¤Æą¤¾ ą¤®ą¤æą¤²ą¤¾ą„¤ +`InputRequiredResult` ą¤øą¤æą¤°ą„ą¤«ą¤¼ protocol version **2026-07-28** पर ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆą„¤ `Client` का default `mode="auto"` ą¤‡ą¤øą„‡ ą¤•ą¤æą¤øą„€ ą¤­ą„€ connection पर ą¤–ą„‹ą¤œ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ connect ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ बाद `client.protocol_version` बताता ą¤¹ą„ˆ कि ą¤†ą¤Ŗą¤•ą„‹ ą¤•ą„ą¤Æą¤¾ ą¤®ą¤æą¤²ą¤¾ą„¤ !!! warning pre-2026 session ą¤•ą„‡ पास `InputRequiredResult` ą¤°ą¤–ą¤Øą„‡ ą¤•ą„€ ą¤•ą„‹ą¤ˆ ą¤œą¤—ą¤¹ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ `mode="legacy"` connection पर diff --git a/i18n/hi/pages/handlers/progress.md b/i18n/hi/pages/handlers/progress.md index b7b3f16c..0ccf140b 100644 --- a/i18n/hi/pages/handlers/progress.md +++ b/i18n/hi/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progress {#progress} @@ -27,21 +27,19 @@ translation: ## Client ą¤øą„‡ ą¤øą„ą¤Øą„‡ą¤‚ {#listen-for-it-from-the-client} -Client **हर call पर** अलग ą¤øą„‡ opt in करता ą¤¹ą„ˆ, `call_tool` ą¤•ą„‹ `progress_callback=` ą¤¦ą„‡ą¤•ą¤°: +client **हर call पर** अलग ą¤øą„‡ opt in करता ą¤¹ą„ˆ, `call_tool` ą¤•ą„‹ `progress_callback=` ą¤¦ą„‡ą¤•ą¤°: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -53,30 +51,34 @@ async def main() -> None: anyio.run(main) ``` -Callback ą¤ą¤• `async` function ą¤¹ą„ˆ ą¤œą„‹ ą¤ ą„€ą¤• ą¤µą¤¹ą„€ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ server ą¤Øą„‡ report किया: `progress`, `total`, `message`ą„¤ +callback ą¤ą¤• `async` function ą¤¹ą„ˆ ą¤œą„‹ ą¤ ą„€ą¤• ą¤µą¤¹ą„€ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ server ą¤Øą„‡ report किया: `progress`, `total`, `message`ą„¤ !!! info - `Client(mcp)` ą¤øą„€ą¤§ą„‡ server object ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆ, memory ą¤®ą„‡ą¤‚, ą¤µą¤¹ą„€ client जिस पर **[Testing](../get-started/testing.md)** - page बना ą¤¹ą„ˆą„¤ `Client` ą¤šą¤¾ą¤¹ą„‡ ą¤•ą„‹ą¤ˆ ą¤­ą„€ transport ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą„‡, `progress_callback` parameter ą¤µą¤¹ą„€ रहता ą¤¹ą„ˆ; - ą¤œą„‹ **timing** आप ą¤…ą¤­ą„€ ą¤¦ą„‡ą¤–ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ ą¤¹ą„ˆą¤‚ वह in-memory connection ą¤•ą„€ ą¤¹ą„ˆą„¤ वह आपका callback inline चलाता ą¤¹ą„ˆ, - ą¤‡ą¤øą¤²ą¤æą¤ हर report `call_tool` ą¤•ą„‡ ą¤²ą„Œą¤Ÿą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤Ŗą¤¹ą„ą¤ą¤š ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą„¤ ą¤…ą¤øą¤²ą„€ transport पर notifications और result - ą¤®ą„‡ą¤‚ ą¤¹ą„‹ą¤”ą¤¼ ą¤²ą¤—ą¤¤ą„€ ą¤¹ą„ˆ, और ą¤ą¤• ą¤§ą„€ą¤®ą¤¾ callback `call_tool` ą¤•ą„‡ ą¤²ą„Œą¤Ÿą¤Øą„‡ ą¤•ą„‡ बाद ą¤­ą„€ चल रहा ą¤¹ą„‹ सकता ą¤¹ą„ˆą„¤ + `Client` ą¤•ą„‹ ą¤†ą¤Ŗą¤Øą„‡ ą¤šą¤¾ą¤¹ą„‡ ą¤œą„‹ ą¤­ą„€ दिया ą¤¹ą„‹, `progress_callback` parameter ą¤µą¤¹ą„€ रहता ą¤¹ą„ˆ: यहाँ ą¤•ą„€ तरह + URL, ą¤•ą„‹ą¤ˆ `StdioServerParameters`, या test ą¤®ą„‡ą¤‚ server objectą„¤ हाँ, ą¤…ą¤øą¤²ą„€ transport पर timing का + ą¤§ą„ą¤Æą¤¾ą¤Ø ą¤°ą¤–ą„‡ą¤‚ą„¤ हर notification ą¤…ą¤Ŗą¤Øą„‡ आप ą¤®ą„‡ą¤‚ अलग, response ą¤•ą„‡ साऄ-साऄ ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ ą¤ą¤• ą¤§ą„€ą¤®ą¤¾ + callback `call_tool` ą¤•ą„‡ ą¤²ą„Œą¤Ÿą¤Øą„‡ ą¤•ą„‡ बाद ą¤­ą„€ चल रहा ą¤¹ą„‹ सकता ą¤¹ą„ˆą„¤ ą¤øą¤æą¤°ą„ą¤«ą¤¼ in-process test connection + ą¤¹ą„€ callback ą¤•ą„‹ inline चलाता ą¤¹ą„ˆ और यह ą¤Ŗą¤•ą„ą¤•ą¤¾ करता ą¤¹ą„ˆ कि हर report ą¤Ŗą¤¹ą¤²ą„‡ ą¤Ŗą¤¹ą„ą¤ą¤šą„‡ą„¤ ### ą¤‡ą¤øą„‡ ą¤†ą¤œą¤¼ą¤®ą¤¾ą¤ą¤ {#try-it} -`client.py` ą¤•ą„‹ `server.py` ą¤•ą„‡ बगल ą¤®ą„‡ą¤‚ ą¤°ą¤–ą„‡ą¤‚ और ą¤šą¤²ą¤¾ą¤ą¤: +`server.py` ą¤•ą„‹ HTTP पर serve ą¤•ą¤°ą„‡ą¤‚, फिर ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ client ą¤šą¤²ą¤¾ą¤ą¤: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Server पर हर `await ctx.report_progress(...)` client पर `show` का ą¤ą¤• call बना, ą¤‰ą¤øą„€ ą¤•ą„ą¤°ą¤® ą¤®ą„‡ą¤‚, और ą¤¦ą„‹ą¤Øą„‹ą¤‚ lines `call_tool` ą¤•ą„‡ ą¤²ą„Œą¤Ÿą¤Øą„‡ ą¤øą„‡ **ą¤Ŗą¤¹ą¤²ą„‡** print ą¤¹ą„ą¤ˆą¤‚ą„¤ Progress result ą¤®ą„‡ą¤‚ बंऔल ą¤¹ą„‹ą¤•ą¤° ą¤Øą¤¹ą„€ą¤‚ आता; tool ą¤•ą„‡ काम ą¤•ą¤°ą¤¤ą„‡ ą¤°ą¤¹ą¤Øą„‡ ą¤•ą„‡ ą¤¦ą„Œą¤°ą¤¾ą¤Ø ą¤¹ą„€ stream ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +server पर हर `await ctx.report_progress(...)` client पर `show` का ą¤ą¤• call बना, ą¤‰ą¤øą„€ ą¤•ą„ą¤°ą¤® ą¤®ą„‡ą¤‚ą„¤ progress result ą¤®ą„‡ą¤‚ ą¤øą¤®ą„‡ą¤Ÿą¤•ą¤° ą¤Øą¤¹ą„€ą¤‚ ą¤­ą„‡ą¤œą¤¾ ą¤œą¤¾ą¤¤ą¤¾ą„¤ वह tool ą¤•ą„‡ काम ą¤•ą¤°ą¤¤ą„‡ ą¤°ą¤¹ą¤Øą„‡ ą¤•ą„‡ ą¤¦ą„Œą¤°ą¤¾ą¤Ø ą¤¹ą„€ stream ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ !!! warning `progress_callback` **call** का ą¤¹ą„ˆ, `Client` का ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤‡ą¤øą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„‹ą¤ˆ constructor argument ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, diff --git a/i18n/hi/pages/protocol-versions.md b/i18n/hi/pages/protocol-versions.md index 79eecb1c..8280d042 100644 --- a/i18n/hi/pages/protocol-versions.md +++ b/i18n/hi/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Protocol versions {#protocol-versions} @@ -11,9 +11,17 @@ MCP ą¤•ą„€ ą¤¦ą„‹ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤ ą¤¹ą„ˆą¤‚ą„¤ ą¤†ą¤Ŗą¤•ą„‹ ą¤‡ą¤øą¤•ą„€ ą¤šą¤æą¤‚ą¤¤ą¤¾ लगभग ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤Øą„€ ą¤Ŗą¤”ą¤¼ą¤¤ą„€, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ `Client` ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ negotiate कर ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ यह page उस ą¤ą¤• constructor argument ą¤•ą„‡ ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ ą¤¹ą„ˆ ą¤œą„‹ ą¤‡ą¤øą„‡ ą¤Øą¤æą¤Æą¤‚ą¤¤ą„ą¤°ą¤æą¤¤ करता ą¤¹ą„ˆ, `mode=`, और उन ą¤¤ą„€ą¤Ø ą¤®ą„Œą¤•ą„‹ą¤‚ ą¤•ą„‡ ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ जब आप ą¤‡ą¤øą„‡ ą¤¬ą¤¦ą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +इस page का हर snippet ą¤ą¤• `client.py` ą¤¹ą„ˆ ą¤œą„‹ **[Client](client/index.md)** ą¤µą¤¾ą¤²ą„‡ Bookshop `server.py` ą¤øą„‡ बात करता ą¤¹ą„ˆą„¤ उस server ą¤•ą„‹ ą¤ą¤• terminal ą¤®ą„‡ą¤‚ ą¤¶ą„ą¤°ą„‚ ą¤•ą¤°ą„‡ą¤‚: + +```console +uv run mcp run server.py --transport streamable-http +``` + +फिर हर snippet ą¤•ą„‹ ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤®ą„‡ą¤‚ `python client.py` ą¤øą„‡ ą¤šą¤²ą¤¾ą¤ą¤ą„¤ + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ MCP ą¤•ą„€ ą¤¦ą„‹ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤ ą¤¹ą„ˆą¤‚ą„¤ ą¤Ŗą„‚ą¤°ą¤¾ feature बस इतना ą¤¹ą„€ ą¤¹ą„ˆą„¤ ą¤ą¤• `Client`, ą¤•ą¤æą¤øą„€ ą¤­ą„€ ą¤Ŗą„€ą¤¢ą¤¼ą„€ का server, और code ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ branching ą¤Øą¤¹ą„€ą¤‚ą„¤ !!! info - `MCPServer` हर transport पर `server/discover` का जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ — in-memory, stdio, streamable - HTTP — ą¤‡ą¤øą¤²ą¤æą¤ ą¤†ą¤Ŗą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ server ą¤•ą„‡ साऄ `auto` ą¤¹ą¤®ą„‡ą¤¶ą¤¾ `2026-07-28` पर ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆą„¤ fallback ą¤øą¤æą¤°ą„ą¤«ą¤¼ - ą¤…ą¤øą¤²ą„€ pre-2026 server ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ ą¤¹ą„€ चलता ą¤¹ą„ˆ, और ą¤ ą„€ą¤• ą¤µą¤¹ą„€ą¤‚ आप ą¤‡ą¤øą„‡ ą¤šą¤¾ą¤¹ą¤¤ą„‡ ą¤­ą„€ ą¤¹ą„ˆą¤‚ą„¤ + `MCPServer` हर transport पर `server/discover` का जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ — Streamable HTTP, stdio, और वह + in-process connection ą¤œą„‹ ą¤†ą¤Ŗą¤•ą„‡ tests ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ — ą¤‡ą¤øą¤²ą¤æą¤ ą¤†ą¤Ŗą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ server ą¤•ą„‡ साऄ `auto` ą¤¹ą¤®ą„‡ą¤¶ą¤¾ + `2026-07-28` पर ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆą„¤ fallback ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤…ą¤øą¤²ą„€ pre-2026 server ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ ą¤¹ą„€ चलता ą¤¹ą„ˆ, और ą¤ ą„€ą¤• ą¤µą¤¹ą„€ą¤‚ + आप ą¤‡ą¤øą„‡ ą¤šą¤¾ą¤¹ą¤¤ą„‡ ą¤­ą„€ ą¤¹ą„ˆą¤‚ą„¤ ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -57,11 +66,11 @@ server-initiated request का मतलब ą¤¹ą„ˆ server का **ą¤†ą¤Ŗą¤•ą„‹ `mode="auto"` handshake ą¤¤ą¤­ą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ जब server इतना ą¤Ŗą„ą¤°ą¤¾ą¤Øą¤¾ ą¤¹ą„‹ कि और ą¤•ą„ą¤› ą¤šą¤²ą„‡ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ `mode="legacy"` ą¤‡ą¤øą¤•ą„€ ą¤—ą¤¾ą¤°ą¤‚ą¤Ÿą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ जब ą¤­ą„€ आप `Client(...)` ą¤•ą„‹ `sampling_callback`, request ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ चलाया ą¤œą¤¾ą¤Øą„‡ वाला `elicitation_callback`, या `message_handler` ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤‡ą¤øą„‡ ą¤šą„ą¤Øą„‡ą¤‚ą„¤ **[Client callbacks](client/callbacks.md)** ą¤®ą„‡ą¤‚ हर ą¤ą¤• ą¤•ą„€ बात ą¤µą¤æą¤øą„ą¤¤ą¤¾ą¤° ą¤øą„‡ ą¤¹ą„ˆą„¤ -## Version pin करना {#pinning-a-version} +## version pin करना {#pinning-a-version} `mode` modern protocol version string ą¤­ą„€ ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° करता ą¤¹ą„ˆą„¤ ą¤†ą¤œ यह set ą¤ ą„€ą¤• `["2026-07-28"]` ą¤¹ą„ˆą„¤ -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ probe ą¤øą¤øą„ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤²ą„‡ą¤•ą¤æą¤Ø फिर ą¤­ą„€ यह ą¤ą¤• rou ą¤‡ą¤øą¤²ą¤æą¤ ą¤‡ą¤øą„‡ संभाल कर ą¤°ą¤–ą„‡ą¤‚ą„¤ `auto` connection ą¤•ą„‡ बाद `client.session.discover_result` ą¤®ą„‡ą¤‚ ą¤ ą„€ą¤• ą¤µą¤¹ą„€ `DiscoverResult` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ server ą¤Øą„‡ ą¤­ą„‡ą¤œą¤¾ ऄा: ą¤‰ą¤øą¤•ą„‡ `supported_versions`, ą¤‰ą¤øą¤•ą„€ `capabilities`, ą¤‰ą¤øą¤•ą„‡ `instructions`, और वह पहचान ą¤œą„‹ server ą¤Øą„‡ result ą¤•ą„‡ `_meta` ą¤®ą„‡ą¤‚ ą¤¦ą¤°ą„ą¤œ ą¤•ą„€ ą¤„ą„€ą„¤ ą¤…ą¤—ą¤²ą„€ बार ą¤‡ą¤øą„‡ `prior_discover=` ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ वापस ą¤¦ą„‡ą¤‚: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/hi/pages/run/asgi.md b/i18n/hi/pages/run/asgi.md index aab87362..c336e7eb 100644 --- a/i18n/hi/pages/run/asgi.md +++ b/i18n/hi/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # ą¤®ą„Œą¤œą„‚ą¤¦ą¤¾ app ą¤®ą„‡ą¤‚ ą¤œą„‹ą¤”ą¤¼ą¤Øą¤¾ {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Starlette का `Host` route ą¤‡ą¤øą„€ तरह काम करता ą¤¹ą„ˆ: --8<-- "docs_src/asgi/tutorial004.py" ``` -अब clients `/notes` ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `/notes/mcp` ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ą„¤ +अब clients `/notes/` ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `/notes/mcp` ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ą„¤ ## browser clients ą¤•ą„‡ ą¤²ą¤æą¤ CORS {#cors-for-browser-clients} @@ -142,4 +142,4 @@ browser-based client ą¤•ą„‹ ą¤†ą¤Ŗą¤øą„‡ ą¤¦ą„‹ ą¤…ą¤Øą„ą¤®ą¤¤ą¤æą¤Æą¤¾ą¤ च * browser clients ą¤•ą„‹ CORS ą¤šą¤¾ą¤¹ą¤æą¤: `Mcp-*` request headers ą¤•ą„‡ ą¤²ą¤æą¤ `allow_headers`, response ą¤•ą„‡ ą¤²ą¤æą¤ `expose_headers=["Mcp-Session-Id"]`ą„¤ * `@mcp.custom_route()` `/mcp` ą¤•ą„‡ बगल ą¤®ą„‡ą¤‚ ą¤øą¤¾ą¤¦ą„‡, बिना authentication ą¤µą¤¾ą¤²ą„‡ HTTP endpoints ą¤œą„‹ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ -जब server ą¤…ą¤øą¤²ą„€ URL पर ą¤Ŗą¤¹ą„ą¤ą¤š ą¤®ą„‡ą¤‚ आ ą¤œą¤¾ą¤, ą¤¤ą„‹ **[Client](../client/index.md)** server object ą¤•ą„‡ बजाय ą¤‰ą¤øą„€ URL ą¤øą„‡ ą¤‰ą¤øą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +जब server ą¤…ą¤øą¤²ą„€ URL पर ą¤Ŗą¤¹ą„ą¤ą¤š ą¤®ą„‡ą¤‚ आ ą¤œą¤¾ą¤, ą¤¤ą„‹ **[Client](../client/index.md)** ą¤‰ą¤øą„€ URL ą¤øą„‡ ą¤‰ą¤øą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/run/authorization.md b/i18n/hi/pages/run/authorization.md index 40f93237..e284739d 100644 --- a/i18n/hi/pages/run/authorization.md +++ b/i18n/hi/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Authorization {#authorization} @@ -23,12 +23,12 @@ OAuth ą¤•ą„€ भाषा ą¤®ą„‡ą¤‚, आपका server **resource server** ह valid token ą¤•ą„ˆą¤øą¤¾ दिखता ą¤¹ą„ˆ, इस ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ SDK ą¤•ą„€ ą¤•ą„‹ą¤ˆ राय ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ यह आप ą¤¬ą¤¤ą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, **`TokenVerifier`** implement ą¤•ą¤°ą¤•ą„‡: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` ą¤ą¤• async method वाला protocol ą¤¹ą„ˆą„¤ `verify_token` ą¤•ą„‹ `Authorization` header ą¤øą„‡ raw token मिलता ą¤¹ą„ˆ, और token valid ą¤¹ą„‹ ą¤¤ą„‹ यह **`AccessToken`** ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ, न ą¤¹ą„‹ ą¤¤ą„‹ `None`ą„¤ ą¤‡ą¤øą¤•ą„‡ अलावा implement ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‹ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ -* यह वाला token ą¤•ą„‹ ą¤ą¤• table ą¤®ą„‡ą¤‚ ą¤¢ą„‚ą¤ą¤¢ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤…ą¤øą¤²ą„€ verifier JWT signature verify करता ą¤¹ą„ˆ या authorization server ą¤•ą„‡ token-introspection endpoint ą¤•ą„‹ call करता ą¤¹ą„ˆą„¤ वह code आपका ą¤¹ą„ˆ; SDK ą¤‰ą¤øą„‡ ą¤øą¤æą¤°ą„ą¤«ą¤¼ call करता ą¤¹ą„ˆą„¤ +* यह वाला token ą¤•ą„‹ ą¤ą¤• table ą¤®ą„‡ą¤‚ ą¤¢ą„‚ą¤ą¤¢ą¤¤ą¤¾ ą¤¹ą„ˆ; हर entry ą¤®ą„‡ą¤‚ ą¤¦ą¤°ą„ą¤œ ą¤¹ą„ˆ कि token किस resource ą¤•ą„‡ ą¤²ą¤æą¤ ą¤œą¤¾ą¤°ą„€ ą¤¹ą„ą¤† ą¤„ą¤¾ą„¤ ą¤…ą¤øą¤²ą„€ verifier JWT signature verify करता ą¤¹ą„ˆ या authorization server ą¤•ą„‡ token-introspection endpoint ą¤•ą„‹ call करता ą¤¹ą„ˆ, और `AccessToken.resource` ą¤®ą„‡ą¤‚ बताता ą¤¹ą„ˆ कि token ą¤•ą¤æą¤øą¤•ą„‡ ą¤²ą¤æą¤ ą¤œą¤¾ą¤°ą„€ ą¤¹ą„ą¤† ऄा (उसका `aud`)ą„¤ वह code आपका ą¤¹ą„ˆ; SDK ą¤‰ą¤øą„‡ ą¤øą¤æą¤°ą„ą¤«ą¤¼ call करता ą¤¹ą„ˆą„¤ * `token_verifier=` और `auth=` ą¤¹ą¤®ą„‡ą¤¶ą¤¾ साऄ ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤ą¤• ą¤•ą„‹ ą¤¦ą„‚ą¤øą¤°ą„‡ ą¤•ą„‡ बिना pass ą¤•ą¤°ą„‡ą¤‚ ą¤¤ą„‹ `MCPServer(...)` ą¤•ą„‹ą¤ˆ request serve ą¤•ą¤°ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ `ValueError` raise कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ `AuthSettings` ą¤†ą¤Ŗą¤•ą„‡ resource server का ą¤øą¤¾ą¤°ą„ą¤µą¤œą¤Øą¤æą¤• ą¤šą„‡ą¤¹ą¤°ą¤¾ ą¤¹ą„ˆ: @@ -36,6 +36,10 @@ valid token ą¤•ą„ˆą¤øą¤¾ दिखता ą¤¹ą„ˆ, इस ą¤¬ą¤¾ą¤°ą„‡ ą¤®ą„‡ą¤‚ S * `issuer_url`: वह authorization server ą¤œą„‹ ą¤†ą¤Ŗą¤•ą„‡ tokens ą¤œą¤¾ą¤°ą„€ करता ą¤¹ą„ˆą„¤ * `resource_server_url`: इस MCP endpoint का public URLą„¤ यह बताता ą¤¹ą„ˆ कि token **किस** resource ą¤•ą„‡ ą¤²ą¤æą¤ ą¤¹ą„ˆ, और discovery document ą¤­ą„€ ą¤Æą¤¹ą„€ą¤‚ रहता ą¤¹ą„ˆą„¤ * `required_scopes`: हर token ą¤®ą„‡ą¤‚ ą¤Æą„‡ ą¤øą¤­ą„€ ą¤¹ą„‹ą¤Øą„‡ ą¤¹ą„€ ą¤šą¤¾ą¤¹ą¤æą¤ą„¤ +* `validate_token_resource`: हर उस token ą¤•ą„‹ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą¤¾ `AccessToken.resource` `resource_server_url` ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ `resource_server_url` set ą¤¹ą„‹ और ą¤‡ą¤øą„‡ unset ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤‚ ą¤¤ą„‹ warning (`MCPDeprecationWarning`) ą¤†ą¤¤ą„€ ą¤¹ą„ˆ और यह `False` ą¤•ą„€ तरह ą¤¬ą¤°ą„ą¤¤ą¤¾ą¤µ करता ą¤¹ą„ˆ; 3.0 ą¤®ą„‡ą¤‚ resource servers ą¤•ą„‡ ą¤²ą¤æą¤ default `True` ą¤¹ą„‹ ą¤œą¤¾ą¤ą¤—ą¤¾ą„¤ + * ą¤‡ą¤øą„‡ तब ą¤šą¤¾ą¤²ą„‚ ą¤•ą¤°ą„‡ą¤‚ जब आपका authorization server tokens ą¤•ą„‹ उस `resource` ą¤øą„‡ बाँधता ą¤¹ą„‹ ą¤œą„‹ client ą¤Øą„‡ माँगा ऄा, और ą¤œą¤æą¤øą„‡ MCP clients ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤­ą„‡ą¤œą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ `resource_server_url` ą¤ ą„€ą¤• ą¤µą¤¹ą„€ URL ą¤°ą¤–ą„‡ą¤‚ ą¤œą¤æą¤øą¤øą„‡ clients ą¤œą„ą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ + * ą¤‡ą¤øą„‡ तब बंद ą¤°ą¤¹ą¤Øą„‡ ą¤¦ą„‡ą¤‚ जब आपका authorization server ą¤…ą¤Ŗą¤Øą„‡ ą¤–ą„ą¤¦ ą¤•ą„‡ audience identifiers ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² करता ą¤¹ą„‹ (Auth0 API identifier, Entra application ID), और ą¤‡ą¤øą¤•ą„€ ą¤œą¤—ą¤¹ ą¤…ą¤Ŗą¤Øą„‡ verifier ą¤®ą„‡ą¤‚ `aud` ą¤œą¤¾ą¤ą¤šą„‡ą¤‚, और ą¤œą„‹ token इस server ą¤•ą„‡ ą¤²ą¤æą¤ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ ą¤‰ą¤øą¤•ą„‡ ą¤²ą¤æą¤ `None` ą¤²ą„Œą¤Ÿą¤¾ą¤ą¤ą„¤ + * अगर `aud` ą¤ą¤• list ą¤¹ą„ˆ, ą¤¤ą„‹ ą¤œą„‹ entry `resource_server_url` ą¤•ą„‡ बराबर ą¤¹ą„ˆ ą¤‰ą¤øą„‡ `resource` ą¤®ą„‡ą¤‚ ą¤°ą¤–ą„‡ą¤‚ą„¤ !!! tip SDK repository ą¤®ą„‡ą¤‚ `examples/servers/simple-auth/` ą¤•ą„‡ अंदर ą¤ą¤• `IntrospectionTokenVerifier` ą¤¹ą„ˆ ą¤œą„‹ @@ -91,7 +95,7 @@ authorization HTTP headers ą¤®ą„‡ą¤‚ रहता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ यह ą¤•ą¤æą¤øą„€ ą¤­ą„€ handler ą¤•ą„‡ अंदर, **`get_access_token()`** ą¤µą¤¹ą„€ `AccessToken` ą¤¹ą„ˆ ą¤œą„‹ ą¤†ą¤Ŗą¤•ą„‡ verifier ą¤Øą„‡ ą¤®ą„Œą¤œą„‚ą¤¦ą¤¾ request ą¤•ą„‡ ą¤²ą¤æą¤ ą¤²ą„Œą¤Ÿą¤¾ą¤Æą¤¾ ऄा: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ authorization server, user ą¤•ą„‡ consent screen पर click ą¤•ą¤°ą¤Øą„‡ क * `token_verifier=` और `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` ą¤¹ą¤®ą„‡ą¤¶ą¤¾ साऄ ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ * SDK `/.well-known/oauth-protected-resource/...` पर [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata publish करता ą¤¹ą„ˆ और unauthenticated requests का जवाब 401 ą¤øą„‡ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤œą¤æą¤øą¤•ą¤¾ `WWW-Authenticate` header ą¤‰ą¤øą„€ ą¤•ą„€ ओर इशारा करता ą¤¹ą„ˆą„¤ discovery ą¤•ą„€ ą¤Ŗą„‚ą¤°ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ बस ą¤‡ą¤¤ą¤Øą„€ ą¤¹ą„€ ą¤¹ą„ˆą„¤ * ą¤•ą¤æą¤øą„€ ą¤­ą„€ handler ą¤®ą„‡ą¤‚ `get_access_token()` बताता ą¤¹ą„ˆ कि call ą¤•ą„Œą¤Ø कर रहा ą¤¹ą„ˆą„¤ -* authorization HTTP का मामला ą¤¹ą„ˆą„¤ `stdio` और in-memory client ą¤‡ą¤øą„‡ ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą„‡ą¤–ą¤¤ą„‡ą„¤ +* authorization HTTP का मामला ą¤¹ą„ˆą„¤ `stdio` और in-memory test client ą¤‡ą¤øą„‡ ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą„‡ą¤–ą¤¤ą„‡ą„¤ client वाला आधा ą¤¹ą¤æą¤øą„ą¤øą¤¾ (ą¤†ą¤Ŗą¤•ą„‡ authorization server ą¤•ą„‹ ą¤–ą„‹ą¤œą¤Øą¤¾ और ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ token लाना) **[OAuth clients](../client/oauth-clients.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ और ą¤œą„‹ client user ą¤øą„‡ पहचान ą¤Ŗą„‚ą¤›ą¤Øą„‡ ą¤•ą„‡ बजाय ą¤–ą„ą¤¦ ą¤•ą„‹ą¤ˆ पहचान **assert** करता ą¤¹ą„ˆ, वह **[Identity assertion](../client/identity-assertion.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ diff --git a/i18n/hi/pages/run/deploy.md b/i18n/hi/pages/run/deploy.md index b27a3a93..9afb3992 100644 --- a/i18n/hi/pages/run/deploy.md +++ b/i18n/hi/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Deploy और scale करना {#deploy-scale} @@ -47,6 +47,22 @@ translation: deploy ą¤¹ą„ą¤† server ą¤œą„‹ हर connection ą¤ ą„ą¤•ą¤°ą¤¾ रहा ą¤¹ą„‹, ą¤‰ą¤øą„‡ Host allowlist ą¤•ą„€ ą¤øą¤®ą¤øą„ą¤Æą¤¾ ą¤¹ą„€ ą¤®ą¤¾ą¤Øą„‡ą¤‚ जब तक ą¤•ą„ą¤› और साबित न ą¤¹ą„‹ą„¤ **[Troubleshooting](../troubleshooting.md)** ą¤­ą„€ ą¤Æą¤¹ą„€ą¤‚ ą¤øą„‡ ą¤¶ą„ą¤°ą„‚ ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +## TLS-terminating proxy ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ {#behind-a-tls-terminating-proxy} + +अगर TLS ą¤•ą¤æą¤øą„€ proxy पर ą¤–ą¤¼ą¤¤ą„ą¤® ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ (ą¤•ą„‹ą¤ˆ ingress, load balancer, Caddy, nginx) और uvicorn ą¤‰ą¤øą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ plain HTTP serve करता ą¤¹ą„ˆ, ą¤¤ą„‹ uvicorn ą¤•ą„‹ ą¤¬ą¤¤ą¤¾ą¤ą¤ कि वह proxy ą¤•ą„‡ `X-Forwarded-*` headers पर ą¤­ą¤°ą„‹ą¤øą¤¾ ą¤•ą¤°ą„‡: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +ą¤‡ą¤øą¤•ą„‡ बिना app यह मान ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ कि ą¤‰ą¤øą„‡ `http://` पर serve किया जा रहा ą¤¹ą„ˆ, और वह ą¤œą„‹ ą¤­ą„€ redirect ą¤­ą„‡ą¤œą¤¤ą¤¾ ą¤¹ą„ˆ (आम ą¤¤ą„Œą¤° पर `/mcp` → `/mcp/`) वह `http://…` ą¤•ą„€ ओर इशारा करता ą¤¹ą„ˆą„¤ Python client ą¤•ą¤æą¤øą„€ HTTPS endpoint ą¤øą„‡ plain HTTP पर ą¤œą¤¾ą¤Øą„‡ ą¤øą„‡ इनकार कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और साफ़ बताता ą¤¹ą„ˆ: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +client ą¤•ą„€ तरफ़ का ą¤•ą¤¾ą¤®ą¤šą¤²ą¤¾ą¤Š उपाय ą¤¹ą„ˆ ą¤ ą„€ą¤• ą¤µą¤¹ą„€ URL configure करना ą¤œą„‹ server serve करता ą¤¹ą„ˆ (`https://mcp.example.com/mcp/`, slash सहित), ताकि ą¤•ą„‹ą¤ˆ redirect ą¤¹ą„‹ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ ą¤…ą¤øą¤²ą„€ ą¤‡ą¤²ą¤¾ą¤œ ऊपर वाला flag ą¤¹ą„ˆą„¤ `FORWARDED_ALLOW_IPS` ą¤‡ą¤øą„€ का environment-variable ą¤°ą„‚ą¤Ŗ ą¤¹ą„ˆ; `*` हर hop पर ą¤­ą¤°ą„‹ą¤øą¤¾ करता ą¤¹ą„ˆ, ą¤œą„‹ ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤¤ą¤­ą„€ ą¤øą¤¹ą„€ ą¤¹ą„ˆ जब proxy ą¤•ą„‡ अलावा ą¤•ą„‹ą¤ˆ uvicorn तक न ą¤Ŗą¤¹ą„ą¤ą¤š ą¤øą¤•ą„‡ą„¤ + ## Workers, और sticky ą¤•ą¤æą¤øą„‡ ą¤¹ą„‹ą¤Øą¤¾ ą¤¹ą„ˆ {#workers-and-who-has-to-be-sticky} जब hostname जवाब ą¤¦ą„‡ą¤Øą„‡ ą¤²ą¤—ą„‡, ą¤¤ą„‹ ą¤‰ą¤øą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ ą¤ą¤• ą¤øą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ worker ą¤²ą¤—ą¤¾ą¤ą¤ą„¤ ą¤‡ą¤øą¤•ą„‡ ą¤²ą¤æą¤ SDK ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ knob ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ; Starlette app ą¤•ą„‹ ą¤µą„ˆą¤øą„‡ ą¤¹ą„€ scale किया जाता ą¤¹ą„ˆ ą¤œą„ˆą¤øą„‡ ą¤•ą¤æą¤øą„€ ą¤­ą„€ ASGI app ą¤•ą„‹, object ą¤•ą¤æą¤øą„€ ą¤ą¤øą„€ ą¤šą„€ą¤œą¤¼ ą¤•ą„‹ ą¤øą„Œą¤‚ą¤Ŗą¤•ą¤° ą¤œą„‹ fork करना ą¤œą¤¾ą¤Øą¤¤ą„€ ą¤¹ą„ˆ: @@ -170,6 +186,7 @@ fan-out ą¤•ą„‹ ą¤‡ą¤øą¤øą„‡ ą¤•ą„‹ą¤ˆ मतलब ą¤Øą¤¹ą„€ą¤‚ कि stream ## सारांश {#recap} * बिना ą¤•ą„ą¤› configure ą¤•ą¤æą¤ app ą¤øą¤æą¤°ą„ą¤«ą¤¼ ą¤‰ą¤Øą„ą¤¹ą„€ą¤‚ requests का जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ localhost ą¤•ą„‹ ą¤­ą„‡ą¤œą„€ ą¤—ą¤ˆ ą¤¹ą„‹ą¤‚ą„¤ `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ą¤¹ą„€ go-live gate ą¤¹ą„ˆ: जब तक आप ą¤‡ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą„‡ą¤¤ą„‡, ą¤…ą¤øą¤²ą„€ hostname ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ हर request `421` ą¤¹ą„ˆ और कारण ą¤øą¤æą¤°ą„ą¤«ą¤¼ server ą¤•ą„‡ log ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ +* TLS-terminating proxy ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ uvicorn ą¤•ą„‹ `--proxy-headers --forwarded-allow-ips=...` ą¤•ą„‡ साऄ ą¤šą¤²ą¤¾ą¤ą¤, वरना ą¤‰ą¤øą¤•ą„‡ redirects `http://` ą¤•ą„€ ओर इशारा ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और client ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ ą¤ ą„ą¤•ą¤°ą¤¾ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ * 2026-07-28 पर ą¤•ą„‹ą¤ˆ session ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ और load balancer ą¤•ą„‡ sticky ą¤¹ą„‹ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ą„¤ `stateless_http=True` ą¤øą¤æą¤°ą„ą¤«ą¤¼ legacy का knob ą¤¹ą„ˆ ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ modern request उस flag ą¤•ą„‡ ą¤Ŗą¤¢ą¤¼ą„‡ ą¤œą¤¾ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ route ą¤¹ą„‹ą¤•ą¤° जवाब पा ą¤²ą„‡ą¤¤ą„€ ą¤¹ą„ˆą„¤ * default `requestState` key `os.urandom(32)` ą¤¹ą„ˆ, हर process ą¤®ą„‡ą¤‚ अलग ą¤¬ą¤Øą„€ ą¤¹ą„ą¤ˆą„¤ ą¤•ą„‹ą¤ˆ multi-round-trip retry ą¤œą„‹ ą¤¦ą„‚ą¤øą¤°ą„‡ worker तक ą¤Ŗą¤¹ą„ą¤ą¤šą„‡, `-32602` *"Invalid or expired requestState"* ą¤•ą„‡ साऄ fail ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą„¤ * ą¤‡ą¤²ą¤¾ą¤œ ą¤¹ą„ˆ `RequestStateSecurity(keys=[...])` **और** हर instance पर ą¤ą¤• ą¤¹ą„€ server ą¤Øą¤¾ą¤®ą„¤ नाम ą¤¹ą„€ token का default audience claim ą¤¹ą„ˆą„¤ ą¤µą¤¹ą„€ keys, ą¤µą¤¹ą„€ ą¤Øą¤¾ą¤®ą„¤ diff --git a/i18n/hi/pages/run/index.md b/i18n/hi/pages/run/index.md index 7eaaee37..3c99980f 100644 --- a/i18n/hi/pages/run/index.md +++ b/i18n/hi/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # अपना server चलाना {#running-your-server} @@ -64,21 +64,26 @@ Inspector ą¤ ą„€ą¤• ą¤µą¤¹ą„€ करता ą¤¹ą„ˆ ą¤œą„‹ ą¤…ą¤øą¤²ą„€ host क --8<-- "docs_src/run/tutorial002.py" ``` -वह ą¤ą¤• line Starlette app ą¤¬ą¤Øą¤¾ą¤¤ą„€ ą¤¹ą„ˆ और ą¤‰ą¤øą„‡ uvicorn ą¤øą„‡ serve ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆą„¤ Clients `http://127.0.0.1:3001/mcp` ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +वह ą¤ą¤• line Starlette app ą¤¬ą¤Øą¤¾ą¤¤ą„€ ą¤¹ą„ˆ और ą¤‰ą¤øą„‡ uvicorn ą¤øą„‡ serve ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆą„¤ clients `http://127.0.0.1:3001/mcp` ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ हर transport ą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ keyword arguments ą¤¹ą„ˆą¤‚, सब `run()` पर: -* `host` / `port`: कहाँ ą¤øą„ą¤Øą¤Øą¤¾ ą¤¹ą„ˆą„¤ Default `127.0.0.1` और `8000`ą„¤ -* `streamable_http_path`: MCP endpoint कहाँ रहता ą¤¹ą„ˆą„¤ Default `/mcp`ą„¤ +* `host` / `port`: कहाँ ą¤øą„ą¤Øą¤Øą¤¾ ą¤¹ą„ˆą„¤ default `127.0.0.1` और `8000`ą„¤ +* `streamable_http_path`: MCP endpoint कहाँ रहता ą¤¹ą„ˆą„¤ default `/mcp`ą„¤ * `json_response=True`: हर POST का जवाब SSE stream ą¤•ą„‡ बजाय ą¤ą¤• ą¤…ą¤•ą„‡ą¤²ą„€ JSON body ą¤øą„‡ ą¤¦ą„‡ą¤Øą¤¾ą„¤ उस body ą¤®ą„‡ą¤‚ ą¤øą¤æą¤°ą„ą¤«ą¤¼ response ą¤•ą„€ ą¤œą¤—ą¤¹ ą¤¹ą„ˆ, और ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ ą¤œą„‹ tool request ą¤•ą„‡ ą¤¬ą„€ą¤š ą¤®ą„‡ą¤‚ client ą¤•ą„‹ वापस call करता ą¤¹ą„ˆ (`ctx.elicit()`, sampling), वह इस leg पर `NoBackChannelError` raise करता ą¤¹ą„ˆ, और चल ą¤°ą¤¹ą„€ call ą¤øą„‡ ą¤œą„ą¤”ą¤¼ą„‡ notifications (`ctx.report_progress()` का progress, per-call log messages) ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą¤æą¤ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; standalone `GET` stream असंबंधित notifications अब ą¤­ą„€ ą¤²ą„‡ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą„¤ * `stateless_http=True`: हर request ą¤•ą„‡ ą¤²ą¤æą¤ नया transport, ą¤•ą„‹ą¤ˆ session tracking ą¤Øą¤¹ą„€ą¤‚ą„¤ -* `max_request_body_size`: ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤•ą„€ ą¤œą¤¾ą¤Øą„‡ ą¤µą¤¾ą¤²ą„€ ą¤øą¤¬ą¤øą„‡ ą¤¬ą¤”ą¤¼ą„€ request body, bytes ą¤®ą„‡ą¤‚ą„¤ Default 4 MiB ą¤¹ą„ˆ; ą¤‡ą¤øą¤øą„‡ ą¤¬ą¤”ą¤¼ą„€ requests +* `max_request_body_size`: ą¤øą„ą¤µą„€ą¤•ą¤¾ą¤° ą¤•ą„€ ą¤œą¤¾ą¤Øą„‡ ą¤µą¤¾ą¤²ą„€ ą¤øą¤¬ą¤øą„‡ ą¤¬ą¤”ą¤¼ą„€ request body, bytes ą¤®ą„‡ą¤‚ą„¤ default 4 MiB ą¤¹ą„ˆ; ą¤‡ą¤øą¤øą„‡ ą¤¬ą¤”ą¤¼ą„€ requests ą¤•ą„‹ parsing या session ą¤¬ą¤Øą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ HTTP 413 मिलता ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ ą¤¤ą¤­ą„€ ą¤¬ą¤¢ą¤¼ą¤¾ą¤ą¤ जब जायज़ MCP messages उस आकार ą¤øą„‡ ą¤¬ą¤”ą¤¼ą„‡ ą¤¹ą„‹ą¤‚ą„¤ +* `session_idle_timeout`: ą¤•ą„‹ą¤ˆ legacy session ą¤•ą¤æą¤¤ą¤Øą„‡ seconds तक बिना ą¤•ą¤æą¤øą„€ चल ą¤°ą¤¹ą„€ request ą¤•ą„‡ ą¤–ą¤¾ą¤²ą„€ रह सकता ą¤¹ą„ˆ, + ą¤‡ą¤øą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ कि server ą¤‰ą¤øą„‡ बंद कर ą¤¦ą„‡ą„¤ default 1800ą„¤ `None` ą¤‡ą¤øą„‡ बंद कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤¦ą„‡ą¤–ą„‡ą¤‚ + [session का ą¤œą„€ą¤µą¤Øą¤•ą¤¾ą¤² और ą¤øą„€ą¤®ą¤¾ą¤ą¤](legacy-clients.md#session-lifetime-and-limits)ą„¤ +* `max_sessions`: ą¤ą¤• process ą¤ą¤• साऄ ą¤•ą¤æą¤¤ą¤Øą„‡ legacy sessions रखता ą¤¹ą„ˆą„¤ default 10 000ą„¤ `None` + ą¤øą„€ą¤®ą¤¾ हटा ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‡ą¤øą¤•ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ ą¤­ą„€ ą¤‰ą¤øą„€ section ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ * `event_store`, `retry_interval`, `transport_security`: resumability और DNS-rebinding ą¤øą„‡ ą¤øą„ą¤°ą¤•ą„ą¤·ą¤¾ą„¤ ą¤Æą„‡ ą¤‡ą¤‚ą¤¤ą¤œą¤¼ą¤¾ą¤° कर ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, जब तक आप localhost ą¤•ą„‡ अलावा ą¤•ą¤¹ą„€ą¤‚ deploy न ą¤•ą¤°ą„‡ą¤‚; `transport_security` ą¤•ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ **[Deploy & scale](deploy.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ !!! warning - Transport options `run()` ą¤•ą„‹ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `MCPServer(...)` ą¤•ą„‹ **ą¤Øą¤¹ą„€ą¤‚**ą„¤ Constructor बताता ą¤¹ą„ˆ कि + transport options `run()` ą¤•ą„‹ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, `MCPServer(...)` ą¤•ą„‹ **ą¤Øą¤¹ą„€ą¤‚**ą„¤ constructor बताता ą¤¹ą„ˆ कि आपका server **ą¤•ą„ą¤Æą¤¾ ą¤¹ą„ˆ**: name, version, instructionsą„¤ `run()` बताता ą¤¹ą„ˆ कि वह ą¤•ą„ˆą¤øą„‡ serve ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤‡ą¤øą„‡ ą¤‰ą¤²ą„ą¤Ÿą¤¾ ą¤•ą¤°ą„‡ą¤‚ą¤—ą„‡ ą¤¤ą„‹ MCP ą¤•ą„‡ शामिल ą¤¹ą„‹ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ Python जवाब ą¤¦ą„‡ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ: diff --git a/i18n/hi/pages/run/legacy-clients.md b/i18n/hi/pages/run/legacy-clients.md index c97b5413..b3477bf0 100644 --- a/i18n/hi/pages/run/legacy-clients.md +++ b/i18n/hi/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # legacy clients ą¤•ą„‹ serve करना {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK हर request ą¤•ą„‹ ą¤‰ą¤øą¤•ą„‡ `MCP-Protocol-Version` header ą¤•ą„‡ ह ## ą¤ą¤• handler, ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤ {#one-handler-both-eras} -यह रहा ą¤ą¤• tool ą¤œą¤æą¤øą„‡ user ą¤øą„‡ ą¤•ą„ą¤› ą¤Ŗą„‚ą¤›ą¤Øą¤¾ ą¤¹ą„ˆ, और ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą„‹ą¤‚ ą¤•ą„‡ client ą¤œą„‹ ą¤‰ą¤øą„‡ call कर ą¤°ą¤¹ą„‡ ą¤¹ą„ˆą¤‚: +यह रहा ą¤ą¤• tool ą¤œą¤æą¤øą„‡ user ą¤øą„‡ ą¤•ą„ą¤› ą¤Ŗą„‚ą¤›ą¤Øą¤¾ ą¤¹ą„ˆ: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` ą¤•ą„‹ ą¤ą¤• ą¤šą„€ą¤œą¤¼ ą¤šą¤¾ą¤¹ą¤æą¤ ą¤œą„‹ model ą¤Øą„‡ ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą„€: ą¤•ą¤æą¤¤ą¤Øą„€ copiesą„¤ tool यह बात `Annotated[..., Resolve(ask_quantity)]` ą¤øą„‡ declare करता ą¤¹ą„ˆ (ą¤Ŗą„‚ą¤°ą„€ ą¤œą¤¾ą¤Øą¤•ą¤¾ą¤°ą„€ **[Dependencies](../handlers/dependencies.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆ)ą„¤ `reserve` ą¤®ą„‡ą¤‚ ą¤•ą¤¹ą„€ą¤‚ ą¤­ą„€ न ą¤•ą¤æą¤øą„€ version का नाम ą¤¹ą„ˆ, न capability ą¤•ą„€ जाँच, न ą¤•ą„‹ą¤ˆ branchą„¤ -ą¤¦ą„‹ą¤Øą„‹ą¤‚ clients **ą¤ą¤• ą¤¹ą„€ समय पर** ą¤–ą„ą¤²ą„‡ ą¤¹ą„ˆą¤‚, ą¤‰ą¤øą„€ `mcp` object ą¤Ŗą¤°ą„¤ `mode="legacy"` `initialize` handshake चलाता ą¤¹ą„ˆ: ą¤ ą„€ą¤• ą¤µą¤¹ą„€ connection ą¤œą„‹ 2026 ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ का client ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤¦ą„‚ą¤øą¤°ą¤¾ client default ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और `2026-07-28` पर ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆą„¤ +ą¤‡ą¤øą„‡ HTTP पर serve ą¤•ą¤°ą„‡ą¤‚, और ą¤Æą„‡ ą¤°ą¤¹ą„‡ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą„‹ą¤‚ ą¤•ą„‡ client ą¤œą„‹ ą¤‡ą¤øą„‡ call कर ą¤°ą¤¹ą„‡ ą¤¹ą„ˆą¤‚: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ą¤¦ą„‹ą¤Øą„‹ą¤‚ clients **ą¤ą¤• ą¤¹ą„€ समय पर** ą¤–ą„ą¤²ą„‡ ą¤¹ą„ˆą¤‚, ą¤‰ą¤øą„€ चल ą¤°ą¤¹ą„‡ server ą¤Ŗą¤°ą„¤ `mode="legacy"` `initialize` handshake चलाता ą¤¹ą„ˆ: ą¤ ą„€ą¤• ą¤µą¤¹ą„€ connection ą¤œą„‹ 2026 ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ का client ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤¦ą„‚ą¤øą¤°ą¤¾ client default ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ और `2026-07-28` पर ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą¤¾ ą¤¹ą„ˆą„¤ ą¤¦ą„‚ą¤øą¤°ą„‡ terminal ą¤øą„‡ `python client.py` ą¤šą¤²ą¤¾ą¤ą¤: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,40 @@ routing ą¤®ą„ą¤«ą¤¼ą„ą¤¤ ą¤¹ą„ˆą„¤ session ą¤Øą¤¹ą„€ą¤‚ą„¤ client ą¤•ą„‹ ą¤›ą„‚ą¤Ÿą„‡ ą¤¹ą„ą¤ SSE events फिर ą¤øą„‡ ą¤­ą„‡ą¤œą¤Øą¤¾), session store ą¤Øą¤¹ą„€ą¤‚ą„¤ यह ą¤•ą¤­ą„€ ą¤•ą¤æą¤øą„€ session ą¤•ą„‹ ą¤¦ą„‚ą¤øą¤°ą„‡ process ą¤øą„‡ ą¤Ŗą¤¹ą„ą¤ą¤š लायक ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤Øą¤¾ą¤¤ą¤¾ą„¤ +## session ą¤•ą„€ ą¤‰ą¤®ą„ą¤° और ą¤øą„€ą¤®ą¤¾ą¤ą¤ {#session-lifetime-and-limits} + +legacy session ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤•ą„‡ ą¤²ą¤æą¤ ą¤Øą¤¹ą„€ą¤‚ ą¤œą„€ą¤¤ą¤¾, और ą¤ą¤• process अनगिनत sessions ą¤Øą¤¹ą„€ą¤‚ ą¤°ą¤–ą¤¤ą¤¾ą„¤ +ą¤‡ą¤øą„‡ ą¤¦ą„‹ settings ą¤Øą¤æą¤Æą¤‚ą¤¤ą„ą¤°ą¤æą¤¤ ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤ ą¤¦ą„‹ą¤Øą„‹ą¤‚ `run()`, `streamable_http_app()` +और `Server.streamable_http_app()` पर keyword arguments ą¤¹ą„ˆą¤‚ą„¤ modern (`2026-07-28`) connections और `stateless_http=True` +ą¤®ą„‡ą¤‚ sessions ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤®ą„‡ą¤‚ ą¤øą„‡ ą¤•ą„‹ą¤ˆ setting उन पर ą¤²ą¤¾ą¤—ą„‚ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą„€ą„¤ + +| Setting | Default | यह ą¤•ą„ą¤Æą¤¾ ą¤•ą¤°ą¤¤ą„€ ą¤¹ą„ˆ | client ą¤•ą„‹ ą¤•ą„ą¤Æą¤¾ दिखता ą¤¹ą„ˆ | बंद ą¤•ą¤°ą¤Øą„‡ ą¤•ą„‡ ą¤²ą¤æą¤ | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 मिनट) | ą¤ą¤øą„‡ session ą¤•ą„‹ बंद कर ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆ ą¤œą¤æą¤øą¤®ą„‡ą¤‚ ą¤‡ą¤¤ą¤Øą„€ ą¤¦ą„‡ą¤° ą¤øą„‡ ą¤•ą„ą¤› in flight न रहा ą¤¹ą„‹ą„¤ | `404 Session not found`ą„¤ ą¤‰ą¤øą„‡ फिर ą¤øą„‡ `initialize` करना पऔ़ता ą¤¹ą„ˆą„¤ | `None` | +| `max_sessions` | `10_000` | ą¤‡ą¤¤ą¤Øą„‡ ą¤øą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ ą¤¹ą„‹ą¤Øą„‡ पर नया session ą¤–ą„‹ą¤²ą¤Øą„‡ ą¤øą„‡ मना कर ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆą„¤ ą¤®ą„Œą¤œą„‚ą¤¦ą¤¾ sessions ą¤…ą¤›ą„‚ą¤¤ą„‡ ą¤°ą¤¹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और ą¤•ą„ą¤› ą¤­ą„€ evict ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾ą„¤ | `503 Too many open sessions`, JSON-RPC code `-32603` ą¤•ą„‡ ą¤øą¤¾ą¤„ą„¤ | `None` | + +"in flight" ą¤•ą¤æą¤øą„‡ माना जाता ą¤¹ą„ˆ: + +* ą¤–ą„ą¤²ą¤¾ `GET` streamą„¤ SDK ą¤•ą„‡ clients ą¤ą¤• ą¤–ą„ą¤²ą¤¾ ą¤°ą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ ą¤œą„ą¤”ą¤¼ą„‡ ą¤¹ą„ą¤ client का session ą¤•ą¤­ą„€ + expire ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾ą„¤ +* ą¤ą¤øą„€ request ą¤œą¤æą¤øą¤•ą¤¾ जवाब ą¤…ą¤­ą„€ दिया जा रहा ą¤¹ą„ˆą„¤ timeout ą¤øą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ ą¤¦ą„‡ą¤° ą¤šą¤²ą¤Øą„‡ वाला tool call ą¤¬ą„€ą¤š ą¤®ą„‡ą¤‚ + ą¤Øą¤¹ą„€ą¤‚ ą¤°ą„‹ą¤•ą¤¾ जाता, और ą¤‰ą¤²ą¤Ÿą„€ ą¤—ą¤æą¤Øą¤¤ą„€ ą¤‰ą¤øą¤•ą„‡ ą¤–ą¤¤ą„ą¤® ą¤¹ą„‹ą¤Øą„‡ पर ą¤¹ą„€ ą¤¶ą„ą¤°ą„‚ ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆą„¤ +* और ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ą„¤ requests ą¤•ą„‡ ą¤¬ą„€ą¤š ą¤˜ą¤”ą¤¼ą„€ ą¤šą¤²ą¤¤ą„€ ą¤°ą¤¹ą¤¤ą„€ ą¤¹ą„ˆą„¤ session पर ą¤†ą¤ˆ ą¤•ą„‹ą¤ˆ ą¤­ą„€ request ą¤‰ą¤øą„‡ फिर ą¤øą„‡ ą¤¶ą„ą¤°ą„‚ कर ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆ, + `ping` ą¤­ą„€ą„¤ ą¤ą¤• बार session expire ą¤¹ą„‹ ą¤œą¤¾ą¤, ą¤¤ą„‹ ą¤‰ą¤øą„‡ ą¤•ą„ą¤› ą¤­ą„€ वापस ą¤Øą¤¹ą„€ą¤‚ ą¤²ą¤¾ą¤¤ą¤¾ą„¤ + +ą¤œą„‹ client अपना session `DELETE` ą¤øą„‡ ą¤–ą¤¤ą„ą¤® करता ą¤¹ą„ˆ, वह ą¤‰ą¤øą„‡ ą¤¤ą„ą¤°ą¤‚ą¤¤ ą¤–ą¤¾ą¤²ą„€ कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ जिस client ą¤•ą„€ +ą¤Ŗą¤¹ą¤²ą„€ request मना कर ą¤¦ą„€ ą¤—ą¤ˆ ą¤¹ą„‹, ą¤‰ą¤øą¤•ą„‡ साऄ ą¤­ą„€ ą¤Æą¤¹ą„€ ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤˜ą¤Ÿą¤Øą¤¾ą¤ą¤ server log ą¤®ą„‡ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤ expiry `INFO` पर `Session idle timeout` ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„€ ą¤¹ą„ˆą„¤ +मना किया गया open `WARNING` पर `Refusing to open a new session: sessions are already open` ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ą„¤ + +ą¤øą„€ą¤®ą¤¾ą¤ą¤ हर process ą¤•ą„€ ą¤…ą¤Ŗą¤Øą„€ ą¤¹ą„ˆą¤‚ą„¤ चार workers ą¤•ą„‡ साऄ ą¤Šą¤Ŗą¤°ą„€ ą¤øą„€ą¤®ą¤¾ `max_sessions` ą¤•ą„€ चार ą¤—ą„ą¤Øą¤¾ ą¤¹ą„ˆ, और हर +worker ą¤…ą¤Ŗą¤Øą„‡ sessions ą¤–ą„ą¤¦ expire करता ą¤¹ą„ˆą„¤ + ## ą¤‡ą¤•ą¤²ą„Œą¤¤ą¤¾ switch: `stateless_http` {#the-one-knob-stateless_http} अगर stickiness ą¤ą¤øą„€ ą¤•ą„€ą¤®ą¤¤ ą¤¹ą„ˆ ą¤œą„‹ आप ą¤šą„ą¤•ą¤¾ą¤Øą¤¾ ą¤Øą¤¹ą„€ą¤‚ ą¤šą¤¾ą¤¹ą¤¤ą„‡, ą¤¤ą„‹ ą¤ ą„€ą¤• ą¤ą¤• ą¤šą„€ą¤œą¤¼ ą¤¹ą„ˆ ą¤œą„‹ आप बदल ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ @@ -86,7 +130,7 @@ routing ą¤®ą„ą¤«ą¤¼ą„ą¤¤ ą¤¹ą„ˆą„¤ session ą¤Øą¤¹ą„€ą¤‚ą„¤ !!! check ą¤œą¤¾ą¤Øą¤¬ą„‚ą¤ą¤•ą¤° गलत काम ą¤•ą¤°ą„‡ą¤‚ą„¤ `reserve` ą¤ ą„€ą¤• ą¤µą¤¹ą„€ tool ą¤¹ą„ˆ ą¤œą¤æą¤øą¤Øą„‡ ą¤…ą¤­ą„€ ą¤¦ą„‹ą¤Øą„‹ą¤‚ clients ą¤•ą„‹ serve ą¤•ą¤æą¤Æą¤¾ą„¤ ą¤‡ą¤øą„‡ - `stateless_http=True` ą¤•ą„‡ साऄ deploy ą¤•ą¤°ą„‡ą¤‚, ą¤µą¤¹ą„€ ą¤¦ą„‹ clients HTTP पर ą¤œą„‹ą¤”ą¤¼ą„‡ą¤‚, और हर ą¤ą¤• ą¤øą„‡ ą¤‡ą¤øą„‡ call ą¤•ą¤°ą„‡ą¤‚ą„¤ + `stateless_http=True` ą¤•ą„‡ साऄ deploy ą¤•ą¤°ą„‡ą¤‚, ą¤µą¤¹ą„€ ą¤¦ą„‹ clients ą¤œą„‹ą¤”ą¤¼ą„‡ą¤‚, और हर ą¤ą¤• ą¤øą„‡ ą¤‡ą¤øą„‡ call ą¤•ą¤°ą„‡ą¤‚ą„¤ modern client ą¤•ą„‹ अब ą¤­ą„€ `Reserved 2 of 'Dune'.` मिलता ą¤¹ą„ˆą„¤ modern ą¤¹ą¤æą¤øą„ą¤øą¤¾ ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤¦ą¤²ą¤¾ą„¤ diff --git a/i18n/hi/pages/troubleshooting.md b/i18n/hi/pages/troubleshooting.md index bb0638ed..a8088dd3 100644 --- a/i18n/hi/pages/troubleshooting.md +++ b/i18n/hi/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # ą¤øą¤®ą¤øą„ą¤Æą¤¾ą¤ą¤ ą¤øą„ą¤²ą¤ą¤¾ą¤Øą¤¾ {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +ą¤µą„‡ entries इस तक `http://localhost:8000/mcp` पर ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą„€ ą¤¹ą„ˆą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ ą¤‡ą¤øą„‡ HTTP पर चलता ą¤›ą„‹ą¤”ą¤¼ ą¤¦ą„‡ą¤‚: + +```console +uv run mcp run server.py --transport streamable-http +``` + इस page पर quote ą¤•ą¤æą¤ ą¤—ą¤ errors ą¤…ą¤øą¤²ą„€ ą¤¹ą„ˆą¤‚: SDK का अपना test suite ą¤‡ą¤Øą¤®ą„‡ą¤‚ ą¤øą„‡ हर ą¤ą¤• ą¤•ą„‹ reproduce करता ą¤¹ą„ˆą„¤ ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -client ą¤Øą„‡ ą¤œą„‹ `Mcp-Session-Id` ą¤­ą„‡ą¤œą¤¾ ą¤‰ą¤øą„‡ server ą¤Øą¤¹ą„€ą¤‚ पहचानता, लगभग ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤‡ą¤øą¤²ą¤æą¤ कि server **restart** ą¤¹ą„ą¤† (या ą¤†ą¤Ŗą¤•ą„‹ ą¤•ą¤æą¤øą„€ ą¤¦ą„‚ą¤øą¤°ą„‡ instance पर route कर दिया गया)ą„¤ sessions ą¤‰ą¤øą„€ ą¤ą¤• process ą¤•ą„€ memory ą¤®ą„‡ą¤‚ ą¤°ą¤¹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ +client ą¤Øą„‡ ą¤œą„‹ `Mcp-Session-Id` ą¤­ą„‡ą¤œą¤¾ ą¤‰ą¤øą„‡ server ą¤Øą¤¹ą„€ą¤‚ ą¤Ŗą¤¹ą¤šą¤¾ą¤Øą¤¤ą¤¾ą„¤ या ą¤¤ą„‹ server **restart** ą¤¹ą„ą¤† (या ą¤†ą¤Ŗą¤•ą„‹ ą¤•ą¤æą¤øą„€ ą¤¦ą„‚ą¤øą¤°ą„‡ instance पर route कर दिया गया), या session **expire** ą¤¹ą„‹ गया ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ `session_idle_timeout` तक, ą¤œą„‹ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ 30 मिनट ą¤¹ą„ˆ, ą¤•ą„ą¤› ą¤­ą„€ in flight ą¤Øą¤¹ą„€ą¤‚ ą¤„ą¤¾ą„¤ [Session lifetime और limits](run/legacy-clients.md#session-lifetime-and-limits) ą¤¦ą„‡ą¤–ą„‡ą¤‚ą„¤ sessions ą¤‰ą¤øą„€ ą¤ą¤• process ą¤•ą„€ memory ą¤®ą„‡ą¤‚ ą¤°ą¤¹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤–ą„‹ą¤œą¤Øą„‡ ą¤•ą„‹ ą¤•ą„‹ą¤ˆ server bug ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ HTTP response ą¤ą¤• `404` ą¤¹ą„ˆ ą¤œą¤æą¤øą¤•ą„€ body JSON-RPC **ą¤¹ą„ˆ**, ą¤‡ą¤øą¤²ą¤æą¤ ऊपर ą¤µą¤¾ą¤²ą„‡ `421` ą¤•ą„‡ ą¤‰ą¤²ą¤Ÿ, python `Client` ą¤‡ą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‹ ą¤œą„ą¤Æą„‹ą¤‚ का ą¤¤ą„ą¤Æą„‹ą¤‚ दिखाता ą¤¹ą„ˆ: @@ -261,9 +267,9 @@ client ą¤Øą„‡ ą¤œą„‹ `Mcp-Session-Id` ą¤­ą„‡ą¤œą¤¾ ą¤‰ą¤øą„‡ server ą¤Øą¤¹ą„€ą¤‚ ą¤øą„ą¤§ą¤¾ą¤° ą¤¹ą„ˆ reconnect करना: `async with Client(...)` block ą¤øą„‡ बाहर ą¤Øą¤æą¤•ą¤²ą„‡ą¤‚ और ą¤Øą¤ ą¤®ą„‡ą¤‚ enter ą¤•ą¤°ą„‡ą¤‚, ą¤œą„‹ नया session negotiate करता ą¤¹ą„ˆą„¤ ą¤²ą¤‚ą¤¬ą„‡ समय तक ą¤šą¤²ą¤Øą„‡ ą¤µą¤¾ą¤²ą„‡ client ą¤•ą„‡ ą¤²ą¤æą¤ इसका मतलब ą¤¹ą„ˆ ą¤…ą¤Ŗą¤Øą„‡ calls ą¤•ą„‡ ą¤šą¤¾ą¤°ą„‹ą¤‚ ओर `MCPError` catch करना और इस message पर reconnect करना, न कि ą¤®ą¤°ą„‡ ą¤¹ą„ą¤ session ą¤•ą„‡ अंदर retry ą¤•ą¤°ą¤¤ą„‡ ą¤°ą¤¹ą¤Øą¤¾ą„¤ -अगर यह restart ą¤•ą„‡ **बिना** ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤¤ą„‹ आप sticky sessions ą¤•ą„‡ बिना ą¤ą¤• ą¤øą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ worker चला ą¤°ą¤¹ą„‡ ą¤¹ą„ˆą¤‚: हर worker ą¤•ą„€ ą¤…ą¤Ŗą¤Øą„€ session table ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ गलत worker पर route ą¤¹ą„ą¤ˆ request ą¤Æą¤¹ą„€ą¤‚ आ ą¤—ą¤æą¤°ą¤¤ą„€ ą¤¹ą„ˆą„¤ वह ą¤Ŗą„‚ą¤°ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ और ą¤‰ą¤øą¤•ą„‡ ą¤¦ą„‹ ą¤øą„ą¤§ą¤¾ą¤° (sticky routing, या `stateless_http=True`) **[Deploy और scale](run/deploy.md)** और **[legacy clients ą¤•ą„‹ serve करना](run/legacy-clients.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ +अगर यह restart ą¤•ą„‡ **बिना** ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और client ą¤‡ą¤¤ą¤Øą„€ ą¤¦ą„‡ą¤° ą¤šą„ą¤Ŗ ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ रहा ऄा, ą¤¤ą„‹ आप sticky sessions ą¤•ą„‡ बिना ą¤ą¤• ą¤øą„‡ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ worker चला ą¤°ą¤¹ą„‡ ą¤¹ą„ˆą¤‚: हर worker ą¤•ą„€ ą¤…ą¤Ŗą¤Øą„€ session table ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ गलत worker पर route ą¤¹ą„ą¤ˆ request ą¤Æą¤¹ą„€ą¤‚ आ ą¤—ą¤æą¤°ą¤¤ą„€ ą¤¹ą„ˆą„¤ वह ą¤Ŗą„‚ą¤°ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ और ą¤‰ą¤øą¤•ą„‡ ą¤¦ą„‹ ą¤øą„ą¤§ą¤¾ą¤° (sticky routing, या `stateless_http=True`) **[Deploy और scale](run/deploy.md)** और **[legacy clients ą¤•ą„‹ serve करना](run/legacy-clients.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ -server operator ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą¤øą„‡ ą¤®ą„‡ą¤² ą¤–ą¤¾ą¤¤ą„€ log line ą¤¹ą„ˆ `Rejected request with unknown or expired session ID: `ą„¤ यह `INFO` पर log ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ आम `WARNING` threshold पर ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„€ą„¤ deploy ą¤•ą„‡ ą¤ ą„€ą¤• बाद ą¤‡ą¤øą„‡ ą¤ą„ą¤‚ą¤” ą¤®ą„‡ą¤‚ ą¤¦ą„‡ą¤–ą¤Øą¤¾ ą¤øą¤¾ą¤®ą¤¾ą¤Øą„ą¤Æ ą¤¹ą„ˆ; हर ą¤œą„ą¤”ą¤¼ą¤¾ ą¤¹ą„ą¤† client reconnect कर रहा ą¤¹ą„ˆą„¤ +server operator ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą¤øą„‡ ą¤®ą„‡ą¤² ą¤–ą¤¾ą¤¤ą„€ log line ą¤¹ą„ˆ `Rejected request with unknown or expired session ID: `ą„¤ यह `INFO` पर log ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ आम `WARNING` threshold पर ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„€ą„¤ deploy ą¤•ą„‡ ą¤ ą„€ą¤• बाद ą¤‡ą¤øą„‡ ą¤ą„ą¤‚ą¤” ą¤®ą„‡ą¤‚ ą¤¦ą„‡ą¤–ą¤Øą¤¾ ą¤øą¤¾ą¤®ą¤¾ą¤Øą„ą¤Æ ą¤¹ą„ˆ; हर ą¤œą„ą¤”ą¤¼ą¤¾ ą¤¹ą„ą¤† client reconnect कर रहा ą¤¹ą„ˆą„¤ जब session expire ą¤¹ą„ą¤† ą¤¹ą„‹, ą¤¤ą„‹ उस line ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ `Session idle timeout` ą¤†ą¤¤ą„€ ą¤¹ą„ˆ, वह ą¤­ą„€ `INFO` ą¤Ŗą¤°ą„¤ ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ server operator ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą¤øą„‡ ą¤®ą„‡ą¤² ą¤–ą¤¾ą¤¤ą„€ log line आपका server user ą¤øą„‡ ą¤•ą„ą¤› ą¤Ŗą„‚ą¤›ą¤Øą¤¾ चाहता ą¤¹ą„ˆ, और इस client ą¤Øą„‡ ą¤•ą¤­ą„€ कहा ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ कि ą¤‰ą¤øą¤øą„‡ ą¤Ŗą„‚ą¤›ą¤¾ जा सकता ą¤¹ą„ˆą„¤ -जब ą¤œą„ą¤”ą¤¼ą„‡ ą¤¹ą„ą¤ client ą¤Øą„‡ form elicitation declare ą¤Øą¤¹ą„€ą¤‚ किया ą¤¹ą„‹, ą¤¤ą„‹ elicitation resolver ą¤¶ą„ą¤°ą„‚ ą¤®ą„‡ą¤‚ ą¤¹ą„€ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, और `e.error.data` ą¤ ą„€ą¤•-ą¤ ą„€ą¤• बताता ą¤¹ą„ˆ कि ą¤•ą„ą¤Æą¤¾ गायब ą¤¹ą„ˆ: +यह Bistro book ą¤•ą¤°ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤Ŗą„‚ą¤›ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤ą¤• resolver ą¤•ą„‡ ą¤œą¤¼ą¤°ą¤æą¤: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather server ą¤•ą„€ ą¤œą¤—ą¤¹ ą¤‡ą¤øą„‡ serve ą¤•ą¤°ą„‡ą¤‚ और ą¤ą¤øą„‡ client ą¤øą„‡ `book_table` call ą¤•ą¤°ą„‡ą¤‚ ą¤œą¤æą¤øą¤Øą„‡ ą¤•ą„‹ą¤ˆ `elicitation_callback` ą¤Øą¤¹ą„€ą¤‚ ą¤¦ą¤æą¤Æą¤¾ą„¤ resolver ą¤¶ą„ą¤°ą„‚ ą¤®ą„‡ą¤‚ ą¤¹ą„€ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤œą„ą¤”ą¤¼ą„‡ ą¤¹ą„ą¤ client ą¤Øą„‡ form elicitation ą¤•ą¤­ą„€ declare ą¤Øą¤¹ą„€ą¤‚ किया, और `e.error.data` ą¤ ą„€ą¤•-ą¤ ą„€ą¤• बताता ą¤¹ą„ˆ कि ą¤•ą„ą¤Æą¤¾ गायब ą¤¹ą„ˆ: ```json { @@ -289,7 +301,7 @@ server operator ą¤•ą„‡ ą¤²ą¤æą¤ ą¤‡ą¤øą¤øą„‡ ą¤®ą„‡ą¤² ą¤–ą¤¾ą¤¤ą„€ log line ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: ą¤†ą¤Ŗą¤•ą„‡ handler ą¤Øą„‡ request ą¤•ą„‡ ą¤¬ą„€ą¤š ą¤®ą„‡ą¤‚ client तक ą¤Ŗą¤¹ą„ą¤ą¤šą¤Øą„‡ ą¤•ą„€ ą¤•ą„‹ą¤¶ą¤æą¤¶ ą¤•ą„€, ą¤ą¤øą„‡ connection पर ą¤œą¤æą¤øą¤•ą„‡ call ą¤®ą„‡ą¤‚ server ą¤•ą„€ ओर ą¤øą„‡ request ą¤²ą„‡ ą¤œą¤¾ą¤Øą„‡ वाला ą¤•ą„‹ą¤ˆ channel ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆą„¤ ą¤¤ą„€ą¤Ø server configurations ą¤¹ą„ˆą¤‚ ą¤œą„‹ ą¤•ą¤æą¤øą„€ call ą¤•ą„‹ इस हालत ą¤®ą„‡ą¤‚ ą¤”ą¤¾ą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ -**`2026-07-28` connection: ą¤•ą„‹ą¤ˆ ą¤­ą„€ transport, ą¤¹ą¤®ą„‡ą¤¶ą¤¾ą„¤** ą¤†ą¤§ą„ą¤Øą¤æą¤• protocol ą¤®ą„‡ą¤‚ server-initiated requests ą¤¹ą„ˆą¤‚ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ server ą¤•ą„ą¤› ą¤­ą„‡ą¤œą„‡ ą¤œą¤¾ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ tool ą¤•ą„‡ अंदर `ctx.elicit()` ą¤‡ą¤øą¤øą„‡ ą¤Ÿą¤•ą¤°ą¤¾ą¤Øą„‡ का classic ą¤¤ą¤°ą„€ą¤•ą¤¾ ą¤¹ą„ˆ (ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ in-memory test पर, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ `Client(server)` बिना ą¤•ą¤¹ą„‡ `2026-07-28` negotiate करता ą¤¹ą„ˆ), और `elicitation_callback=` ą¤¦ą„‡ą¤Øą„‡ ą¤øą„‡ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ बदलता, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ client तक ą¤•ą¤­ą„€ ą¤•ą„‹ą¤ˆ request ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą„€ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤œą¤æą¤øą¤•ą¤¾ वह जवाब ą¤¦ą„‡: +**`2026-07-28` connection: ą¤•ą„‹ą¤ˆ ą¤­ą„€ transport, ą¤¹ą¤®ą„‡ą¤¶ą¤¾ą„¤** ą¤†ą¤§ą„ą¤Øą¤æą¤• protocol ą¤®ą„‡ą¤‚ server-initiated requests ą¤¹ą„ˆą¤‚ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚, ą¤‡ą¤øą¤²ą¤æą¤ server ą¤•ą„ą¤› ą¤­ą„‡ą¤œą„‡ ą¤œą¤¾ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ मना कर ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ tool ą¤•ą„‡ अंदर `ctx.elicit()` ą¤‡ą¤øą¤øą„‡ ą¤Ÿą¤•ą¤°ą¤¾ą¤Øą„‡ का classic ą¤¤ą¤°ą„€ą¤•ą¤¾ ą¤¹ą„ˆ, आम ą¤¤ą„Œą¤° पर उस tool ą¤•ą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤¹ą„€ in-memory **[test](get-started/testing.md)** ą¤®ą„‡ą¤‚, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ `Client(mcp)` बिना ą¤•ą¤¹ą„‡ `2026-07-28` negotiate करता ą¤¹ą„ˆą„¤ `elicitation_callback=` ą¤¦ą„‡ą¤Øą„‡ ą¤øą„‡ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ बदलता, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ client तक ą¤•ą¤­ą„€ ą¤•ą„‹ą¤ˆ request ą¤Ŗą¤¹ą„ą¤ą¤šą¤¤ą„€ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤œą¤æą¤øą¤•ą¤¾ वह जवाब ą¤¦ą„‡: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ client ą¤Øą„‡ ą¤œą„‹ `requestState` token वापस echo किया, server ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * server log ą¤®ą„‡ą¤‚ `Tool already exists:` ą¤¹ą„€ ą¤‡ą¤•ą¤²ą„Œą¤¤ą¤¾ ą¤øą¤‚ą¤•ą„‡ą¤¤ ą¤¹ą„ˆ कि ą¤ą¤• ą¤¹ą„€ नाम ą¤•ą„‡ ą¤¦ą„‹ tools ą¤øą¤æą¤®ą¤Ÿą¤•ą¤° ą¤ą¤• रह ą¤—ą¤ą„¤ * ą¤ą¤• 421, ą¤¤ą„€ą¤Ø ą¤°ą„‚ą¤Ŗ: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (ą¤¬ą¤¾ą¤•ą„€ सब), `Invalid Host header: ` (server log)ą„¤ ą¤øą„ą¤§ą¤¾ą¤°: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`ą„¤ * `Task group is not initialized` -> mounted app ą¤œą¤æą¤øą¤•ą„‡ host lifespan ą¤Øą„‡ ą¤•ą¤­ą„€ `mcp.session_manager.run()` ą¤®ą„‡ą¤‚ enter ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤æą¤Æą¤¾ą„¤ -* `Session not found` -> server restart ą¤¹ą„ą¤†; reconnect ą¤•ą¤°ą„‡ą¤‚ą„¤ +* `Session not found` -> server restart ą¤¹ą„ą¤† या session expire ą¤¹ą„‹ गया (`session_idle_timeout`); reconnect ą¤•ą¤°ą„‡ą¤‚ą„¤ * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` ą¤•ą„‹ server-to-client channel ą¤šą¤¾ą¤¹ą¤æą¤: `2026-07-28` connection ą¤®ą„‡ą¤‚ वह ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„‹ą¤¤ą¤¾, `stateless_http=True` legacy वाला ą¤›ą„€ą¤Ø ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, और `json_response=True` request-scoped ą¤µą¤¾ą¤²ą¤¾ą„¤ resolver ą¤‡ą¤øą„ą¤¤ą„‡ą¤®ą¤¾ą¤² ą¤•ą¤°ą„‡ą¤‚ (legacy client ą¤•ą„‹ ऐसा server ą¤­ą„€ ą¤šą¤¾ą¤¹ą¤æą¤ ą¤œą„‹ channel रखता ą¤¹ą„‹)ą„¤ इसका ą¤Ŗą¤”ą¤¼ą„‹ą¤øą„€ `Method not found` ą¤ą¤øą„‡ method ą¤•ą„€ request ą¤¹ą„ˆ ą¤œą„‹ ą¤¦ą„‚ą¤øą¤°ą„€ side ą¤•ą„‡ protocol revision ą¤®ą„‡ą¤‚ ą¤¹ą„ˆ ą¤¹ą„€ ą¤Øą¤¹ą„€ą¤‚ą„¤ * `Client did not declare the form elicitation capability ...` और `Elicitation not supported` -> client ą¤®ą„‡ą¤‚ `elicitation_callback=` गायब ą¤¹ą„ˆą„¤ * `Invalid or expired requestState` wire पर ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ बताता कि ą¤•ą„ą¤Æą„‹ą¤‚ą„¤ server log बताता ą¤¹ą„ˆ; `unknown key` का मतलब ą¤¹ą„ˆ workers ą¤•ą„‡ ą¤¬ą„€ą¤š `RequestStateSecurity(keys=[...])` ą¤øą¤¾ą¤ą¤¾ ą¤•ą¤°ą„‡ą¤‚ą„¤ diff --git a/i18n/hi/pages/whats-new.md b/i18n/hi/pages/whats-new.md index 3550445d..7178b87f 100644 --- a/i18n/hi/pages/whats-new.md +++ b/i18n/hi/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 ą¤®ą„‡ą¤‚ नया ą¤•ą„ą¤Æą¤¾ ą¤¹ą„ˆ {#whats-new-in-v2} @@ -42,11 +42,11 @@ tool ą¤•ą„‹ ą¤œą„‹ ą¤•ą„ą¤› ą¤šą¤¾ą¤¹ą¤æą¤, वह सब model ą¤øą„‡ नह v1 ą¤†ą¤Ŗą¤•ą„‹ ą¤¤ą„€ą¤Ø nested ą¤Ŗą¤°ą¤¤ą„‡ą¤‚ ऄमाता ऄा: raw streams ą¤¦ą„‡ą¤Øą„‡ वाला transport context manager, ą¤‰ą¤Øą¤•ą„‡ ą¤šą¤¾ą¤°ą„‹ą¤‚ ओर लिपटा `ClientSession`, और हाऄ ą¤øą„‡ call किया ą¤œą¤¾ą¤Øą„‡ वाला `await session.initialize()`ą„¤ v2 ą¤®ą„‡ą¤‚ ą¤ą¤• ą¤¹ą„€ object ą¤¹ą„ˆ: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` server object ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ (in memory, ą¤•ą„‹ą¤ˆ transport ą¤Øą¤¹ą„€ą¤‚: testing ą¤µą¤¾ą¤²ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€), URL (Streamable HTTP), `StdioServerParameters` (stdio subprocess), या `sse_client(...)` ą¤œą„ˆą¤øą¤¾ ą¤•ą„‹ą¤ˆ ą¤­ą„€ ą¤¦ą„‚ą¤øą¤°ą¤¾ transport context managerą„¤ `async with` ą¤®ą„‡ą¤‚ ą¤Ŗą„ą¤°ą¤µą„‡ą¤¶ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„€ connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और protocol version negotiate ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, server ą¤šą¤¾ą¤¹ą„‡ जिस ą¤Ŗą„€ą¤¢ą¤¼ą„€ का ą¤¹ą„‹; ą¤‰ą¤øą¤•ą„‡ बाद `client.server_capabilities` और `client.protocol_version` बस ą¤‰ą¤Ŗą¤²ą¤¬ą„ą¤§ ą¤°ą¤¹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और जब server ą¤…ą¤Ŗą¤Øą„€ पहचान बताता ą¤¹ą„ˆ ą¤¤ą„‹ `client.server_info` ą¤­ą„€ (यह अब `Implementation | None` ą¤¹ą„ˆ, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ 2026 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤®ą„‡ą¤‚ identity optional ą¤¹ą„ˆ)ą„¤ v1 ą¤®ą„‡ą¤‚ register ą¤•ą¤æą¤ ą¤—ą¤ sampling और elicitation callbacks अब ą¤­ą„€ काम ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (ą¤‰ą¤Øą¤•ą„€ bodies ą¤®ą„‡ą¤‚ ą¤µą¤¹ą„€ snake_case attribute rename दिखता ą¤¹ą„ˆ ą¤œą„‹ इस page ą¤•ą„€ हर ą¤šą„€ą¤œą¤¼ ą¤®ą„‡ą¤‚), ą¤µą„‡ अब 2026-style requests-inside-results (ą¤Øą„€ą¤šą„‡) का जवाब ą¤­ą„€ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और ą¤µą„‡ ą¤ą¤•-ą¤ą¤• ą¤•ą¤°ą¤•ą„‡ ą¤Øą¤¹ą„€ą¤‚, ą¤¬ą¤²ą„ą¤•ą¤æ concurrently ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤œą¤æą¤øą„‡ low-level surface ą¤šą¤¾ą¤¹ą¤æą¤, ą¤‰ą¤øą¤•ą„‡ ą¤²ą¤æą¤ `ClientSession` अब ą¤­ą„€ ą¤Øą„€ą¤šą„‡ ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆ, और `client.session` ą¤‰ą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‹ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; वह ą¤­ą„€ बदला ą¤¹ą„ˆ (वह ą¤Øą¤ dispatcher engine पर चलता ą¤¹ą„ˆ, और ą¤‰ą¤øą¤•ą„‡ ą¤•ą„ą¤› ą¤…ą¤Ŗą¤Øą„‡ signatures ą¤¬ą¤¦ą¤²ą„‡ ą¤¹ą„ˆą¤‚), ą¤‡ą¤øą¤²ą¤æą¤ ą¤Øą„€ą¤šą„‡ ą¤‰ą¤¤ą¤°ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** ą¤Ŗą¤¢ą¤¼ą„‡ą¤‚ą„¤ +`Client` URL ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ (Streamable HTTP), `StdioServerParameters` (stdio subprocess), `sse_client(...)` ą¤œą„ˆą¤øą¤¾ ą¤•ą„‹ą¤ˆ ą¤­ą„€ ą¤¦ą„‚ą¤øą¤°ą¤¾ transport context manager, या, tests ą¤®ą„‡ą¤‚, ą¤–ą„ą¤¦ server object (in memory, ą¤•ą„‹ą¤ˆ transport ą¤Øą¤¹ą„€ą¤‚)ą„¤ `async with` ą¤®ą„‡ą¤‚ ą¤Ŗą„ą¤°ą¤µą„‡ą¤¶ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„€ connect ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ और protocol version negotiate ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ, server ą¤šą¤¾ą¤¹ą„‡ जिस ą¤Ŗą„€ą¤¢ą¤¼ą„€ का ą¤¹ą„‹; ą¤‰ą¤øą¤•ą„‡ बाद `client.server_capabilities` और `client.protocol_version` बस ą¤‰ą¤Ŗą¤²ą¤¬ą„ą¤§ ą¤°ą¤¹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और जब server ą¤…ą¤Ŗą¤Øą„€ पहचान बताता ą¤¹ą„ˆ ą¤¤ą„‹ `client.server_info` ą¤­ą„€ (यह अब `Implementation | None` ą¤¹ą„ˆ, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ 2026 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤®ą„‡ą¤‚ identity optional ą¤¹ą„ˆ)ą„¤ v1 ą¤®ą„‡ą¤‚ register ą¤•ą¤æą¤ ą¤—ą¤ sampling और elicitation callbacks अब ą¤­ą„€ काम ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (ą¤‰ą¤Øą¤•ą„€ bodies ą¤®ą„‡ą¤‚ ą¤µą¤¹ą„€ snake_case attribute rename दिखता ą¤¹ą„ˆ ą¤œą„‹ इस page ą¤•ą„€ हर ą¤šą„€ą¤œą¤¼ ą¤®ą„‡ą¤‚), ą¤µą„‡ अब 2026-style requests-inside-results (ą¤Øą„€ą¤šą„‡) का जवाब ą¤­ą„€ ą¤¦ą„‡ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और ą¤µą„‡ ą¤ą¤•-ą¤ą¤• ą¤•ą¤°ą¤•ą„‡ ą¤Øą¤¹ą„€ą¤‚, ą¤¬ą¤²ą„ą¤•ą¤æ concurrently ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤œą¤æą¤øą„‡ low-level surface ą¤šą¤¾ą¤¹ą¤æą¤, ą¤‰ą¤øą¤•ą„‡ ą¤²ą¤æą¤ `ClientSession` अब ą¤­ą„€ ą¤Øą„€ą¤šą„‡ ą¤®ą„Œą¤œą„‚ą¤¦ ą¤¹ą„ˆ, और `client.session` ą¤‰ą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‹ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; वह ą¤­ą„€ बदला ą¤¹ą„ˆ (वह ą¤Øą¤ dispatcher engine पर चलता ą¤¹ą„ˆ, और ą¤‰ą¤øą¤•ą„‡ ą¤•ą„ą¤› ą¤…ą¤Ŗą¤Øą„‡ signatures ą¤¬ą¤¦ą¤²ą„‡ ą¤¹ą„ˆą¤‚), ą¤‡ą¤øą¤²ą¤æą¤ ą¤Øą„€ą¤šą„‡ ą¤‰ą¤¤ą¤°ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** ą¤Ŗą¤¢ą¤¼ą„‡ą¤‚ą„¤ **[The Client](client/index.md)** इसका परिचय ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, **[Client transports](client/transports.md)** connection ą¤•ą„‡ ą¤šą¤¾ą¤°ą„‹ą¤‚ ą¤°ą„‚ą¤Ŗ ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ, **[Client callbacks](client/callbacks.md)** ą¤–ą„ą¤¦ callbacks ą¤•ą„‹, और **[Testing](get-started/testing.md)** वह in-memory pattern दिखाता ą¤¹ą„ˆ ą¤œą„‹ v1 ą¤•ą„‡ `create_connected_server_and_client_session()` helper ą¤•ą„€ ą¤œą¤—ą¤¹ ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ @@ -153,7 +153,7 @@ renames ą¤–ą„ą¤¦ ą¤…ą¤Ŗą¤Øą„€ ą¤˜ą„‹ą¤·ą¤£ą¤¾ ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ ą¤Æą„‡ * `McpError`, ą¤œą¤æą¤øą¤•ą¤¾ नाम बदलकर **`MCPError`** ą¤¹ą„ą¤†, ą¤øą„€ą¤§ą„‡ `(code, message, data)` constructor ą¤•ą„‡ ą¤øą¤¾ą¤„ą„¤ * `MCPServer.get_context()`, `mount_path=`, और lowlevel `Server` ą¤•ą„‡ decorator methods, ContextVar और handler dictsą„¤ -## Protocol: 2025-11-25 ą¤øą„‡ 2026-07-28 {#the-protocol-2025-11-25-to-2026-07-28} +## protocol: 2025-11-25 ą¤øą„‡ 2026-07-28 {#the-protocol-2025-11-25-to-2026-07-28} v2 2026-07-28 revision implement करता ą¤¹ą„ˆ, और यह **ą¤¦ą„‹ą¤Øą„‹ą¤‚** revisions ą¤ą¤• साऄ serve करता ą¤¹ą„ˆ: ą¤µą¤¹ą„€ `streamable_http_app()` (और ą¤µą¤¹ą„€ stdio server) 2025 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ client ą¤•ą„‡ `initialize` और 2026 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ client ą¤•ą„€ requests, ą¤¦ą„‹ą¤Øą„‹ą¤‚ का जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ, बिना ą¤•ą„ą¤› configure ą¤•ą¤æą¤, बिना ą¤•ą„‹ą¤ˆ flag ą¤Ŗą¤²ą¤Ÿą„‡, और बिना अलग deployment ą¤•ą„‡ą„¤ नया revision serve ą¤•ą¤°ą¤Øą„‡ ą¤øą„‡ ą¤Ŗą„ą¤°ą¤¾ą¤Øą„‡ revision वाला client ą¤¬ą„€ą¤š ą¤®ą„‡ą¤‚ ą¤Øą¤¹ą„€ą¤‚ ą¤›ą„‚ą¤Ÿą¤¤ą¤¾ą„¤ ą¤†ą¤—ą„‡ वह ą¤¹ą„ˆ ą¤œą„‹ नया revision ą¤–ą„ą¤¦ बदलता ą¤¹ą„ˆą„¤ @@ -163,19 +163,23 @@ v2 2026-07-28 revision implement करता ą¤¹ą„ˆ, और यह **ą¤¦ą„‹ą¤Ø Streamable HTTP पर 2026 path ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ `Mcp-Session-Id` ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, और operational headline ą¤Æą¤¹ą„€ ą¤¹ą„ˆ: **ą¤•ą„‹ą¤ˆ ą¤šą„€ą¤œą¤¼ modern request ą¤•ą„‹ ą¤•ą¤æą¤øą„€ worker ą¤øą„‡ ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤¾ą¤ą¤§ą¤¤ą„€**, ą¤‡ą¤øą¤²ą¤æą¤ ą¤øą¤¾ą¤¦ą„‡ round-robin load balancer ą¤•ą„‡ ą¤Ŗą„€ą¤›ą„‡ ą¤•ą„‹ą¤ˆ ą¤­ą„€ replica उसका जवाब ą¤¦ą„‡ सकता ą¤¹ą„ˆą„¤ ą¤¦ą„‹ ईमानदार ą¤¶ą¤°ą„ą¤¤ą„‡ą¤‚ą„¤ ą¤†ą¤Ŗą¤•ą„‡ 2025 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ clients (ą¤†ą¤œ ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ą¤¤ą¤° clients ą¤Æą¤¹ą„€ ą¤¹ą„ˆą¤‚) अब ą¤­ą„€ sessions ą¤–ą„‹ą¤²ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ और ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ अब ą¤­ą„€ ą¤µą¤¹ą„€ stickiness ą¤šą¤¾ą¤¹ą¤æą¤ ą¤œą„‹ v1 पर ą¤šą¤¾ą¤¹ą¤æą¤ ą¤„ą„€; ą¤‰ą¤Øą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤¦ą¤²ą¤¤ą¤¾ą„¤ और ą¤ą¤• ą¤šą„€ą¤œą¤¼ ą¤œą„‹ **multi-round-trip** retry ą¤•ą„‹ workers ą¤•ą„‡ पार ą¤²ą„‡ ą¤œą¤¾ą¤Øą„€ ą¤¹ą„‹ą¤¤ą„€ ą¤¹ą„ˆ, वह उसका sealed `request_state` ą¤¹ą„ˆ, ą¤œą¤æą¤øą¤•ą„€ default key हर process ą¤®ą„‡ą¤‚ अलग ą¤¬ą¤Øą¤¤ą„€ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ scaled-out deployment `RequestStateSecurity(keys=[...])` pass करता ą¤¹ą„ˆą„¤ (`stateless_http=True` का ą¤‡ą¤øą¤øą„‡ ą¤²ą„‡ą¤Øą¤¾-ą¤¦ą„‡ą¤Øą¤¾ ą¤Øą¤¹ą„€ą¤‚: वह ą¤øą¤æą¤°ą„ą¤«ą¤¼ यह तय करता ą¤¹ą„ˆ कि 2025 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ clients ą¤•ą„ˆą¤øą„‡ serve ą¤¹ą„‹ą¤‚, और 2026 traffic ą¤‰ą¤øą„‡ ą¤•ą¤­ą„€ ą¤Øą¤¹ą„€ą¤‚ पढ़ता; अगर ą¤†ą¤Ŗą¤Øą„‡ v1 ą¤®ą„‡ą¤‚ ą¤‰ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤øą„‡ set किया ą¤¹ą„ˆ, ą¤¤ą„‹ ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤¦ą¤²ą¤¤ą¤¾ą„¤) -इसका client वाला ą¤Ŗą¤¹ą¤²ą„‚ **[Protocol versions](protocol-versions.md)** ą¤¹ą„ˆ, operator ą¤•ą„€ checklist **[Deploy & scale](run/deploy.md)** ą¤¹ą„ˆ (Host allowlist, `request_state` key, replicas ą¤•ą„‡ पार notifications), और ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤ ą¤ą¤• साऄ serve ą¤•ą¤°ą¤Øą„‡ ą¤•ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ **[Legacy clients ą¤•ą„‹ serve करना](run/legacy-clients.md)** ą¤¹ą„ˆą„¤ +इसका client वाला ą¤Ŗą¤¹ą¤²ą„‚ **[Protocol versions](protocol-versions.md)** ą¤¹ą„ˆ, operator ą¤•ą„€ checklist **[Deploy & scale](run/deploy.md)** ą¤¹ą„ˆ (Host allowlist, `request_state` key, replicas ą¤•ą„‡ पार notifications), और ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤ ą¤ą¤• साऄ serve ą¤•ą¤°ą¤Øą„‡ ą¤•ą„€ ą¤•ą¤¹ą¤¾ą¤Øą„€ **[legacy clients ą¤•ą„‹ serve करना](run/legacy-clients.md)** ą¤¹ą„ˆą„¤ -### Server client ą¤•ą„‹ call ą¤Øą¤¹ą„€ą¤‚ कर सकता: multi-round-trip requests {#the-server-cannot-call-the-client-multi-round-trip-requests} +### server client ą¤•ą„‹ call ą¤Øą¤¹ą„€ą¤‚ कर सकता: multi-round-trip requests {#the-server-cannot-call-the-client-multi-round-trip-requests} 2026-07-28 पर हर server-initiated request हट ą¤—ą¤ˆ ą¤¹ą„ˆ: push elicitation, sampling, `roots/list`ą„¤ 2026 connection पर ą¤‰ą¤Øą¤•ą„‡ ą¤²ą¤æą¤ ą¤•ą„‹ą¤ˆ channel ą¤Øą¤¹ą„€ą¤‚ ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ `ctx.elicit()` और `ctx.session.create_message()` वहाँ `NoBackChannelError` ą¤•ą„‡ साऄ fail ą¤¹ą„‹ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ (legacy clients ą¤•ą„‡ ą¤²ą¤æą¤ ą¤µą„‡ अब ą¤­ą„€ काम ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚)ą„¤ -इसका ą¤µą¤æą¤•ą¤²ą„ą¤Ŗ call ą¤•ą„‹ पलट ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ जिस tool ą¤•ą„‹ user ą¤øą„‡ ą¤•ą„ą¤› ą¤šą¤¾ą¤¹ą¤æą¤, वह सवाल **ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾** ą¤¹ą„ˆ (`InputRequiredResult`), client ą¤‰ą¤Øą„ą¤¹ą„€ą¤‚ callbacks ą¤øą„‡ उसका जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ ą¤‰ą¤øą¤•ą„‡ पास ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤øą„‡ ą¤„ą„‡, और call ą¤•ą„‹ ą¤œą¤µą¤¾ą¤¬ą„‹ą¤‚ ą¤•ą„‡ साऄ retry किया जाता ą¤¹ą„ˆą„¤ `Client` यह loop ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ चलाता ą¤¹ą„ˆą„¤ Server पर आप result शायद ą¤¹ą„€ ą¤•ą¤­ą„€ ą¤–ą„ą¤¦ ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤ą¤• **[dependency](handlers/dependencies.md)** यह कर ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆ: parameter ą¤•ą„‹ `Resolve(ask_quantity)` ą¤øą„‡ annotate ą¤•ą¤°ą„‡ą¤‚, जहाँ `ask_quantity` आपका लिखा साधारण function ą¤¹ą„ˆ, और SDK ą¤‰ą¤øą„€ mechanism ą¤øą„‡ ą¤Ŗą„‚ą¤›ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ connection support करता ą¤¹ą„ˆ, legacy session पर live elicitation request या 2026 पर multi-round-tripą„¤ ą¤ą¤• tool body, ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤: +इसका ą¤µą¤æą¤•ą¤²ą„ą¤Ŗ call ą¤•ą„‹ पलट ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ जिस tool ą¤•ą„‹ user ą¤øą„‡ ą¤•ą„ą¤› ą¤šą¤¾ą¤¹ą¤æą¤, वह सवाल **ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾** ą¤¹ą„ˆ (`InputRequiredResult`), client ą¤‰ą¤Øą„ą¤¹ą„€ą¤‚ callbacks ą¤øą„‡ उसका जवाब ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą„‹ ą¤‰ą¤øą¤•ą„‡ पास ą¤¹ą¤®ą„‡ą¤¶ą¤¾ ą¤øą„‡ ą¤„ą„‡, और call ą¤•ą„‹ ą¤œą¤µą¤¾ą¤¬ą„‹ą¤‚ ą¤•ą„‡ साऄ retry किया जाता ą¤¹ą„ˆą„¤ `Client` यह loop ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ चलाता ą¤¹ą„ˆą„¤ server पर आप result शायद ą¤¹ą„€ ą¤•ą¤­ą„€ ą¤–ą„ą¤¦ ą¤¬ą¤Øą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤•ą„ą¤Æą„‹ą¤‚ą¤•ą¤æ ą¤ą¤• **[dependency](handlers/dependencies.md)** यह कर ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆ: parameter ą¤•ą„‹ `Resolve(ask_quantity)` ą¤øą„‡ annotate ą¤•ą¤°ą„‡ą¤‚, जहाँ `ask_quantity` आपका लिखा साधारण function ą¤¹ą„ˆ, और SDK ą¤‰ą¤øą„€ mechanism ą¤øą„‡ ą¤Ŗą„‚ą¤›ą¤¤ą¤¾ ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ connection support करता ą¤¹ą„ˆ, legacy session पर live elicitation request या 2026 पर multi-round-tripą„¤ ą¤ą¤• tool body, ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤Ŗą„€ą¤¢ą¤¼ą¤æą¤Æą¤¾ą¤: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -वह file ą¤Ŗą„‚ą¤°ą„€ बात ą¤ą¤• ą¤œą¤—ą¤¹ कह ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆ: ą¤ą¤• server, ą¤ą¤• `Resolve`-backed tool, और ą¤ą¤• legacy client तऄा ą¤ą¤• modern client, ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤•ą„‹ अपना जवाब मिलता ą¤¹ą„ˆ, in memoryą„¤ **[Multi-round-trip requests](handlers/multi-round-trip.md)** mechanism ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ (`request_state` ą¤øą¤®ą„‡ą¤¤, ą¤œą¤æą¤øą„‡ SDK ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ seal और verify करता ą¤¹ą„ˆ); ą¤Ŗą„‚ą¤›ą¤Øą„‡ का ą¤¹ą¤æą¤øą„ą¤øą¤¾ **[Elicitation](handlers/elicitation.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ą¤Æą„‡ ą¤¦ą„‹ files ą¤Ŗą„‚ą¤°ą„€ बात कह ą¤¦ą„‡ą¤¤ą„€ ą¤¹ą„ˆą¤‚: ą¤ą¤• server, ą¤ą¤• `Resolve`-backed tool, और ą¤ą¤• legacy client तऄा ą¤ą¤• modern client, ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤•ą„‹ ą¤‰ą¤øą„€ ą¤šą¤²ą¤¤ą„‡ ą¤¹ą„ą¤ server ą¤øą„‡ अपना जवाब मिलता ą¤¹ą„ˆ (**[legacy clients ą¤•ą„‹ serve करना](run/legacy-clients.md)** ą¤‡ą¤Øą„ą¤¹ą„‡ą¤‚ कदम दर कदम ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ)ą„¤ **[Multi-round-trip requests](handlers/multi-round-trip.md)** mechanism ą¤øą¤®ą¤ą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ (`request_state` ą¤øą¤®ą„‡ą¤¤, ą¤œą¤æą¤øą„‡ SDK ą¤†ą¤Ŗą¤•ą„‡ ą¤²ą¤æą¤ seal और verify करता ą¤¹ą„ˆ); ą¤Ŗą„‚ą¤›ą¤Øą„‡ का ą¤¹ą¤æą¤øą„ą¤øą¤¾ **[Elicitation](handlers/elicitation.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą„¤ !!! warning "ą¤Æą¤¹ą„€ वह ą¤ą¤• ą¤œą¤—ą¤¹ ą¤¹ą„ˆ जहाँ port ą¤•ą¤æą¤ ą¤—ą¤ v1 server का ą¤µą„ą¤Æą¤µą¤¹ą¤¾ą¤° बदलता ą¤¹ą„ˆ" ą¤†ą¤Ŗą¤•ą„‡ ą¤…ą¤Ŗą¤Øą„‡ tests ą¤‡ą¤øą¤øą„‡ ą¤øą¤¬ą¤øą„‡ ą¤Ŗą¤¹ą¤²ą„‡ ą¤Ÿą¤•ą¤°ą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚: `Client(mcp)` default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ ą¤†ą¤Ŗą¤•ą„‡ v2 server ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ 2026-07-28 negotiate करता ą¤¹ą„ˆ, @@ -183,28 +187,28 @@ Streamable HTTP पर 2026 path ą¤®ą„‡ą¤‚ ą¤•ą„‹ą¤ˆ `Mcp-Session-Id` ą¤Øą¤¹ą„€ `Resolve(...)` parameter ą¤®ą„‡ą¤‚ ą¤²ą„‡ ą¤œą¤¾ą¤ą¤ (हर ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤®ą„‡ą¤‚ ą¤šą¤²ą¤Øą„‡ वाला), या अगर ą¤†ą¤Ŗą¤•ą„‹ ą¤µą¤¾ą¤•ą¤ˆ push ą¤µą„ą¤Æą¤µą¤¹ą¤¾ą¤° ą¤šą¤¾ą¤¹ą¤æą¤ ą¤¤ą„‹ test client ą¤•ą„‹ `mode="legacy"` पर pin ą¤•ą¤°ą„‡ą¤‚ą„¤ -### Roots, sampling और protocol logging deprecated ą¤¹ą„ˆą¤‚; `ping` हटा दिया गया {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} +### roots, sampling और protocol logging deprecated ą¤¹ą„ˆą¤‚; `ping` हटा दिया गया {#roots-sampling-and-protocol-logging-are-deprecated-ping-is-removed} [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) ą¤¤ą„€ą¤Ø ą¤Ŗą„‚ą¤°ą„€ **capabilities** ą¤•ą„‹ हर protocol version पर deprecated करता ą¤¹ą„ˆ: roots, sampling और MCP-level logging (`ctx.info()` ą¤µą¤—ą„ˆą¤°ą¤¹)ą„¤ यह ऊपर ą¤•ą„‡ गायब back-channel ą¤øą„‡ अलग ą¤§ą„ą¤°ą„€ ą¤¹ą„ˆ; deprecated ą¤øą¤æą¤°ą„ą¤«ą¤¼ सलाह ą¤¹ą„ˆ, 2025 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ sessions ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ सब ą¤•ą„ą¤› काम करता रहता ą¤¹ą„ˆ, और wire पर ą¤•ą„ą¤› ą¤Øą¤¹ą„€ą¤‚ ą¤¬ą¤¦ą¤²ą¤¤ą¤¾ą„¤ ą¤œą„‹ ą¤†ą¤Ŗą¤•ą„‹ दिखता ą¤¹ą„ˆ वह `MCPDeprecationWarning` ą¤¹ą„ˆ, ą¤œą„‹ `UserWarning` ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ print ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆ; मानकर ą¤šą¤²ą„‡ą¤‚ कि upgrade ą¤•ą„‡ बाद आपका पहला `ctx.info(...)` ą¤Æą¤¹ą„€ ą¤•ą¤¹ą„‡ą¤—ą¤¾ą„¤ -`ping` ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ ą¤øą¤–ą„ą¤¤ ą¤¹ą„ˆ: deprecated ą¤Øą¤¹ą„€ą¤‚, protocol ą¤øą„‡ हटा दिया ą¤—ą¤Æą¤¾ą„¤ Deprecated features ą¤•ą„‡ ą¤¦ą„‹ standalone methods ą¤­ą„€ 2026-07-28 पर ą¤‡ą¤øą„€ तरह ą¤¹ą¤Ÿą¤¾ą¤ ą¤—ą¤ ą¤¹ą„ˆą¤‚, `logging/setLevel` और client का `notifications/roots/list_changed`, और progress notifications अब ą¤øą¤æą¤°ą„ą¤«ą¤¼ server-to-client ą¤¹ą„ˆą¤‚ą„¤ +`ping` ą¤œą¤¼ą„ą¤Æą¤¾ą¤¦ą¤¾ ą¤øą¤–ą„ą¤¤ ą¤¹ą„ˆ: deprecated ą¤Øą¤¹ą„€ą¤‚, protocol ą¤øą„‡ हटा दिया ą¤—ą¤Æą¤¾ą„¤ deprecated features ą¤•ą„‡ ą¤¦ą„‹ standalone methods ą¤­ą„€ 2026-07-28 पर ą¤‡ą¤øą„€ तरह ą¤¹ą¤Ÿą¤¾ą¤ ą¤—ą¤ ą¤¹ą„ˆą¤‚, `logging/setLevel` और client का `notifications/roots/list_changed`, और progress notifications अब ą¤øą¤æą¤°ą„ą¤«ą¤¼ server-to-client ą¤¹ą„ˆą¤‚ą„¤ **[Deprecated features](deprecated.md)** ą¤®ą„‡ą¤‚ ą¤Ŗą„‚ą¤°ą„€ table, हर ą¤ą¤• का ą¤µą¤æą¤•ą¤²ą„ą¤Ŗ, और legacy clients ą¤•ą„‹ serve ą¤•ą¤°ą¤¤ą„‡ समय शांत log ą¤šą¤¾ą¤¹ą¤æą¤ ą¤¤ą„‹ one-line filter ą¤¹ą„ˆą„¤ -### Change notifications ą¤ą¤• stream बन ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ {#change-notifications-become-one-stream} +### change notifications ą¤ą¤• stream बन ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ {#change-notifications-become-one-stream} -2026-07-28 पर standalone HTTP GET stream और `resources/subscribe` ą¤•ą„€ ą¤œą¤—ą¤¹ `subscriptions/listen` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ: client ą¤ą¤• long-lived stream ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆ और बताता ą¤¹ą„ˆ कि ą¤‰ą¤øą„‡ किस तरह ą¤•ą„‡ notifications ą¤šą¤¾ą¤¹ą¤æą¤ą„¤ `MCPServer` ą¤‡ą¤øą„‡ बिना ą¤•ą„ą¤› configure ą¤•ą¤æą¤ serve करता ą¤¹ą„ˆ; आप `await ctx.notify_resource_updated(uri)` (और `notify_tools_changed()`, ą¤µą¤—ą„ˆą¤°ą¤¹) ą¤øą„‡ publish ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤ą¤• middleware हर caller ą¤•ą„‡ ą¤²ą¤æą¤ listen request ą¤ ą„ą¤•ą¤°ą¤¾ सकता ą¤¹ą„ˆ, और multi-replica deployments ą¤ą¤• ą¤øą¤¾ą¤ą¤¾ `SubscriptionBus` ą¤²ą¤—ą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ Client पर `async with client.listen(...)` stream ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆ: filter keyword arguments ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ जाता ą¤¹ą„ˆ, typed change events वापस ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और `sub.honored` वह subset ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ server deliver ą¤•ą¤°ą¤Øą„‡ पर ą¤°ą¤¾ą¤œą¤¼ą„€ ą¤¹ą„ą¤†ą„¤ +2026-07-28 पर standalone HTTP GET stream और `resources/subscribe` ą¤•ą„€ ą¤œą¤—ą¤¹ `subscriptions/listen` ą¤²ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ: client ą¤ą¤• long-lived stream ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆ और बताता ą¤¹ą„ˆ कि ą¤‰ą¤øą„‡ किस तरह ą¤•ą„‡ notifications ą¤šą¤¾ą¤¹ą¤æą¤ą„¤ `MCPServer` ą¤‡ą¤øą„‡ बिना ą¤•ą„ą¤› configure ą¤•ą¤æą¤ serve करता ą¤¹ą„ˆ; आप `await ctx.notify_resource_updated(uri)` (और `notify_tools_changed()`, ą¤µą¤—ą„ˆą¤°ą¤¹) ą¤øą„‡ publish ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤ą¤• middleware हर caller ą¤•ą„‡ ą¤²ą¤æą¤ listen request ą¤ ą„ą¤•ą¤°ą¤¾ सकता ą¤¹ą„ˆ, और multi-replica deployments ą¤ą¤• ą¤øą¤¾ą¤ą¤¾ `SubscriptionBus` ą¤²ą¤—ą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ client पर `async with client.listen(...)` stream ą¤–ą„‹ą¤²ą¤¤ą¤¾ ą¤¹ą„ˆ: filter keyword arguments ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ जाता ą¤¹ą„ˆ, typed change events वापस ą¤†ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और `sub.honored` वह subset ą¤¹ą„ˆ ą¤œą¤æą¤øą„‡ server deliver ą¤•ą¤°ą¤Øą„‡ पर ą¤°ą¤¾ą¤œą¤¼ą„€ ą¤¹ą„ą¤†ą„¤ -Publishing और serving **[Subscriptions](handlers/subscriptions.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆ, ą¤¦ą„‡ą¤–ą¤Øą„‡ वाला ą¤›ą„‹ą¤° **[ą¤‡ą¤øą¤•ą„‡ Clients ą¤µą¤¾ą¤²ą„‡ ą¤œą„ą¤”ą¤¼ą¤µą¤¾ą¤ page](client/subscriptions.md)** ą¤®ą„‡ą¤‚, और bus **[Deploy & scale](run/deploy.md)** ą¤®ą„‡ą¤‚ą„¤ +publishing और serving **[Subscriptions](handlers/subscriptions.md)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆ, ą¤¦ą„‡ą¤–ą¤Øą„‡ वाला ą¤›ą„‹ą¤° **[ą¤‡ą¤øą¤•ą„‡ Clients ą¤µą¤¾ą¤²ą„‡ ą¤œą„ą¤”ą¤¼ą¤µą¤¾ą¤ page](client/subscriptions.md)** ą¤®ą„‡ą¤‚, और bus **[Deploy & scale](run/deploy.md)** ą¤®ą„‡ą¤‚ą„¤ ### ą¤¬ą¤¾ą¤•ą„€, फटाफट {#the-rest-quickly} -* **Identity optional, per-message metadata ą¤¹ą„ˆą„¤** Request-side `clientInfo` `_meta` key optional ą¤¹ą„ˆ (ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤œą„‹ą¤”ą¤¼ą„€ `protocolVersion` + `clientCapabilities` ą¤¹ą„ˆ), और `serverInfo` `server/discover` result body ą¤øą„‡ बाहर चला गया: servers ą¤‡ą¤øą¤•ą„‡ बजाय ą¤‰ą¤øą„‡ हर 2026 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ result ą¤•ą„‡ `_meta` ą¤®ą„‡ą¤‚ stamp ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))ą„¤ SDK ą¤¹ą¤®ą„‡ą¤¶ą¤¾ stamp करता ą¤¹ą„ˆ; जब server ą¤…ą¤Ŗą¤Øą„€ पहचान ą¤Øą¤¹ą„€ą¤‚ बताता (उदाहरण ą¤•ą„‡ ą¤²ą¤æą¤, ą¤•ą¤æą¤øą„€ middleware ą¤Øą„‡ key हटा ą¤¦ą„€) ą¤¤ą„‹ `client.server_info` `None` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ **[The low-level Server](advanced/low-level-server.md)** wire पर stamp दिखाता ą¤¹ą„ˆą„¤ -* **Requests bodies parse ą¤•ą¤æą¤ बिना route ą¤¹ą„‹ ą¤øą¤•ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤** Modern HTTP requests `Mcp-Method` ą¤²ą„‡ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą¤‚ (और ą¤¤ą„€ą¤Ø tool ą¤œą„ˆą¤øą„€ calls ą¤•ą„‡ ą¤²ą¤æą¤ `Mcp-Name`); `x-mcp-header` ą¤øą„‡ annotate ą¤•ą„€ ą¤—ą¤ˆ tool input-schema property ą¤•ą„‹ `Mcp-Param-*` header ą¤®ą„‡ą¤‚ mirror किया जाता ą¤¹ą„ˆ और server ą¤‰ą¤øą„‡ cross-check करता ą¤¹ą„ˆ ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))ą„¤ Gateways और rate limiters ą¤øą¤æą¤°ą„ą¤«ą¤¼ headers पर route कर ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; नियम **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ -* **Results cache hints ą¤²ą„‡ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** List और read results `ttlMs` और `cacheScope` declare ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); आप ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ `cache_hints=` ą¤øą„‡ हर method ą¤•ą„‡ ą¤²ą¤æą¤ set ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और `Client` built-in response cache ą¤•ą„‡ साऄ उनका मान रखता ą¤¹ą„ˆą„¤ ą¤œą„‹ server ą¤•ą„‹ą¤ˆ hints ą¤Øą¤¹ą„€ą¤‚ ą¤­ą„‡ą¤œą¤¤ą¤¾ (हर pre-2026 server), ą¤‰ą¤øą„‡ जस का तस, uncached traffic दिखता ą¤¹ą„ˆą„¤ **[Caching hints](client/caching.md)**ą„¤ -* **Extensions first class ą¤¹ą„ˆą¤‚ą„¤** Servers और clients reverse-DNS identifiers ą¤•ą„‡ ą¤Øą„€ą¤šą„‡ optional capability bundles declare ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); built-in `Apps` extension (MCP Apps) reference ą¤¹ą„ˆą„¤ **[Extensions](advanced/extensions.md)** और **[MCP Apps](advanced/apps.md)**ą„¤ -* **Error codes standardized ą¤¹ą„‹ ą¤—ą¤ą„¤** गायब resource `-32602` ą¤¹ą„ˆ, `error.data` ą¤®ą„‡ą¤‚ URI ą¤•ą„‡ साऄ, और ą¤Øą¤ spec-reserved codes `-32020` (header mismatch), `-32021` (ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ capability गायब) और `-32022` (unsupported protocol version) ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ **[Troubleshooting](troubleshooting.md)** ą¤ ą„€ą¤• ą¤‰ą¤Øą„ą¤¹ą„€ą¤‚ messages ą¤•ą„‡ हिसाब ą¤øą„‡ ą¤µą„ą¤Æą¤µą¤øą„ą¤„ą¤æą¤¤ ą¤¹ą„ˆą„¤ -* **Authorization ą¤•ą„‹ गलत पकऔ़ना अब ą¤®ą„ą¤¶ą„ą¤•ą¤æą¤² ą¤¹ą„ˆą„¤** Client authorization code ą¤•ą„‡ साऄ ą¤²ą„Œą¤Ÿą„‡ `iss` ą¤•ą„‹ validate करता ą¤¹ą„ˆ ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207); आपका `callback_handler` अब `AuthorizationCodeResult` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ), register ą¤•ą¤°ą¤¤ą„‡ समय `application_type` ą¤­ą„‡ą¤œą¤¤ą¤¾ ą¤¹ą„ˆ, और credentials ą¤•ą„‹ ą¤•ą¤­ą„€ ą¤•ą¤æą¤øą„€ ą¤¦ą„‚ą¤øą¤°ą„‡ authorization server ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ replay ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ Enterprise ą¤•ą„‹ą¤Øą„‡ ą¤®ą„‡ą¤‚ नया: [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) identity-assertion flową„¤ **[Migration Guide](migration.md)** हर OAuth बदलाव ą¤•ą„€ ą¤øą„‚ą¤šą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; pages **[OAuth for clients](client/oauth-clients.md)** और **[Identity assertion](client/identity-assertion.md)** ą¤¹ą„ˆą¤‚ą„¤ +* **identity optional, per-message metadata ą¤¹ą„ˆą„¤** request-side `clientInfo` `_meta` key optional ą¤¹ą„ˆ (ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ ą¤œą„‹ą¤”ą¤¼ą„€ `protocolVersion` + `clientCapabilities` ą¤¹ą„ˆ), और `serverInfo` `server/discover` result body ą¤øą„‡ बाहर चला गया: servers ą¤‡ą¤øą¤•ą„‡ बजाय ą¤‰ą¤øą„‡ हर 2026 ą¤Ŗą„€ą¤¢ą¤¼ą„€ ą¤•ą„‡ result ą¤•ą„‡ `_meta` ą¤®ą„‡ą¤‚ stamp ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002))ą„¤ SDK ą¤¹ą¤®ą„‡ą¤¶ą¤¾ stamp करता ą¤¹ą„ˆ; जब server ą¤…ą¤Ŗą¤Øą„€ पहचान ą¤Øą¤¹ą„€ą¤‚ बताता (उदाहरण ą¤•ą„‡ ą¤²ą¤æą¤, ą¤•ą¤æą¤øą„€ middleware ą¤Øą„‡ key हटा ą¤¦ą„€) ą¤¤ą„‹ `client.server_info` `None` ą¤¹ą„‹ą¤¤ą¤¾ ą¤¹ą„ˆą„¤ **[The low-level Server](advanced/low-level-server.md)** wire पर stamp दिखाता ą¤¹ą„ˆą„¤ +* **requests bodies parse ą¤•ą¤æą¤ बिना route ą¤¹ą„‹ ą¤øą¤•ą¤¤ą„€ ą¤¹ą„ˆą¤‚ą„¤** modern HTTP requests `Mcp-Method` ą¤²ą„‡ ą¤œą¤¾ą¤¤ą„€ ą¤¹ą„ˆą¤‚ (और ą¤¤ą„€ą¤Ø tool ą¤œą„ˆą¤øą„€ calls ą¤•ą„‡ ą¤²ą¤æą¤ `Mcp-Name`); `x-mcp-header` ą¤øą„‡ annotate ą¤•ą„€ ą¤—ą¤ˆ tool input-schema property ą¤•ą„‹ `Mcp-Param-*` header ą¤®ą„‡ą¤‚ mirror किया जाता ą¤¹ą„ˆ और server ą¤‰ą¤øą„‡ cross-check करता ą¤¹ą„ˆ ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243))ą„¤ gateways और rate limiters ą¤øą¤æą¤°ą„ą¤«ą¤¼ headers पर route कर ą¤øą¤•ą¤¤ą„‡ ą¤¹ą„ˆą¤‚; नियम **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** ą¤®ą„‡ą¤‚ ą¤¹ą„ˆą¤‚ą„¤ +* **results cache hints ą¤²ą„‡ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤** list और read results `ttlMs` और `cacheScope` declare ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); आप ą¤‰ą¤Øą„ą¤¹ą„‡ą¤‚ `cache_hints=` ą¤øą„‡ हर method ą¤•ą„‡ ą¤²ą¤æą¤ set ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, और `Client` built-in response cache ą¤•ą„‡ साऄ उनका मान रखता ą¤¹ą„ˆą„¤ ą¤œą„‹ server ą¤•ą„‹ą¤ˆ hints ą¤Øą¤¹ą„€ą¤‚ ą¤­ą„‡ą¤œą¤¤ą¤¾ (हर pre-2026 server), ą¤‰ą¤øą„‡ जस का तस, uncached traffic दिखता ą¤¹ą„ˆą„¤ **[Caching hints](client/caching.md)**ą„¤ +* **extensions first class ą¤¹ą„ˆą¤‚ą„¤** servers और clients reverse-DNS identifiers ą¤•ą„‡ ą¤Øą„€ą¤šą„‡ optional capability bundles declare ą¤•ą¤°ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); built-in `Apps` extension (MCP Apps) reference ą¤¹ą„ˆą„¤ **[Extensions](advanced/extensions.md)** और **[MCP Apps](advanced/apps.md)**ą„¤ +* **error codes standardized ą¤¹ą„‹ ą¤—ą¤ą„¤** गायब resource `-32602` ą¤¹ą„ˆ, `error.data` ą¤®ą„‡ą¤‚ URI ą¤•ą„‡ साऄ, और ą¤Øą¤ spec-reserved codes `-32020` (header mismatch), `-32021` (ą¤œą¤¼ą¤°ą„‚ą¤°ą„€ capability गायब) और `-32022` (unsupported protocol version) ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ ą¤¦ą¤æą¤–ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ **[Troubleshooting](troubleshooting.md)** ą¤ ą„€ą¤• ą¤‰ą¤Øą„ą¤¹ą„€ą¤‚ messages ą¤•ą„‡ हिसाब ą¤øą„‡ ą¤µą„ą¤Æą¤µą¤øą„ą¤„ą¤æą¤¤ ą¤¹ą„ˆą„¤ +* **authorization ą¤•ą„‹ गलत पकऔ़ना अब ą¤®ą„ą¤¶ą„ą¤•ą¤æą¤² ą¤¹ą„ˆą„¤** client authorization code ą¤•ą„‡ साऄ ą¤²ą„Œą¤Ÿą„‡ `iss` ą¤•ą„‹ validate करता ą¤¹ą„ˆ ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207); आपका `callback_handler` अब `AuthorizationCodeResult` ą¤²ą„Œą¤Ÿą¤¾ą¤¤ą¤¾ ą¤¹ą„ˆ), register ą¤•ą¤°ą¤¤ą„‡ समय `application_type` ą¤­ą„‡ą¤œą¤¤ą¤¾ ą¤¹ą„ˆ, और credentials ą¤•ą„‹ ą¤•ą¤­ą„€ ą¤•ą¤æą¤øą„€ ą¤¦ą„‚ą¤øą¤°ą„‡ authorization server ą¤•ą„‡ ą¤øą¤¾ą¤®ą¤Øą„‡ replay ą¤Øą¤¹ą„€ą¤‚ ą¤•ą¤°ą¤¤ą¤¾ą„¤ enterprise ą¤•ą„‹ą¤Øą„‡ ą¤®ą„‡ą¤‚ नया: [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) identity-assertion flową„¤ **[Migration Guide](migration.md)** हर OAuth बदलाव ą¤•ą„€ ą¤øą„‚ą¤šą„€ ą¤¦ą„‡ą¤¤ą¤¾ ą¤¹ą„ˆ; pages **[OAuth for clients](client/oauth-clients.md)** और **[Identity assertion](client/identity-assertion.md)** ą¤¹ą„ˆą¤‚ą„¤ * **हर server traceable ą¤¹ą„ˆą„¤** OpenTelemetry middleware ą¤•ą„‡ ą¤°ą„‚ą¤Ŗ ą¤®ą„‡ą¤‚ default ą¤°ą„‚ą¤Ŗ ą¤øą„‡ ą¤šą¤¾ą¤²ą„‚ आता ą¤¹ą„ˆ: हर request ą¤•ą„‹ ą¤ą¤• server span मिलता ą¤¹ą„ˆ, और जब तक process ą¤•ą„‹ą¤ˆ exporter configure न ą¤•ą¤°ą„‡, ą¤‡ą¤øą¤•ą„€ ą¤•ą„‹ą¤ˆ लागत ą¤Øą¤¹ą„€ą¤‚ą„¤ जब ą¤¦ą„‹ą¤Øą„‹ą¤‚ ą¤›ą„‹ą¤° SDK ą¤šą¤²ą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚, ą¤¤ą„‹ client `_meta` ą¤®ą„‡ą¤‚ W3C trace context ą¤­ą„€ propagate करता ą¤¹ą„ˆ, ą¤‡ą¤øą¤²ą¤æą¤ traces ą¤œą„ą¤”ą¤¼ ą¤œą¤¾ą¤¤ą„‡ ą¤¹ą„ˆą¤‚ą„¤ **[OpenTelemetry](run/opentelemetry.md)**ą„¤ ## v1 ą¤øą„‡ upgrade कर ą¤°ą¤¹ą„‡ ą¤¹ą„ˆą¤‚? {#upgrading-from-v1} diff --git a/i18n/ja/pages/advanced/apps.md b/i18n/ja/pages/advanced/apps.md index 497d06be..d1b4bf58 100644 --- a/i18n/ja/pages/advanced/apps.md +++ b/i18n/ja/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK ćÆć“ć‚Œć‚’ēµ„ćæč¾¼ćæć® `Apps` ę‹”å¼µļ¼ˆ`io.modelcontextprotocol/ui`) ## č¦‹ćŸē›®ć®ć‚ć‚‹ę™‚čØˆ {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ HTML č‡Ŗä½“ćÆćƒ›ć‚¹ćƒˆć® `postMessage` ć‚’å¾…ć”å—ć‘ć¦ēµęžœć‚’č”Øē¤ŗć—ć¾ ćƒ¢ćƒ‡ćƒ«ćŒčŖ­ć‚€ć®ćÆ `content` 恧态iframe ćÆäŗŗé–“ć®ćŸć‚ć®ć‚‚ć®ć§ć™ć€‚UI ć«åÆ¾åæœć—ćŸćƒ›ć‚¹ćƒˆć§ć‚‚ćƒ†ć‚­ć‚¹ćƒˆć®ēµęžœćÆćƒ¢ćƒ‡ćƒ«ć«ęø”ć•ć‚Œć¾ć™ć—ć€ćƒ†ć‚­ć‚¹ćƒˆå°‚ē”Øć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆćć‚Œć€Œć ć‘ć€ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ć§ć™ć‹ć‚‰å®šē•Ŗć®ćƒ‘ć‚æćƒ¼ćƒ³ćÆć€Œ1 ć¤ć®ćƒ„ćƒ¼ćƒ«ć€2 ć¤ć®ē­”ćˆć€ć§ć™ć€‚ć‚‚ć†äø€åŗ¦ `get_time` ć‚’č¦‹ć¦ćć ć•ć„ć€‚ -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` が `True` ć«ćŖć‚‹ć®ćÆć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒ `io.modelcontextprotocol/ui` 拔張を宣言し、**恋恤** `mimeTypes` 設定に `text/html;profile=mcp-app` ć‚’å«ć‚ć¦ć„ć‚‹å “åˆć ć‘ć§ć™ć€‚ć“ć®ćƒ•ć‚£ćƒ¼ćƒ«ćƒ‰ćÆåæ…é ˆćŖć®ć§ć€ēœē•„ć—ćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆč©²å½“ć—ć¾ć›ć‚“ć€‚åŒć˜ćƒ•ć‚”ć‚¤ćƒ«ć® `main()` ćŒå®£čØ€ć—ć¦ć„ć‚‹ć®ćÆć¾ć•ć«ć“ć‚Œć§ć™ć€‚ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć§ć‚ć‚Šć€ćć®ēµęžœćƒŖćƒƒćƒćŖē­”ćˆćŒčæ”ć£ć¦ćć¾ć™ć€‚ +`client_supports_apps(ctx)` が `True` ć«ćŖć‚‹ć®ćÆć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒ `io.modelcontextprotocol/ui` 拔張を宣言し、**恋恤** `mimeTypes` 設定に `text/html;profile=mcp-app` ć‚’å«ć‚ć¦ć„ć‚‹å “åˆć ć‘ć§ć™ć€‚ć“ć®ćƒ•ć‚£ćƒ¼ćƒ«ćƒ‰ćÆåæ…é ˆćŖć®ć§ć€ēœē•„ć—ćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆč©²å½“ć—ć¾ć›ć‚“ć€‚ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ćÆę¬”ć®ćØćŠć‚Šć§ć™ć€‚ + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` 悒 HTTP ć§ęä¾›ć—ć€åˆ„ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +ćƒŖćƒƒćƒćŖē­”ćˆćŒčæ”ć£ć¦ćć¾ć—ćŸć€‚`Client` の呼び出しから `extensions=[APPS_SUPPORT]` ć‚’å¤–ć™ćØć€åŒć˜ćƒ—ćƒ­ć‚°ćƒ©ćƒ ćÆä»£ć‚ć‚Šć« `The time is 2026-06-26T12:00:00Z.` ćØč”Øē¤ŗć—ć¾ć™ć€‚ćƒ†ć‚­ć‚¹ćƒˆå°‚ē”Øć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒē›®ć«ć™ć‚‹ć®ćÆć€ć“ć‚ŒćŒć™ć¹ć¦ć§ć™ć€‚ !!! warning `"[Rendered UI]"` ć®ć‚ˆć†ćŖćƒ—ćƒ¬ćƒ¼ć‚¹ćƒ›ćƒ«ćƒ€ćƒ¼ć‚’å”Æäø€ć®ć‚³ćƒ³ćƒ†ćƒ³ćƒ„ćØć—ć¦čæ”ć•ćŖć„ć§ćć ć•ć„ć€‚ćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚Æć®ćƒ†ć‚­ć‚¹ćƒˆćŒå½¹ć«ē«‹ćŸćŖć‘ć‚Œć°ć€ćć®ćƒ„ćƒ¼ćƒ«ćÆćƒ†ć‚­ć‚¹ćƒˆå°‚ē”Øć®ć™ć¹ć¦ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćØć£ć¦ć‚‚ć€ćƒ¢ćƒ‡ćƒ«č‡Ŗčŗ«ć«ćØć£ć¦ć‚‚å½¹ć«ē«‹ć”ć¾ć›ć‚“ć€‚ćć”ć‚“ćØę–‡ć‚’ę›øć„ć¦ćć ć•ć„ć€‚ diff --git a/i18n/ja/pages/advanced/extensions.md b/i18n/ja/pages/advanced/extensions.md index 5734fafd..8bf2d07d 100644 --- a/i18n/ja/pages/advanced/extensions.md +++ b/i18n/ja/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # ę‹”å¼µę©Ÿčƒ½ {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string å½¹ć«ē«‹ć¤ęœ€å°ć®ę‹”å¼µę©Ÿčƒ½ćÆć€ćƒ„ćƒ¼ćƒ« 1 ć¤ćØčØ­å®šćƒžćƒƒćƒ— 1 恤恧恙怂 -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` は `capabilities.extensions["com.example/stamps"]` ć«ć‚¢ćƒ‰ćƒć‚æć‚¤ć‚ŗć•ć‚Œć‚‹å€¤ć§ć™ć€‚čØ­å®šćŖć—ć§ę‹”å¼µę©Ÿčƒ½ć‚’ć‚¢ćƒ‰ćƒć‚æć‚¤ć‚ŗć™ć‚‹ć«ćÆ `{}`ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆļ¼‰ć‚’čæ”ć—ć¦ćć ć•ć„ć€‚ * ę‹”å¼µę©Ÿčƒ½ćŒć‚µćƒ¼ćƒćƒ¼ć‚’å—ć‘å–ć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć€‚ęä¾›ć™ć‚‹ć‚‚ć®ć‚’ćƒ‡ćƒ¼ć‚æćØć—ć¦å®£čØ€ć—ć€`MCPServer` ćŒćć‚Œć‚’ę¶ˆč²»ć—ć¾ć™ć€‚ę›øćę›ćˆć‚‰ć‚Œć‚‹ `self.server` ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ -ćć—ć¦ `main()` ćŒćć®čØ¼ę˜Žć§ć™ć€‚`mcp` ć«ē›“ęŽ„ć¤ćŖćć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć™ć€‚ +HTTP ć§é…äæ”ć™ć‚Œć°ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćć®čØ¼ę˜Žć«ćŖć‚Šć¾ć™ć€‚ -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +ć“ć®ćƒšćƒ¼ć‚øć® `server.py` ćÆć©ć‚Œć‚‚ć“ć®ć‚³ćƒžćƒ³ćƒ‰ć§é…äæ”ć—ć€`client.py` ćÆć©ć‚Œć‚‚ćć®ęØŖć§ć€2 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ `python client.py` ć§å®Ÿč”Œć—ć¾ć™ć€‚ + ### ē‹¬č‡Ŗćƒ”ć‚½ćƒƒćƒ‰ć®ęä¾› {#serving-your-own-methods} ę‹”å¼µę©Ÿčƒ½ćÆ**ę–°ć—ć„ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒ”ć‚½ćƒƒćƒ‰**ć‚’ē™»éŒ²ć§ćć¾ć™ć€‚ä»•ę§˜ć®ćƒ”ć‚½ćƒƒćƒ‰ćØäø¦ć‚“ć§é…äæ”ć•ć‚Œć‚‹ć€ē‹¬č‡Ŗć®å‹•č©žļ¼ˆverb)です。 -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ {#the-client-side} -åŒć˜ćƒ•ć‚”ć‚¤ćƒ«ć® `main()` ć«ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®č©±ćŒć™ć¹ć¦ć€ćć®äø”åŠåˆ†ćØć‚‚å…„ć£ć¦ć„ć¾ć™ć€‚ +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆē‹¬ē«‹ć—ćŸćƒ—ćƒ­ć‚°ćƒ©ćƒ ć§ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®č©±ć®äø”åŠåˆ†ć‚’ę‹…ć£ć¦ć„ć¾ć™ć€‚ -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` ćŒę‹”å¼µę©Ÿčƒ½ć‚’å®£čØ€ć—ć¾ć™ć€‚å®£čØ€ćÆ `ClientCapabilities.extensions` ć«ćŖć‚Šć¾ć™ć€‚2026-07-28 ęŽ„ē¶šć§ćÆć€ć“ć®ćƒžćƒƒćƒ—ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć”ćØć® `_meta` ć‚Øćƒ³ćƒ™ćƒ­ćƒ¼ćƒ—ć§é‹ć°ć‚Œć‚‹ćŸć‚ć€ć‚µćƒ¼ćƒćƒ¼ćÆ**すべての**ćƒŖć‚Æć‚Øć‚¹ćƒˆć§ćć‚Œć‚’č¦‹ć¾ć™ć€‚ćƒ¬ć‚¬ć‚·ćƒ¼ęŽ„ē¶šć§ćÆ `initialize` ćƒćƒ³ćƒ‰ć‚·ć‚§ć‚¤ć‚Æć«č¼‰ć‚Šć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć®ć‚³ćƒ¼ćƒ‰ćÆć©ć”ć‚‰ć§ć‚‚ę°—ć«ć—ć¾ć›ć‚“ć€‚`require_client_extension(ctx, ...)` と `ctx.session.check_client_capability(...)` ćÆć€ć©ć”ć‚‰ć®ēµŒč·Æć§ć‚‚ę­£ć—ć„ęƒ…å ±ęŗć‚’čŖ­ćæć¾ć™ć€‚ * ćƒ™ćƒ³ćƒ€ćƒ¼ćƒ”ć‚½ćƒƒćƒ‰ćÆ 1 å±¤äø‹ćŒć£ć¦ `client.session.send_request(...)` を使います。`Client` ćŒćƒ•ć‚”ćƒ¼ć‚¹ćƒˆć‚Æćƒ©ć‚¹ć®ćƒ”ć‚½ćƒƒćƒ‰ć‚’å¢—ć‚„ć™ć®ćÆä»•ę§˜ć®å‹•č©žć«åÆ¾ć—ć¦ć ć‘ć§ć™ć€‚`send_request` はどんな `Request` ć‚µćƒ–ć‚Æćƒ©ć‚¹ć‚‚å—ć‘ä»˜ć‘ć‚‹ćŸć‚ć€ćƒ™ćƒ³ćƒ€ćƒ¼ćƒŖć‚Æć‚Øć‚¹ćƒˆćÆćć®ć¾ć¾ęø”ć›ć¾ć™ć€‚ +* `SearchRequest` ćØć€ćć‚ŒćŒé‹ć¶ 2 ć¤ć®ćƒ¢ćƒ‡ćƒ«ćÆę‹”å¼µę©Ÿčƒ½ć®é€šäæ”äøŠć®å„‘ē“„ćŖć®ć§ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆč‡Ŗåˆ†ć§ćć‚Œć‚‰ć‚’å®£čØ€ć—ć¾ć™ć€‚å…¬é–‹ć•ć‚ŒćŸę‹”å¼µę©Ÿčƒ½ćŖć‚‰ć€äø”å“ćŒć‚¤ćƒ³ćƒćƒ¼ćƒˆć™ć‚‹ćƒ‘ćƒƒć‚±ćƒ¼ć‚øć«å«ć‚ć¦é…åøƒć™ć‚‹ć§ć—ć‚‡ć†ć€‚ ### `tools/call` ć®ć‚¤ćƒ³ć‚æćƒ¼ć‚»ćƒ—ćƒˆ {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆę‹”å¼µę©Ÿčƒ½ć‚’ä½æć† {#using-a-client-extension} -**ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆę‹”å¼µę©Ÿčƒ½**ćÆć€åŒć˜å„‘ē“„ć‚’åˆ©ē”Øć™ć‚‹å“ć‹ć‚‰č¦‹ćŸć‚‚ć®ć§ć™ć€‚1 ć¤ć®č­˜åˆ„å­ć®äø‹ć«ć¾ćØć‚ć‚‰ć‚ŒćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®ęŒÆć‚‹čˆžć„äø€å¼ć§ć™ć€‚ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć‚’ `Client(extensions=[...])` ć«ęø”ć—ć€é€šåøøć©ćŠć‚Šćƒ„ćƒ¼ćƒ«ć‚’å‘¼ć³å‡ŗć—ć¾ć™ć€‚ +**ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆę‹”å¼µę©Ÿčƒ½**ćÆć€åŒć˜å„‘ē“„ć‚’åˆ©ē”Øć™ć‚‹å“ć‹ć‚‰č¦‹ćŸć‚‚ć®ć§ć™ć€‚1 ć¤ć®č­˜åˆ„å­ć®äø‹ć«ć¾ćØć‚ć‚‰ć‚ŒćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®ęŒÆć‚‹čˆžć„äø€å¼ć§ć™ć€‚ć“ć“ć§ć®ć‚µćƒ¼ćƒćƒ¼ćÆ `buy` ć«åÆ¾ć—ć¦ć€å“ē‰©ć§ćÆćŖćå¼•ćę›ćˆē”Øć®ćƒ¬ć‚·ćƒ¼ćƒˆć§ē­”ćˆć¾ć™ć€‚ćŸć ć—ć€ę‹”å¼µę©Ÿčƒ½ć‚’å®£čØ€ć—ćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«åÆ¾ć—ć¦ć ć‘ć§ć™ć€‚ -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ćÆć€ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć‚’ `Client(extensions=[...])` ć«ęø”ć—ć€é€šåøøć©ćŠć‚Šćƒ„ćƒ¼ćƒ«ć‚’å‘¼ć³å‡ŗć—ć¾ć™ć€‚ + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` ćÆć€ä»–ć®ć™ć¹ć¦ć®å‘¼ć³å‡ŗć—ćØåŒę§˜ć«ćƒ—ćƒ¬ćƒ¼ćƒ³ćŖ `CallToolResult` ć‚’čæ”ć—ć¾ć™ć€‚ę‹”å¼µę©Ÿčƒ½ćŒå¤‰ćˆćŸć®ćÆę¬”ć®ē‚¹ć§ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ćÆ `buy` ć«åÆ¾ć—ć¦ć€ęœ€ēµ‚ēµęžœć®ä»£ć‚ć‚Šć« `receipt` という**ēµęžœć®å½¢ēŠ¶**ć§ē­”ćˆć‚‰ć‚Œć‚‹ć‚ˆć†ć«ćŖć‚Šć€`call_tool` ćŒęˆ»ć‚‹å‰ć« `Receipts` ćŒćć‚Œć‚’å®Œäŗ†ć•ć›ć¾ć™ļ¼ˆć“ć“ć§ćÆå¾Œē¶šć®å‘¼ć³å‡ŗć—ć§ćƒ¬ć‚·ćƒ¼ćƒˆć‚’å¼•ćę›ćˆć¾ć™ļ¼‰ć€‚å‘¼ć³å‡ŗć—å“ć®ć‚³ćƒ¼ćƒ‰ćÆä½•ć‚‚å¤‰ć‚ć‚Šć¾ć›ć‚“ć€‚ ę‹”å¼µę©Ÿčƒ½ć‚’å¤–ć›ć°ć€ć“ć®ć©ć‚Œć‚‚å­˜åœØć—ć¾ć›ć‚“ć€‚ć‚µćƒ¼ćƒćƒ¼ć®ć‚²ćƒ¼ćƒˆćÆå®£čØ€ć—ćŖć‹ć£ćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ę‹’å¦ć—ļ¼ˆć‚Øćƒ©ćƒ¼ -32021ļ¼‰ć€ć‚²ćƒ¼ćƒˆć‚’ēœć„ćŸć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰å±Šć„ćŸå¼•ćå—ć‘åÆ¾č±”ć®å½¢ēŠ¶ćÆę¤œčØ¼ć«å¤±ę•—ć—ć¾ć™ć€‚čŖč­˜ć§ććŖć„ `resultType` ć«åÆ¾ć—ć¦ä»•ę§˜ćŒę±‚ć‚ć‚‹ćØćŠć‚Šć§ć™ć€‚é€šäæ”č·Æć®äø”ē«Æć§ć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆć‚Ŗćƒ•ć§ć™ć€‚ @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆę‹”å¼µę©Ÿčƒ½ć‚’ę›øć {#writing-a-client-extension} `ClientExtension` ć‚’ć‚µćƒ–ć‚Æćƒ©ć‚¹åŒ–ć—ć€åæ…č¦ćŖć‚‚ć®ć ć‘ć‚’ć‚Ŗćƒ¼ćƒćƒ¼ćƒ©ć‚¤ćƒ‰ć—ć¾ć™ć€‚ęä¾›ć§ćć‚‹ć‚‚ć®ćÆ 3 ēØ®é”žć§ć€ćć‚Œćžć‚Œć«ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćŒć‚ć‚Šć¾ć™ć€‚`settings()`态`claims()`态`notifications()` 恧恙怂 -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * č­˜åˆ„å­ćÆć‚µćƒ¼ćƒćƒ¼å“ćØåŒć˜ę–‡ę³•ć«å¾“ć„ć€ć‚Æćƒ©ć‚¹ć®å®šē¾©ę™‚ć«ę¤œčØ¼ć•ć‚Œć¾ć™ć€‚ @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### ę‹”å¼µę©Ÿčƒ½ć®å‹•č©ž {#extension-verbs} -ę‹”å¼µę©Ÿčƒ½ē‹¬č‡Ŗć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒ”ć‚½ćƒƒćƒ‰ć«ćÆć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®ē™»éŒ²ćÆäøč¦ć§ć™ć€‚ćƒ™ćƒ³ćƒ€ćƒ¼ćƒŖć‚Æć‚Øć‚¹ćƒˆåž‹ćÆ `mcp.types.Request` ć‚’ć‚µćƒ–ć‚Æćƒ©ć‚¹åŒ–ć—ć€[ē‹¬č‡Ŗćƒ”ć‚½ćƒƒćƒ‰ć®ęä¾›](#serving-your-own-methods)と同様に `client.session.send_request` ć‚’é€šć‚Šć¾ć™ć€‚čæ½åŠ ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć®ć‚­ćƒ¼ć‚’ `Mcp-Name` ćƒ˜ćƒƒćƒ€ćƒ¼ć«č¼‰ć›ćŖć‘ć‚Œć°ćŖć‚‰ćŖć„å “åˆļ¼ˆtasks ć®ć‚ˆć†ćŖę‹”å¼µę©Ÿčƒ½ć®ä»•ę§˜ć§ćÆć€ćć®å‹•č©žć«ć“ć‚ŒćŒåæ…č¦ć§ć™ļ¼‰ć€ćƒŖć‚Æć‚Øć‚¹ćƒˆåž‹ćÆ `name_param` を宣言します。 +ę‹”å¼µę©Ÿčƒ½ē‹¬č‡Ŗć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒ”ć‚½ćƒƒćƒ‰ć«ćÆć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®ē™»éŒ²ćÆäøč¦ć§ć™ć€‚ćƒ™ćƒ³ćƒ€ćƒ¼ćƒŖć‚Æć‚Øć‚¹ćƒˆåž‹ćÆ `mcp.types.Request` ć‚’ć‚µćƒ–ć‚Æćƒ©ć‚¹åŒ–ć—ć€[ē‹¬č‡Ŗćƒ”ć‚½ćƒƒćƒ‰ć®ęä¾›](#serving-your-own-methods)と同様に `client.session.send_request` ć‚’é€šć‚Šć¾ć™ć€‚åå‰ä»˜ćć®ć‚øćƒ§ćƒ–ć«é–¢ć™ć‚‹å‹•č©žć‚’ 1 ć¤ć€ę‹”å¼µę©Ÿčƒ½ćŒé…äæ”ć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ć‚’ä¾‹ć«å–ć‚Šć¾ć—ć‚‡ć†ć€‚ -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć§ć®čæ½åŠ ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć®ć‚­ćƒ¼ć‚’ `Mcp-Name` ćƒ˜ćƒƒćƒ€ćƒ¼ć«č¼‰ć›ćŖć‘ć‚Œć°ćŖć‚‰ćŖć„å “åˆļ¼ˆtasks ć®ć‚ˆć†ćŖę‹”å¼µę©Ÿčƒ½ć®ä»•ę§˜ć§ćÆć€ćć®å‹•č©žć«ć“ć‚ŒćŒåæ…č¦ć§ć™ļ¼‰ć€ćƒŖć‚Æć‚Øć‚¹ćƒˆåž‹ćÆ `name_param` を宣言します。 + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + ć‚»ćƒƒć‚·ćƒ§ćƒ³ćÆć©ć®é€äæ”ēµŒč·Æć§ć‚‚ `params["jobId"]` 悒 `Mcp-Name` ć«åę˜ ć—ć€å€¤ćŒę¬ ć‘ć¦ć„ć‚‹å “åˆćÆåæ…é ˆćƒ˜ćƒƒćƒ€ćƒ¼ć‚’é»™ć£ć¦ēœćć®ć§ćÆćŖćć€ćÆć£ćć‚ŠćØć‚Øćƒ©ćƒ¼ć«ćŖć‚Šć¾ć™ć€‚ ## ę‹”å¼µę©Ÿčƒ½ć«ć§ććŖć„ć“ćØ {#what-an-extension-cannot-do} diff --git a/i18n/ja/pages/advanced/low-level-server.md b/i18n/ja/pages/advanced/low-level-server.md index c940ae82..6681b622 100644 --- a/i18n/ja/pages/advanced/low-level-server.md +++ b/i18n/ja/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # ä½Žćƒ¬ćƒ™ćƒ«ć® Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 試してみる {#try-it} -ć“ć‚Œć«ćÆ Inspector ćŒć‚ć‚Šć¾ć›ć‚“ć€‚`mcp dev` と `mcp run` は `MCPServer` ć—ć‹å—ć‘ä»˜ć‘ćŖć„ć‹ć‚‰ć§ć™ć€‚ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć® `Client` は気にしません。`MCPServer` ć‚’å—ć‘å–ć‚‹ć®ćØć¾ć£ćŸćåŒć˜ć‚ˆć†ć«ć€ä½Žćƒ¬ćƒ™ćƒ«ć® `Server` ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ +`mcp dev` と `mcp run` は `MCPServer` ć—ć‹å—ć‘ä»˜ć‘ćŖć„ć®ć§ć€ć“ć®ć‚µćƒ¼ćƒćƒ¼ćÆč‡Ŗåˆ†ć§čµ·å‹•ć—ć¾ć™ć€‚`server.py` ć®ęœ€å¾Œć®č”ŒćŒć“ć“ć‹ć‚‰ę™®é€šć® ASGI ć‚¢ćƒ—ćƒŖć‚’ēµ„ćæē«‹ć¦ć€uvicorn ćŒćć‚Œć‚’å®Ÿč”Œć—ć¾ć™ć€‚ -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector ć§ć‚‚ć€ć©ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć‚‚ć€`http://localhost:8000/mcp` ć«å‘ć‘ć¦ćć ć•ć„ć€‚ + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` は `None` ć§ć™ć€‚é«˜ćƒ¬ćƒ™ćƒ«ć®ć‚µćƒ¼ćƒćƒ¼ćÆ `-> str` 悒 `{"result": ...}` ć«ćƒ©ćƒƒćƒ—ć—ć¦ćć‚Œć¾ć™ćŒć€ć“ć“ć§ćÆč‡Ŗåˆ†ć§ēµ„ćæē«‹ć¦ćŖć‹ć£ćŸć‚‚ć®ć‚’čŖ°ć‚‚ēµ„ćæē«‹ć¦ć¦ćć‚Œć¾ć›ć‚“ć€‚ * `list_tools` は**č‡Ŗåˆ†ć§**ę‰“ć”č¾¼ć‚“ć ć‚¹ć‚­ćƒ¼ćƒžć‚’äø€å­—äø€å„ćć®ć¾ć¾čæ”ć—ć¾ć™ć€‚é«˜ćƒ¬ćƒ™ćƒ«ē‰ˆć«ćÆć™ć¹ć¦ć®ćƒ—ćƒ­ćƒ‘ćƒ†ć‚£ć« `"title": "Query"` ćŒć‚ć‚Šć€ćƒ«ćƒ¼ćƒˆć« `"title": "search_booksArguments"` ćŒć‚ć‚Šć¾ć—ćŸć€‚Pydantic ć®ē”£ē‰©ć§ć™ć€‚ć“ć®å±¤ć§ćÆć€é€šäæ”äøŠć«ē¾ć‚Œć‚‹ć‚‚ć®ćÆć™ć¹ć¦č‡Ŗåˆ†ćŒč¼‰ć›ćŸć‚‚ć®ć§ć™ć€‚ +ćƒ†ć‚¹ćƒˆć§ćÆ uvicorn ć‚‚ćƒćƒ¼ćƒˆć‚‚ēœć‘ć¾ć™ć€‚`Client(server)` は `MCPServer` ć‚’å—ć‘å–ć‚‹ć®ćØć¾ć£ćŸćåŒć˜ć‚ˆć†ć«ć€ä½Žćƒ¬ćƒ™ćƒ«ć® `Server` ć‚’ć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ć§å—ć‘å–ć‚Šć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ćŒć¾ć•ć«ćć®ćƒ‘ć‚æćƒ¼ćƒ³ć§ć™ć€‚ + ## ä½•ć‚‚ćƒć‚§ćƒƒć‚Æć•ć‚ŒćŖć„ {#nothing-is-checked-for-you} `MCPServer` ćÆć€ē”Ÿęˆć—ćŸć‚¹ć‚­ćƒ¼ćƒžć«ē…§ć‚‰ć—ć¦å‘¼ć³å‡ŗć—ć‚’ę¤œčØ¼ć—ć€é–¢ę•°ćŒå®Ÿč”Œć•ć‚Œć‚‹å‰ć«äøę­£ćŖå¼•ę•°ć‚’ę‹’å¦ć—ć¾ć™ļ¼ˆ**[ćƒ„ćƒ¼ćƒ«](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` ćÆä»»ę„ć®ćƒ”ć‚½ćƒƒćƒ‰ć‚’ęä¾›ć—ć¾ć™ć€‚`initialize` ćÆäŗˆē“„ć•ć‚Œć¦ć„ć¾ć™ć€‚ * `Server` ćŒå…¬é–‹ć™ć‚‹ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ćÆć€ć©ć®ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć‚’ē™»éŒ²ć—ćŸć‹ć‹ć‚‰å°Žå‡ŗć•ć‚Œć¾ć™ć€‚ -`Client(server)` ćŒäø”ę–¹ć®ć‚µćƒ¼ćƒćƒ¼ć‚’åŒć˜ć‚ˆć†ć«ę‰±ć£ćŸć®ćÆć€äø”č€…ćŒć¾ć•ć«åŒć˜ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć ć‹ć‚‰ć§ć‚ć‚Šć€ćć‚Œć“ććŒč¦ē‚¹ć§ć™ć€‚ć•ć‚‰ć«äø‹ć®å±¤ćÆć‚Æćƒ©ć‚¹ć§ć™ć‚‰ć‚ć‚Šć¾ć›ć‚“ć€‚**[ćƒŸćƒ‰ćƒ«ć‚¦ć‚§ć‚¢](middleware.md)** 恧恙怂 +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒäø”ę–¹ć®ć‚µćƒ¼ćƒćƒ¼ć‚’åŒć˜ć‚ˆć†ć«ę‰±ć£ćŸć®ćÆć€äø”č€…ćŒć¾ć•ć«åŒć˜ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć ć‹ć‚‰ć§ć‚ć‚Šć€ćć‚Œć“ććŒč¦ē‚¹ć§ć™ć€‚ć•ć‚‰ć«äø‹ć®å±¤ćÆć‚Æćƒ©ć‚¹ć§ć™ć‚‰ć‚ć‚Šć¾ć›ć‚“ć€‚**[ćƒŸćƒ‰ćƒ«ć‚¦ć‚§ć‚¢](middleware.md)** 恧恙怂 diff --git a/i18n/ja/pages/advanced/pagination.md b/i18n/ja/pages/advanced/pagination.md index fb8c4890..6a2a0033 100644 --- a/i18n/ja/pages/advanced/pagination.md +++ b/i18n/ja/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # ćƒšćƒ¼ć‚øćƒćƒ¼ć‚·ćƒ§ćƒ³ {#pagination} @@ -29,9 +29,13 @@ translation: ### 試してみる {#try-it} -`Client(server)` は、`MCPServer` ć«ęŽ„ē¶šć™ć‚‹ć®ćØć¾ć£ćŸćåŒć˜ć‚ˆć†ć«ć€ä½Žćƒ¬ćƒ™ćƒ«ć® `Server` ć«ćƒ”ćƒ¢ćƒŖå†…ć§ęŽ„ē¶šć—ć¾ć™ć€‚ +`mcp run` は `MCPServer` ć—ć‹å—ć‘ä»˜ć‘ćŖć„ć®ć§ć€ć“ć®ć‚µćƒ¼ćƒćƒ¼ćÆč‡Ŗåˆ†ć§é…äæ”ć—ć¾ć™ć€‚`server.py` ć®ęœ€å¾Œć®č”Œć§ `Server` 恋悉ꙮ通恮 ASGI ć‚¢ćƒ—ćƒŖć‚’ēµ„ćæē«‹ć¦ć¦ćŠć‚Šć€ćć‚Œć‚’ uvicorn ćŒå®Ÿč”Œć—ć¾ć™ć€‚ -引数なしで `list_resources()` ć‚’å‘¼ć³å‡ŗć—ć¦ćć ć•ć„ć€‚`book-1` 恋悉 `book-10` までの 10 å€‹ć®ćƒŖć‚½ćƒ¼ć‚¹ćŒčæ”ć‚Šć€`next_cursor` ćÆę–‡å­—åˆ— `"10"` 恧恙怂 +```console +uvicorn server:app --port 8000 +``` + +ä»»ę„ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆļ¼ˆ**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/index.md)**ć€ć¾ćŸćÆ Inspector)を `http://localhost:8000/mcp` に向け、引数なしで `list_resources()` ć‚’å‘¼ć³å‡ŗć—ć¦ćć ć•ć„ć€‚`book-1` 恋悉 `book-10` までの 10 å€‹ć®ćƒŖć‚½ćƒ¼ć‚¹ćŒčæ”ć‚Šć€`next_cursor` ćÆę–‡å­—åˆ— `"10"` 恧恙怂 恝悌悒 `list_resources(cursor="10")` ćØć—ć¦čæ”ć™ćØć€ęœ€åˆć®ćƒŖć‚½ćƒ¼ć‚¹ćÆ `book-11` ć«ćŖć‚Šć€ę–°ć—ć„ `next_cursor` は `"20"` 恧恙怂 @@ -41,7 +45,7 @@ translation: `Client` のすべての `list_*` ćƒ”ć‚½ćƒƒćƒ‰ļ¼ˆ`list_tools`态`list_resources`态`list_resource_templates`态`list_prompts`)は `cursor=` ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ćƒšćƒ¼ć‚øćƒ³ć‚°ć•ć‚ŒćŸäø€č¦§ć‚’ć™ć¹ć¦å–ć‚Šå‡ŗć™ć«ćÆć€`while True` 悒 1 ć¤ę›øćć ć‘ć§ć™ć€‚ -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * `next_cursor` 悒見悋**å‰ć«** extend ć—ć¦ćć ć•ć„ć€‚ęœ€å¾Œć®ćƒšćƒ¼ć‚øć«ć‚‚ćƒŖć‚½ćƒ¼ć‚¹ćÆć‚ć‚Šć¾ć™ć€‚ * `next_cursor is None` ćŒå‡ŗå£ć§ć™ć€‚ćć‚Œä»„å¤–ćÆćć®ć¾ć¾ć€ę‰‹ć‚’åŠ ćˆćšć« `cursor=` ć«ęˆ»ć—ć¾ć™ć€‚ -ćć® `main()` ć‚’å®Ÿč”Œć™ć‚‹ćØ `100 resources` ćØč”Øē¤ŗć•ć‚Œć¾ć™ć€‚10 件ずつの 10 ćƒšćƒ¼ć‚øćŒć€10 ćƒšćƒ¼ć‚øć‚ć‚‹ć“ćØćŖć©ēŸ„ć‚‰ćŖć„ćƒ«ćƒ¼ćƒ—ć«ć‚ˆć£ć¦ć¤ćŖćŽåˆć‚ć•ć‚ŒćŸēµęžœć§ć™ć€‚ +uvicorn が `server.py` ć‚’é…äæ”ć—ćŸć¾ć¾ć®ēŠ¶ę…‹ć§ć€2 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć§ `python client.py` ć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚`100 resources` ćØč”Øē¤ŗć•ć‚Œć¾ć™ć€‚10 件ずつの 10 ćƒšćƒ¼ć‚øćŒć€10 ćƒšćƒ¼ć‚øć‚ć‚‹ć“ćØćŖć©ēŸ„ć‚‰ćŖć„ćƒ«ćƒ¼ćƒ—ć«ć‚ˆć£ć¦ć¤ćŖćŽåˆć‚ć•ć‚ŒćŸēµęžœć§ć™ć€‚ ć“ć‚ŒćÆ **[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/index.md)** ćŒć™ć¹ć¦ć® `list_*` ćƒ”ć‚½ćƒƒćƒ‰ć«ć¤ć„ć¦ē¤ŗć—ć¦ć„ć‚‹ć®ćØåŒć˜ćƒ«ćƒ¼ćƒ—ć§ć€ćƒšćƒ¼ć‚øćƒ³ć‚°ć—ćŖć„ć‚µćƒ¼ćƒćƒ¼ć«åÆ¾ć—ć¦ć‚‚ć‚³ć‚¹ćƒˆćÆć‹ć‹ć‚Šć¾ć›ć‚“ć€‚ęœ€åˆć®ćƒ¬ć‚¹ćƒćƒ³ć‚¹ć§ `next_cursor` が `None` ć«ćŖć‚Šć€ćƒ«ćƒ¼ćƒ—ćÆ 1 å›žć ć‘å›žć‚Šć¾ć™ć€‚ diff --git a/i18n/ja/pages/client/caching.md b/i18n/ja/pages/client/caching.md index 79ffd119..dc9914a6 100644 --- a/i18n/ja/pages/client/caching.md +++ b/i18n/ja/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # ć‚­ćƒ£ćƒƒć‚·ćƒ„ćƒ’ćƒ³ćƒˆ {#caching-hints} @@ -27,7 +27,7 @@ translation: ä½Žćƒ¬ćƒ™ćƒ«ć® `Server` ć§ćÆć€ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ćŒēµęžœć‚’ę‰‹ä½œę„­ć§ēµ„ćæē«‹ć¦ć¾ć™ć€‚`ttl_ms` と `cache_scope` ćÆēµęžœćƒ¢ćƒ‡ćƒ«ć®å˜ćŖć‚‹ćƒ•ć‚£ćƒ¼ćƒ«ćƒ‰ć§ć™ć€‚ć“ć‚Œć‚‰ć‚’ę˜Žē¤ŗēš„ć«čØ­å®šć—ćŸćƒćƒ³ćƒ‰ćƒ©ćƒ¼ćÆć€ćƒ•ć‚£ćƒ¼ćƒ«ćƒ‰å˜ä½ć§åøøć«ć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æćƒ¼ć®ćƒžćƒƒćƒ—ć‚ˆć‚Šå„Ŗå…ˆć•ć‚Œć¾ć™ć€‚ -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -41,10 +41,24 @@ translation: 2026-07-28 ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć§ćÆć€`Client` ćŒćƒ’ćƒ³ćƒˆć«č‡Ŗå‹•ć§å¾“ć„ć¾ć™ć€‚ēµ„ćæč¾¼ćæć®ćƒ¬ć‚¹ćƒćƒ³ć‚¹ć‚­ćƒ£ćƒƒć‚·ćƒ„ćŒć‚ć‚Šć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ęœ‰åŠ¹ć§ć™ć€‚`ttlMs` ć‚’ęŒć£ć¦å±Šć„ćŸēµęžœćÆäæå­˜ć•ć‚Œć€ćć® TTL å†…ć«åŒäø€ć®å‘¼ć³å‡ŗć—ćŒć‚ć‚Œć°ć€ćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ćŖć—ć§ć‚­ćƒ£ćƒƒć‚·ćƒ„ć‹ć‚‰čæ”ć•ć‚Œć¾ć™ć€‚ćƒ’ćƒ³ćƒˆć‚’ć€ŒęŒćŸćŖć„ć€ēµęžœćÆć‚­ćƒ£ćƒƒć‚·ćƒ„ć•ć‚Œć¾ć›ć‚“ć€‚ćƒ’ćƒ³ćƒˆć®ćŖć„ēµęžœć«ćÆ `CacheConfig.default_ttl_ms` ćŒé©ē”Øć•ć‚Œć€ćć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆ `0`ļ¼ˆć™ćć«å¤ććŖć‚‹ļ¼‰ć§ć™ć€‚ćć®ćŸć‚ć€ä½•ć‚‚å®£čØ€ć—ćŖć„ć‚µćƒ¼ćƒćƒ¼ć«ćÆć€ć“ć‚Œć¾ć§ćØć¾ć£ćŸćåŒć˜å‘¼ć³å‡ŗć—ć”ćØć®ćƒˆćƒ©ćƒ•ć‚£ćƒƒć‚ÆćŒå±Šćć¾ć™ć€‚ -```python title="client.py" hl_lines="33 35 38" +ć“ć‚ŒćŒčµ·ć“ć‚‹ę§˜å­ć‚’č¦‹ć‚‹ć«ćÆć€å‰ć®ć‚»ć‚Æć‚·ćƒ§ćƒ³ć® `server.py` 悒 uvicorn ć§čµ·å‹•ć—ć¦ćć ć•ć„ļ¼ˆęœ€å¾Œć®č”ŒćŒ ASGI ć‚¢ćƒ—ćƒŖć‚’ēµ„ćæē«‹ć¦ć¦ć„ć¾ć™ļ¼‰ć€‚ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ćÆå®Ÿéš›ć«å®Ÿč”Œć•ć‚Œć‚‹ćŸć³ć« 1 č”Œå‡ŗåŠ›ć—ć¾ć™ć€‚ + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +2 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ `python client.py` ć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ęœ€åˆć®ēµęžœćŒęŒć£ć¦ć„ćŸćƒ’ćƒ³ćƒˆć€ć¤ć¾ć‚Šćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć® `ttlMs` ćØćƒžćƒƒćƒ—ć® `cacheScope` ćŒäø¦ć‚“ć§å‡ŗåŠ›ć•ć‚Œć¾ć™ć€‚ + +```text +1000 public +``` + +ę®‹ć‚ŠćÆć‚µćƒ¼ćƒćƒ¼å“ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‚’č¦‹ć‚Œć°ć‚ć‹ć‚Šć¾ć™ć€‚uvicorn ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒ­ć‚°ć®åˆé–“ć«ć€`tools/list served` が 3 å›žē¾ć‚Œć¾ć™ć€‚ + 呼び出し 4 å›žć€å–å¾— 3 å›žć§ć™ć€‚2 å›žē›®ć®å‘¼ć³å‡ŗć—ćÆę–°é®®ćŖć‚Øćƒ³ćƒˆćƒŖćƒ¼ć‚’č¦‹ć¤ć‘ć€ć‚µćƒ¼ćƒćƒ¼ć«åˆ°é”ć—ć¾ć›ć‚“ć§ć—ćŸć€‚ļ¼ˆę³Øå…„ć—ćŸļ¼‰ć‚Æćƒ­ćƒƒć‚Æć‚’ TTL ć®å…ˆćøé€²ć‚ćŸć“ćØć§ć€3 å›žē›®ćÆå†ć³å–å¾—ć—ć¾ć—ćŸć€‚4 å›žē›®ćÆ `cache_mode="refresh"` ć‚’ęŒ‡å®šć—ć¦ć„ć¾ć™ć€‚ć“ć®ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰å¼•ę•°ćÆć‚­ćƒ£ćƒƒć‚·ćƒ„åÆ¾č±”ć® 5 ć¤ć®ćƒ”ć‚½ćƒƒćƒ‰ļ¼ˆ`list_tools`态`list_prompts`态`list_resources`态`list_resource_templates`态`read_resource`ļ¼‰ć«ć‚ć‚Šć¾ć™ć€‚ * `"use"`ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆļ¼‰ćÆć€ę–°é®®ćŖć‚Øćƒ³ćƒˆćƒŖćƒ¼ćŒć‚ć‚Œć°ćć‚Œć‚’čæ”ć—ć€ćŖć‘ć‚Œć°å–å¾—ć—ć¦äæå­˜ć—ć¾ć™ć€‚ @@ -53,7 +67,7 @@ translation: `"use"` ć‚ˆć‚ŠäøŠä½ć«ćƒ«ćƒ¼ćƒ«ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚**`meta` ć‚’ęŒć¤å‘¼ć³å‡ŗć—ćÆåæ…ćšć‚µćƒ¼ćƒćƒ¼ć«åˆ°é”ć—ć¾ć™ć€‚**`meta` ć‚’čØ­å®šć—ćŸćƒŖć‚Æć‚Øć‚¹ćƒˆļ¼ˆé€²ę—ćƒˆćƒ¼ć‚Æćƒ³ć‚„ćƒˆćƒ¬ćƒ¼ć‚·ćƒ³ć‚°ē”Øćƒ•ć‚£ćƒ¼ćƒ«ćƒ‰ćŖć©ļ¼‰ćÆå®Ÿéš›ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆé€äæ”ć‚’å‰ęć«ć—ć¦ć„ć‚‹ćŸć‚ć€`cache_mode="use"` では `"refresh"` ćØć—ć¦ę‰±ć‚ć‚Œć¾ć™ć€‚ć‚­ćƒ£ćƒƒć‚·ćƒ„ć®čŖ­ćæč¾¼ćæćÆēœē•„ć•ć‚Œć€å–å¾—ć—ćŸēµęžœćÆå¼•ćē¶šćć‚­ćƒ£ćƒƒć‚·ćƒ„ć®ć‚Øćƒ³ćƒˆćƒŖćƒ¼ć‚’ē½®ćę›ćˆć¾ć™ć€‚`"bypass"` ćØę˜Žē¤ŗēš„ćŖ `"refresh"` ćÆć„ć¤ć‚‚ć©ćŠć‚Šć«å‹•ä½œć—ć¾ć™ć€‚ -ć‚­ćƒ£ćƒƒć‚·ćƒ„ć‚’å®Œå…Øć«ē„”åŠ¹ć«ć™ć‚‹ć«ćÆć€`Client(server, cache=None)` ć§ę§‹ēÆ‰ć—ć¦ćć ć•ć„ć€‚ć™ć¹ć¦ć®å‘¼ć³å‡ŗć—ćŒå†ć³ćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ć«ćŖć‚Šć€`cache_mode` ćÆå—ć‘ä»˜ć‘ć‚‰ć‚Œć‚‹ć‚‚ć®ć®ä½•ć‚‚ć—ć¾ć›ć‚“ć€‚ +ć‚­ćƒ£ćƒƒć‚·ćƒ„ć‚’å®Œå…Øć«ē„”åŠ¹ć«ć™ć‚‹ć«ćÆć€`Client` の構築時に `cache=None` ć‚’ęø”ć—ć¦ćć ć•ć„ć€‚ć™ć¹ć¦ć®å‘¼ć³å‡ŗć—ćŒå†ć³ćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ć«ćŖć‚Šć€`cache_mode` ćÆå—ć‘ä»˜ć‘ć‚‰ć‚Œć‚‹ć‚‚ć®ć®ä½•ć‚‚ć—ć¾ć›ć‚“ć€‚ ć‚¹ć‚³ćƒ¼ćƒ—ć‚‚č‡Ŗå‹•ēš„ć«å°Šé‡ć•ć‚Œć¾ć™ć€‚`"private"` ć®ć‚Øćƒ³ćƒˆćƒŖćƒ¼ćÆć‚­ćƒ£ćƒƒć‚·ćƒ„ć®ć€Œćƒ‘ćƒ¼ćƒ†ć‚£ć‚·ćƒ§ćƒ³ć€ļ¼ˆå¾Œčæ°ļ¼‰ć‚’ć‚­ćƒ¼ć«ć—ć€`"public"` ć®ć‚Øćƒ³ćƒˆćƒŖćƒ¼ćÆć‚ˆć‚Šåŗƒć„å…±ęœ‰ć‚’éøć¹ć¾ć™ć€‚ćć—ć¦ć€åęŒ‡ć—ć•ć‚ŒćŸć‚Øćƒ³ćƒˆćƒŖćƒ¼ć«ć¤ć„ć¦ćÆ**é€šēŸ„ćŒ TTL ć«å‹ć”ć¾ć™**怂`list_changed` é€šēŸ„ćÆåÆ¾åæœć™ć‚‹ć‚­ćƒ£ćƒƒć‚·ćƒ„ęøˆćæć®äø€č¦§ć‚’ē “ę£„ć—ć€`resources/updated` ćÆćć® URI ćØå®Œå…Øć«äø€č‡“ć™ć‚‹ć‚­ćƒ¼ć§äæå­˜ć•ć‚ŒćŸć‚­ćƒ£ćƒƒć‚·ćƒ„ęøˆćæć®čŖ­ćæč¾¼ćæēµęžœć‚’ć€ć©ć‚Œć ć‘ę–°é®®ć§ć‚‚ē “ę£„ć—ć¾ć™ć€‚2026-07-28 ć®ęŽ„ē¶šć§ćÆć€ć“ć‚Œć‚‰ć®é€šēŸ„ćÆ `client.listen(...)` 恧開恏 `subscriptions/listen` ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć«å±Šćć€ē “ę£„ćÆć‚¦ć‚©ćƒƒćƒćƒ£ćƒ¼ćŒć‚¤ćƒ™ćƒ³ćƒˆć‚’č¦‹ć‚‹å‰ć«å®Œäŗ†ć—ć¾ć™ć€‚č©³ć—ććÆ **[ć‚µćƒ–ć‚¹ć‚ÆćƒŖćƒ—ć‚·ćƒ§ćƒ³](subscriptions.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ diff --git a/i18n/ja/pages/client/callbacks.md b/i18n/ja/pages/client/callbacks.md index bcf4c266..2ad71759 100644 --- a/i18n/ja/pages/client/callbacks.md +++ b/i18n/ja/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æ {#client-callbacks} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') 恓恔悉恋悉 `tools/call` が 1 å›žć€ć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ć®ęŠ˜ć‚Ščæ”ć—ć® `elicitation/create` が 1 å›žć€ćć‚Œć«ē­”ćˆć‚‹ć®ćŒć“ć”ć‚‰ć®é–¢ę•°ć§ć™ć€‚ć™ć¹ć¦ćŒ 1 å›žć®ćƒ„ćƒ¼ćƒ«å‘¼ć³å‡ŗć—ć®äø­ć§å®Œēµć—ć¾ć™ć€‚ !!! info - `Client(...)` の呼び出しにある `mode="legacy"` ćÆć€å®Ÿéš›ć«åƒć„ć¦ć„ć¾ć™ć€‚ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆ `Client(...)` ćÆę–°ć—ć„ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®ēµŒč·Æć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć—ć¾ć™ćŒć€ćć®ēµŒč·Æć«ćÆć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć®ćŸć‚ć®ćƒćƒƒć‚Æćƒćƒ£ćƒćƒ«ļ¼ˆback-channelļ¼‰ćŒć‚ć‚Šć¾ć›ć‚“ć€‚ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒå‹•ćå‰ć« `ctx.elicit` ćŒå¤±ę•—ć—ć¾ć™ć€‚ć“ć‚Œć‚’ę±ŗć‚ć‚‹ć®ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ćÆćŖćć€ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć•ć‚ŒćŸćƒ—ćƒ­ćƒˆć‚³ćƒ«ć§ć™ć€‚ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ć‚‚ URL č¶Šć—ć§ć‚‚åŒć˜ć§ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒć“ć†ć—ćŸćƒŖć‚Æć‚Øć‚¹ćƒˆć«ē­”ćˆć‚‹åæ…č¦ćŒć‚ć‚‹ćØććÆć€åæ…ćš `mode="legacy"` ć‚’ęŒ‡å®šć—ć¦ćć ć•ć„ć€‚ć“ć®ćƒšćƒ¼ć‚øć®č£ć«ć‚ć‚‹ćƒ†ć‚¹ćƒˆćÆć™ć¹ć¦ćć†ć—ć¦ć„ć¾ć™ć€‚č©³ć—ććÆ **[ćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³](../protocol-versions.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ + `Client(...)` の呼び出しにある `mode="legacy"` ćÆć€å®Ÿéš›ć«åƒć„ć¦ć„ć¾ć™ć€‚ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆ `Client(...)` ćÆę–°ć—ć„ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®ēµŒč·Æć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć—ć¾ć™ćŒć€ćć®ēµŒč·Æć«ćÆć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć®ćŸć‚ć®ćƒćƒƒć‚Æćƒćƒ£ćƒćƒ«ļ¼ˆback-channelļ¼‰ćŒć‚ć‚Šć¾ć›ć‚“ć€‚ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒå‹•ćå‰ć« `ctx.elicit` ćŒå¤±ę•—ć—ć¾ć™ć€‚ć“ć‚Œć‚’ę±ŗć‚ć‚‹ć®ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ćÆćŖćć€ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć•ć‚ŒćŸćƒ—ćƒ­ćƒˆć‚³ćƒ«ć§ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒć“ć†ć—ćŸćƒŖć‚Æć‚Øć‚¹ćƒˆć«ē­”ćˆć‚‹åæ…č¦ćŒć‚ć‚‹ćØććÆć€åæ…ćš `mode="legacy"` ć‚’ęŒ‡å®šć—ć¦ćć ć•ć„ć€‚ć“ć®ćƒšćƒ¼ć‚øć®č£ć«ć‚ć‚‹ćƒ†ć‚¹ćƒˆćÆć™ć¹ć¦ćć†ć—ć¦ć„ć¾ć™ć€‚č©³ć—ććÆ **[ćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³](../protocol-versions.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ 2026-07-28 ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć§ć‚‚ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒä½æć‚ć‚ŒćŖććŖć‚‹ć‚ć‘ć§ćÆćŖćć€å‘¼ć°ć‚Œę–¹ćŒå¤‰ć‚ć‚Šć¾ć™ć€‚ćƒ„ćƒ¼ćƒ«ćŒ `ElicitRequest` を含む `InputRequiredResult` を返すと、`Client` ćÆćć®ć‚Øćƒ³ćƒˆćƒŖć‚’åŒć˜ `elicitation_callback` ć«ęŒÆć‚Šåˆ†ć‘ć€å‘¼ć³å‡ŗć—ć‚’å†č©¦č”Œć—ć¦ćć‚Œć¾ć™ć€‚ć“ć®ęµć‚ŒćÆ **[ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ļ¼ˆmulti-round-tripļ¼‰ćƒŖć‚Æć‚Øć‚¹ćƒˆ](../handlers/multi-round-trip.md)** ć§čŖ¬ę˜Žć—ć¦ć„ć¾ć™ć€‚ @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` と `list_roots_callback` ć‚‚åŒć˜ć‚ˆć†ć«å‹•ćć¾ć™ćŒć€éžęŽØå„Øć®ę©Ÿčƒ½ć®ćŸć‚ć®ć‚‚ć®ć§ć™ć€‚ę–°ć—ć„ć‚µćƒ¼ćƒćƒ¼ćÆä»£ć‚ć‚Šć«ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ä½æć„ć¾ć™ć€‚ * `logging_callback` と `message_handler` ćÆé€šēŸ„ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ä½•ć‚‚å®£čØ€ć—ć¾ć›ć‚“ć€‚ -`Client(...)` の第 1 å¼•ę•°ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć§ć™ć€‚ć™ć¹ć¦ć®ēØ®é”žćÆ **[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ](transports.md)** で扱っています。 +`Client(...)` の第 1 å¼•ę•°ć§ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćŒę±ŗć¾ć‚Šć¾ć™ć€‚ć™ć¹ć¦ć®ēØ®é”žćÆ **[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ](transports.md)** で扱っています。 diff --git a/i18n/ja/pages/client/index.md b/i18n/ja/pages/client/index.md index 7ca42416..7e53cc20 100644 --- a/i18n/ja/pages/client/index.md +++ b/i18n/ja/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## ęœ€åˆć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ {#your-first-client} -```python title="client.py" hl_lines="14-18" +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćÆć€åÆ¾č©±ć™ć‚‹ē›øę‰‹ć®ć‚µćƒ¼ćƒćƒ¼ćŒåæ…č¦ć§ć™ć€‚ć“ć®ćƒšćƒ¼ć‚øć®ć™ć¹ć¦ć®ć‚¹ćƒ‹ćƒšćƒƒćƒˆćŒęŽ„ē¶šć™ć‚‹ć®ćŒć€ć“ć® Bookshop 恧恙怂`server.py` ćØć—ć¦äæå­˜ć—ć€HTTP ć§čµ·å‹•ć—ćŸć¾ć¾ć«ć—ć¦ćć ć•ć„ć€‚ + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -å†’é ­ć®ć‚µćƒ¼ćƒćƒ¼ćÆć€ęŽ„ē¶šå…ˆć‚’ē”Øę„ć™ć‚‹ćŸć‚ć ć‘ć«ć‚ć‚Šć¾ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆćƒć‚¤ćƒ©ć‚¤ćƒˆć•ć‚ŒćŸ 5 č”Œć§ć™ć€‚ +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` には**ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®**ć‚’ęø”ć—ć¦ć„ć¾ć™ć€‚ć“ć‚ŒćŒć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć™ć€‚ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ćƒćƒ¼ćƒˆć‚‚ HTTP ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć®ćƒšćƒ¼ć‚øć®ć™ć¹ć¦ć®ä¾‹ć€ćć—ć¦ä½œęˆć™ć‚‹ć™ć¹ć¦ć®ćƒ†ć‚¹ćƒˆćŒć€ć“ć®ę–¹ę³•ć§ęŽ„ē¶šć—ć¾ć™ć€‚ +ć“ć‚Œć§ć‚µćƒ¼ćƒćƒ¼ćÆ `http://localhost:8000/mcp` ć§ęä¾›ć•ć‚Œć¾ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆåˆ„ć®ćƒ—ćƒ­ć‚°ćƒ©ćƒ ć§ć™ć€‚`client.py` ćØć—ć¦äæå­˜ć—ć€2 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć§ `python client.py` ć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` には **URL** ć‚’ęø”ć—ć¦ć„ć‚‹ć®ć§ć€ć„ć¾čµ·å‹•ć—ćŸć‚µćƒ¼ćƒćƒ¼ć« Streamable HTTP ć§ęŽ„ē¶šć—ć¾ć™ć€‚ * `async with` が**ćƒ©ć‚¤ćƒ•ć‚µć‚¤ć‚Æćƒ«**ć§ć™ć€‚å…„ć‚‹ćØęŽ„ē¶šć—ć¦ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć‚’č”Œć„ć€å‡ŗć‚‹ćØåˆ‡ę–­ć—ć¾ć™ć€‚`connect()` / `close()` ć®ćƒšć‚¢ćÆćŖćć€ćƒ–ćƒ­ćƒƒć‚ÆćŒēµ‚ć‚ć£ćŸå¾Œć® `Client` ćÆå†åˆ©ē”Øć§ćć¾ć›ć‚“ć€‚ * ćƒ–ćƒ­ćƒƒć‚Æć®äø­ć§ćÆć€ęŽ„ē¶šć«é–¢ć™ć‚‹ęƒ…å ±ćŒć™ć§ć«é€šåøøć®ćƒ—ćƒ­ćƒ‘ćƒ†ć‚£ćØć—ć¦ęƒć£ć¦ć„ć¾ć™ć€‚ @@ -25,10 +35,10 @@ translation: `Client` ćÆä½ē½®å¼•ę•°ć‚’ 1 ć¤å–ć‚Šć€ćć®åž‹ć‹ć‚‰ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć‚’ę±ŗå®šć—ć¾ć™ć€‚ -* `MCPServer`ļ¼ˆć¾ćŸćÆä½Žćƒ¬ćƒ™ćƒ«ć® `Server`ļ¼‰ć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ļ¼š**ćƒ—ćƒ­ć‚»ć‚¹å†…**ć§ęŽ„ē¶šć—ć¾ć™ć€‚ -* URL ę–‡å­—åˆ—ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPć€‚ęœ¬ē•Ŗå‘ć‘ć®ēµŒč·Æć§ć™ć€‚ -* `StdioServerParameters`:**ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹**ćØć—ć¦čµ·å‹•ć™ć‚‹ć‚³ćƒžćƒ³ćƒ‰ć§ć€ćć® stdin と stdout ć‚’é€šć˜ć¦åÆ¾č©±ć—ć¾ć™ć€‚ +* URL ę–‡å­—åˆ—ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ć§ä½æć†ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć™ć€‚ +* `StdioServerParameters`ļ¼šćƒ­ćƒ¼ć‚«ćƒ«ć®**ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹**ćØć—ć¦čµ·å‹•ć™ć‚‹ć‚³ćƒžćƒ³ćƒ‰ć§ć€ćć® stdin と stdout ć‚’é€šć˜ć¦åÆ¾č©±ć—ć¾ć™ć€‚ * **ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ**:`async with ... as (read, write)` ć§ćć‚‹ć‚‚ć®ćŖć‚‰ä½•ć§ć‚‚ć€‚ćŸćØćˆć°ć€č‡Ŗåˆ†ć® HTTP ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ćƒ©ćƒƒćƒ—ć™ć‚‹ `streamable_http_client(url, http_client=...)` 恧恙怂 +* `MCPServer`ļ¼ˆć¾ćŸćÆä½Žćƒ¬ćƒ™ćƒ«ć® `Server`ļ¼‰ć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ļ¼š**ćƒ—ćƒ­ć‚»ć‚¹å†…**ć§ęŽ„ē¶šć—ć¾ć™ć€‚ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ćƒćƒ¼ćƒˆć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć‚ŒćÆćƒ†ć‚¹ćƒˆå‘ć‘ć§ć€**[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ćŒć“ć‚Œć‚’åœŸå°ć«ć—ć¦ć„ć¾ć™ć€‚ ć“ć®ćƒšćƒ¼ć‚øć®ę®‹ć‚Šć®å†…å®¹ćÆć€4 ć¤ć®ć©ć‚Œć§ć‚‚åŒć˜ć§ć™ć€‚ćƒ˜ćƒƒćƒ€ćƒ¼ć€ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć€ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆć€ćć—ć¦ `Transport` ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć«ć¤ć„ć¦ćÆć€å°‚ē”Øć®ćƒšćƒ¼ć‚ø **[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ](transports.md)** ćŒć‚ć‚Šć¾ć™ć€‚ @@ -48,11 +58,11 @@ translation: ## ćƒ„ćƒ¼ćƒ«ć®äø€č¦§å–å¾— {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` は `ListToolsResult` ć‚’čæ”ć—ć€ćƒ„ćƒ¼ćƒ«ćÆ `.tools` ć«å…„ć£ć¦ć„ć¾ć™ć€‚ćć‚Œćžć‚ŒćŒć€ćƒ›ć‚¹ćƒˆćŒćƒ¢ćƒ‡ćƒ«ć«ęø”ć™å®Œå…ØćŖå®šē¾©ć§ć™ć€‚ +`list_tools()` は `ListToolsResult` ć‚’čæ”ć—ć€ćƒ„ćƒ¼ćƒ«ćÆ `.tools` ć«å…„ć£ć¦ć„ć¾ć™ć€‚ćć‚Œćžć‚ŒćŒć€ćƒ›ć‚¹ćƒˆćŒćƒ¢ćƒ‡ćƒ«ć«ęø”ć™å®Œå…ØćŖå®šē¾©ć§ć™ć€‚ęœ€åˆć®ćƒ„ćƒ¼ćƒ«ćÆę¬”ć®ćØćŠć‚Šć§ć™ć€‚ ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' ć“ć®ć‚¹ć‚­ćƒ¼ćƒžć«ćÆć€UI ćŒå¼•ę•°ćƒ•ć‚©ćƒ¼ćƒ ć‚’ęē”»ć™ć‚‹ć®ć«åæ…č¦ćŖć‚‚ć®ć‚‚ć€ćƒ¢ćƒ‡ćƒ«ćŒęœ‰åŠ¹ćŖå¼•ę•°ć‚’ē”Ÿęˆć™ć‚‹ć®ć«åæ…č¦ćŖć‚‚ć®ć‚‚ć€ć™ć¹ć¦å«ć¾ć‚Œć¦ć„ć¾ć™ć€‚ +2 ć¤ē›®ć®ćƒ„ćƒ¼ćƒ« `lookup_book` は `title=` ćŖć—ć§ē™»éŒ²ć•ć‚Œć¦ć„ć‚‹ć®ć§ć€ćć® `tool.title` は `None` 恧恙怂 + !!! tip `title` ćÆēœē•„åÆčƒ½ćŖć®ć§ć€äŗŗé–“ć«ćƒ„ćƒ¼ćƒ«ć‚’č¦‹ć›ć‚‹ UI ćÆć©ć”ć‚‰ć‹ć‚’éøć¶åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚`title` ćŒć‚ć‚Œć°ćć‚Œć‚’ć€ćŖć‘ć‚Œć° `name` を使います。`from mcp.shared.metadata_utils import get_display_name` ćŒć¾ć•ć«ćć‚Œć‚’č”Œć„ć€ćƒ„ćƒ¼ćƒ«ć€ćƒŖć‚½ćƒ¼ć‚¹ć€ćƒŖć‚½ćƒ¼ć‚¹ćƒ†ćƒ³ćƒ—ćƒ¬ćƒ¼ćƒˆć€ćƒ—ćƒ­ćƒ³ćƒ—ćƒˆć«åÆ¾åæœć—ć¦ć„ć¾ć™ć€‚ @@ -83,7 +95,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` ćÆćƒ„ćƒ¼ćƒ«ć‚’å®Ÿč”Œć—ć€`CallToolResult` を返します。 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False ćƒŖć‚½ćƒ¼ć‚¹ć®å‹•č©žćÆēµ„ć«ćŖć£ć¦ć„ć¾ć™ć€‚äø€č¦§å–å¾—ćŒ 2 é€šć‚Šć€čŖ­ćæå–ć‚ŠćŒ 1 通悊恧恙怂 -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -141,11 +153,11 @@ result.is_error # False `read_resource` は `contents` ć‚’čæ”ć—ć¾ć™ć€‚ć“ć‚ŒćÆ `TextResourceContents` または `BlobResourceContents` ć®ćƒŖć‚¹ćƒˆć§ć™ć€‚č€ƒćˆę–¹ćÆćƒ„ćƒ¼ćƒ«ć®ć‚³ćƒ³ćƒ†ćƒ³ćƒ„ćØåŒć˜ć§ć€`isinstance` ć§ēµžć‚Šč¾¼ć‚“ć§ć‹ć‚‰ `.text`(または `.blob`)を読みます。 -ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€ćƒŖć‚½ćƒ¼ć‚¹ćŒå¤‰ę›“ć•ć‚ŒćŸćØćć«é€šēŸ„ć‚’å—ć‘ć‚‹ć“ćØć‚‚ć§ćć¾ć™ć€‚2025 å¹“äø–ä»£ć®ęŽ„ē¶šć§ćÆ `subscribe_resource(uri)` / `unsubscribe_resource(uri)` ćŒćć‚Œć«ć‚ćŸć‚Šć¾ć™ć€‚ćŸć ć—ć“ć®ćƒ”ć‚½ćƒƒćƒ‰ć®ćƒšć‚¢ćÆ `MCPServer` ćŒå®Ÿč£…ć—ć¦ć„ćŖć„ćŸć‚ć€2026-07-28 ć®é€šäæ”äøŠļ¼ˆć“ć‚Œć‚‰ć®å‹•č©žćÆć‚‚ć†å­˜åœØć—ć¾ć›ć‚“ļ¼‰ć§ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć« `-32601`态*Method not found* ćŒčæ”ć‚Šć¾ć™ć€‚2026 幓の代替は `subscriptions/listen` ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć§ć€ć“ć”ć‚‰ćÆ `MCPServer` ćŒå®Ÿéš›ć«ęä¾›ć—ć¦ć„ć¾ć™ļ¼ˆćć“ć§ćÆ `server_capabilities.resources.subscribe` が `True` ć§ć™ļ¼‰ć€‚ć“ć‚Œć‚’ `client.listen(...)` ć§ę¶ˆč²»ć™ć‚‹ę–¹ę³•ćÆć€ć“ć®ć‚»ć‚Æć‚·ćƒ§ćƒ³ć® **[ć‚µćƒ–ć‚¹ć‚ÆćƒŖćƒ—ć‚·ćƒ§ćƒ³](subscriptions.md)** ć®ćƒšćƒ¼ć‚øć§čŖ¬ę˜Žć—ć¦ć„ć¾ć™ć€‚ +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€ćƒŖć‚½ćƒ¼ć‚¹ćŒå¤‰ę›“ć•ć‚ŒćŸćØćć«é€šēŸ„ć‚’å—ć‘ć‚‹ć“ćØć‚‚ć§ćć¾ć™ć€‚2025 å¹“äø–ä»£ć®ęŽ„ē¶šć§ćÆ `subscribe_resource(uri)` / `unsubscribe_resource(uri)` ćŒćć‚Œć«ć‚ćŸć‚Šć¾ć™ć€‚ć“ć®ćƒ”ć‚½ćƒƒćƒ‰ć®ćƒšć‚¢ćÆ `MCPServer` ćŒå®Ÿč£…ć—ć¦ć„ćŖć„ćŸć‚ć€2026-07-28 ć®é€šäæ”äøŠļ¼ˆć“ć‚Œć‚‰ć®å‹•č©žćÆć‚‚ć†å­˜åœØć—ć¾ć›ć‚“ļ¼‰ć§ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć« `-32601`态*Method not found* ćŒčæ”ć‚Šć¾ć™ć€‚2026 幓の代替は `subscriptions/listen` ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć§ć€ć“ć”ć‚‰ćÆ `MCPServer` ćŒå®Ÿéš›ć«ęä¾›ć—ć¦ć„ć¾ć™ļ¼ˆćć“ć§ćÆ `server_capabilities.resources.subscribe` が `True` ć§ć™ļ¼‰ć€‚ć“ć‚Œć‚’ `client.listen(...)` ć§ę¶ˆč²»ć™ć‚‹ę–¹ę³•ćÆć€ć“ć®ć‚»ć‚Æć‚·ćƒ§ćƒ³ć® **[ć‚µćƒ–ć‚¹ć‚ÆćƒŖćƒ—ć‚·ćƒ§ćƒ³](subscriptions.md)** ć®ćƒšćƒ¼ć‚øć§čŖ¬ę˜Žć—ć¦ć„ć¾ć™ć€‚ ## ćƒ—ćƒ­ćƒ³ćƒ—ćƒˆ {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from č£œå®Œćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć‚’ęŒć¤ć‚µćƒ¼ćƒćƒ¼ćÆć€ćƒ¦ćƒ¼ć‚¶ćƒ¼ć®å…„åŠ›ć«åˆć‚ć›ć¦ćƒ—ćƒ­ćƒ³ćƒ—ćƒˆć‚„ćƒŖć‚½ćƒ¼ć‚¹ćƒ†ćƒ³ćƒ—ćƒ¬ćƒ¼ćƒˆć®å¼•ę•°ć‚’č‡Ŗå‹•č£œå®Œć§ćć¾ć™ć€‚ -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from `list_*` ćƒ”ć‚½ćƒƒćƒ‰ćÆć©ć‚Œć‚‚ `cursor=` ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰ć‚’å–ć‚Šć€ēµęžœćÆć©ć‚Œć‚‚ `next_cursor` ć‚’ęŒć”ć¾ć™ć€‚`next_cursor` が `None` ćŖć‚‰ć€ć™ć¹ć¦å–å¾—ęøˆćæć§ć™ć€‚ -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -ć“ć®ćƒ«ćƒ¼ćƒ—ćÆć©ć®ć‚µćƒ¼ćƒćƒ¼ć«åÆ¾ć—ć¦ć‚‚ę­£ć—ćå‹•ćć¾ć™ć€‚`MCPServer` はすべてを 1 ćƒšćƒ¼ć‚øć§čæ”ć™ć®ć§ć€`next_cursor` は `None` ć«ćŖć‚Šć€ćƒ«ćƒ¼ćƒ—ćÆ 1 å›žć ć‘å®Ÿč”Œć•ć‚Œć¾ć™ć€‚ć»ćØć‚“ć©ć®ć‚³ćƒ¼ćƒ‰ćŒć“ć®ćƒ«ćƒ¼ćƒ—ć‚’ę›øć‹ćŖć„ć®ćÆćć®ćŸć‚ć§ć™ć€‚å®Ÿéš›ć«ćƒšćƒ¼ć‚øåˆ†å‰²ć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ćØć€ć‚«ćƒ¼ć‚½ćƒ«ćŒå¾“ć†ćƒ«ćƒ¼ćƒ«ć«ć¤ć„ć¦ćÆ **[ćƒšćƒ¼ć‚øćƒćƒ¼ć‚·ćƒ§ćƒ³](../advanced/pagination.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ +`list_all_tools` ćÆć©ć®ć‚µćƒ¼ćƒćƒ¼ć«åÆ¾ć—ć¦ć‚‚ę­£ć—ćå‹•ćć¾ć™ć€‚`MCPServer` はすべてを 1 ćƒšćƒ¼ć‚øć§čæ”ć™ć®ć§ć€`next_cursor` は `None` ć«ćŖć‚Šć€ćƒ«ćƒ¼ćƒ—ćÆ 1 å›žć ć‘å®Ÿč”Œć•ć‚Œć¾ć™ć€‚ć»ćØć‚“ć©ć®ć‚³ćƒ¼ćƒ‰ćŒć“ć®ćƒ«ćƒ¼ćƒ—ć‚’ę›øć‹ćŖć„ć®ćÆćć®ćŸć‚ć§ć™ć€‚å®Ÿéš›ć«ćƒšćƒ¼ć‚øåˆ†å‰²ć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ćØć€ć‚«ćƒ¼ć‚½ćƒ«ćŒå¾“ć†ćƒ«ćƒ¼ćƒ«ć«ć¤ć„ć¦ćÆ **[ćƒšćƒ¼ć‚øćƒćƒ¼ć‚·ćƒ§ćƒ³](../advanced/pagination.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ ## ćƒ†ć‚¹ćƒˆć§ć®åˆ©ē”Ø {#in-tests} -ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ćƒćƒ¼ćƒˆć‚‚ä½æć‚ćŖć„ `Client(mcp)` ćÆć€ćć‚Œć ć‘ć§ć‚µćƒ¼ćƒćƒ¼ć®ćƒ†ć‚¹ćƒˆćƒćƒ¼ćƒć‚¹ć«ćŖć‚Šć¾ć™ć€‚ +ć“ć®ćƒšćƒ¼ć‚øć® `client.py` ćÆć©ć‚Œć‚‚ć€HTTP ēµŒē”±ć§ `server.py` ć«åˆ°é”ć—ć¦ć„ć¾ć—ćŸć€‚ćƒ†ć‚¹ćƒˆć§ćÆćƒćƒƒćƒˆćƒÆćƒ¼ć‚Æć‚’ēœćć€`Client` ć«ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ć‚’ęø”ć—ć¾ć™ć€‚`from server import mcp` としてから `Client(mcp)` ć§ć™ć€‚ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ćƒćƒ¼ćƒˆć‚‚äøč¦ć§ć€äøŠčØ˜ć®ćƒ”ć‚½ćƒƒćƒ‰ćÆć©ć‚Œć‚‚åŒć˜ć‚ˆć†ć«å‹•ćć¾ć™ć€‚ -ćć®ćŸć‚ć«ē”Øę„ć•ć‚ŒćŸć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æćƒ¼ć®ćƒ•ćƒ©ć‚°ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚`Client(mcp, raise_exceptions=True)` ć§ć™ć€‚åŠ¹ęžœćŒć‚ć‚‹ć®ćÆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖęŽ„ē¶šć®ćØćć ć‘ć§ć€ćć®čŖ¬ę˜ŽćØć€ćć‚Œć‚’äø­åæƒć«ć—ćŸćƒ‘ć‚æćƒ¼ćƒ³å…Øä½“ć®ēµ„ćæē«‹ć¦ćÆ **[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ć®ćƒšćƒ¼ć‚øć«ć‚ć‚Šć¾ć™ć€‚ +ćć®ćŸć‚ć«ē”Øę„ć•ć‚ŒćŸć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æćƒ¼ć®ćƒ•ćƒ©ć‚°ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚`Client(mcp, raise_exceptions=True)` ć§ć™ć€‚åŠ¹ęžœćŒć‚ć‚‹ć®ćÆćƒ—ćƒ­ć‚»ć‚¹å†…ęŽ„ē¶šć®ćØćć ć‘ć§ć€ćć®čŖ¬ę˜ŽćØć€ćć‚Œć‚’äø­åæƒć«ć—ćŸćƒ‘ć‚æćƒ¼ćƒ³å…Øä½“ć®ēµ„ćæē«‹ć¦ćÆ **[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ć®ćƒšćƒ¼ć‚øć«ć‚ć‚Šć¾ć™ć€‚ ## まとめ {#recap} -* `Client(x)` ćÆć€ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć«ćÆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ć€URL ę–‡å­—åˆ—ć«ćÆ Streamable HTTP ć§ć€ćć‚Œä»„å¤–ć«ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆēµŒē”±ć§ęŽ„ē¶šć—ć¾ć™ć€‚ +* `Client(x)` は、URL ę–‡å­—åˆ—ć«ćÆ Streamable HTTP ć§ęŽ„ē¶šć—ć€`StdioServerParameters` ć«ćÆć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚’čµ·å‹•ć—ć€ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć«ćÆē›“ęŽ„å…„ć‚Šć€ćƒ†ć‚¹ćƒˆć§ćÆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ * `async with` ćŒćƒ©ć‚¤ćƒ•ć‚µć‚¤ć‚Æćƒ«ć®ć™ć¹ć¦ć§ć™ć€‚ćć®äø­ć§ćÆ `server_capabilities` と `protocol_version` ć«ć™ć§ć«å€¤ćŒå…„ć£ć¦ćŠć‚Šć€ć‚µćƒ¼ćƒćƒ¼ćŒęä¾›ć—ć¦ć„ć‚Œć° `server_info` と `instructions` ć‚‚åŒę§˜ć§ć™ć€‚ * `list_tools()` ć§å„ćƒ„ćƒ¼ćƒ«ć® `name`态`title`态`description`态`input_schema` ćŒå¾—ć‚‰ć‚Œć¾ć™ć€‚ * `call_tool()` ćÆćƒ¢ćƒ‡ćƒ«å‘ć‘ć® `content`ć€ć‚³ćƒ¼ćƒ‰å‘ć‘ć® `structured_content`ć€ćć—ć¦ `is_error` ć‚’čæ”ć—ć¾ć™ć€‚ä¾‹å¤–ć‚’é€å‡ŗć™ć‚‹ćƒ„ćƒ¼ćƒ«ćÆć€ä¾‹å¤–ć§ćÆćŖćēµęžœćØć—ć¦čæ”ć£ć¦ćć¾ć™ć€‚ diff --git a/i18n/ja/pages/client/oauth-clients.md b/i18n/ja/pages/client/oauth-clients.md index e71580ae..fe05023d 100644 --- a/i18n/ja/pages/client/oauth-clients.md +++ b/i18n/ja/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ {#oauth-clients} @@ -77,18 +77,20 @@ translation: `Client` ćŒåˆć‚ć¦ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’é€ć‚‹ćØć€ć‚µćƒ¼ćƒćƒ¼ćÆ `401` ć‚’čæ”ć—ć¾ć™ć€‚ćć“ć‹ć‚‰ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ćŒå¼•ćē¶™ćŽć¾ć™ć€‚ -1. **発見。** `WWW-Authenticate` ćƒ˜ćƒƒćƒ€ćƒ¼ć‚’čŖ­ćæć€ć‚µćƒ¼ćƒćƒ¼ć® Protected Resource Metadata 悒 `/.well-known/oauth-protected-resource` ć‹ć‚‰å–å¾—ć—ć¾ć™ć€‚ćć“ć‹ć‚‰ć“ć®ćƒŖć‚½ćƒ¼ć‚¹ć‚’äæč­·ć—ć¦ć„ć‚‹čŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’ēŸ„ć‚Šć€ć€Œćć®ć€ć‚µćƒ¼ćƒćƒ¼ć®ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć‚’å–å¾—ć—ć¾ć™ć€‚ +1. **発見。** `WWW-Authenticate` ćƒ˜ćƒƒćƒ€ćƒ¼ć‚’čŖ­ćæć€ć‚µćƒ¼ćƒćƒ¼ć® Protected Resource Metadata 悒 `/.well-known/oauth-protected-resource` ć‹ć‚‰å–å¾—ć—ć¾ć™ć€‚ćć“ć‹ć‚‰ć“ć®ćƒŖć‚½ćƒ¼ć‚¹ć‚’äæč­·ć—ć¦ć„ć‚‹čŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’ēŸ„ć‚Šć€ć€Œćć®ć€ć‚µćƒ¼ćƒćƒ¼ć®ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć‚’å–å¾—ć—ć¾ć™ć€‚ļ¼ˆćƒŖć‚½ćƒ¼ć‚¹ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć‚’å…¬é–‹ć—ć¦ć„ćŖć„å¤ć„ć‚µćƒ¼ćƒćƒ¼ć«ćÆć€ä»£ć‚ć‚Šć«ćć®ć‚µćƒ¼ćƒćƒ¼č‡Ŗčŗ«ć®ć‚ŖćƒŖć‚øćƒ³ć§čŖåÆć‚µćƒ¼ćƒćƒ¼ć®ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć‚’å•ć„åˆć‚ć›ć¾ć™ć€‚ļ¼‰ć©ć”ć‚‰ć®å “åˆć‚‚ć€ćƒ”ć‚æćƒ‡ćƒ¼ć‚æćÆå–å¾—åÆ¾č±”ć®ć‚µćƒ¼ćƒćƒ¼ć‚’ `issuer` ćØć—ć¦åęŒ‡ć—ć—ć¦ć„ćŖć‘ć‚Œć°ćŖć‚Šć¾ć›ć‚“ć€‚ćć‚Œä»„å¤–ćÆę‹’å¦ć•ć‚Œć¾ć™ć€‚ 2. **ē™»éŒ²ć€‚** ć‚¹ćƒˆćƒ¬ćƒ¼ć‚øć«ä½•ć‚‚ćŖć‘ć‚Œć°ć€`OAuthClientMetadata` ć‚’ä½æć£ć¦å‹•ēš„ć«ē™»éŒ²ć—ć€ēµęžœć‚’äæå­˜ć—ć¾ć™ć€‚ 3. **čŖåÆć€‚** PKCE ć®ćƒšć‚¢ćØ `state` ć‚’ē”Ÿęˆć—ć€čŖåÆ URL を組み立て、`redirect_handler` 悒 await ć—ć¾ć™ć€‚ē¶šć„ć¦ć€ć‚³ćƒ¼ćƒ‰ć‚’å—ć‘å–ć‚‹ćŸć‚ć« `callback_handler` 悒 await します。 4. **äŗ¤ę›ć€‚** ć‚³ćƒ¼ćƒ‰ć‚’ `OAuthToken` ćØå¼•ćę›ćˆć¦äæå­˜ć—ć€å…ƒć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ `Authorization: Bearer ...` ä»˜ćć§å†é€ć—ć¾ć™ć€‚ ćć‚Œä»„é™ćÆé™ć‹ć«ćŖć‚Šć¾ć™ć€‚ćƒˆćƒ¼ć‚Æćƒ³ćÆć‚¹ćƒˆćƒ¬ćƒ¼ć‚øć‹ć‚‰å–ć‚Šå‡ŗć•ć‚Œć€ęœŸé™åˆ‡ć‚Œć®ć‚¢ć‚Æć‚»ć‚¹ćƒˆćƒ¼ć‚Æćƒ³ćÆćƒŖćƒ•ćƒ¬ćƒƒć‚·ćƒ„ćƒˆćƒ¼ć‚Æćƒ³ć§ę›“ę–°ć•ć‚Œć¾ć™ć€‚ćć®ć©ć‚Œć‚‚ć†ć¾ćć„ć‹ćŖć„ćØćć ć‘ć€ćƒ•ćƒ­ćƒ¼ć‚’ć‚‚ć†äø€åŗ¦å®Ÿč”Œć—ć¾ć™ć€‚ +ć“ć‚Œć‚‰ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć™ć¹ć¦ć«å…±é€šć™ć‚‹ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ćƒ«ćƒ¼ćƒ«ćŒ 1 ć¤ć‚ć‚Šć¾ć™ć€‚å¤–å“ć® MCP ćƒŖć‚Æć‚Øć‚¹ćƒˆćØåŒć˜ćć€ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć«å¾“ć†ć®ćÆåŒć˜ć‚ŖćƒŖć‚øćƒ³ć«ćØć©ć¾ć‚Šć€ć‹ć¤ćƒ”ć‚½ćƒƒćƒ‰ć‚’äæć¤å “åˆļ¼ˆćŸćØćˆć°ęœ«å°¾ć‚¹ćƒ©ćƒƒć‚·ćƒ„ć® 307/308ļ¼‰ć ć‘ć§ć™ć€‚ćć‚Œä»„å¤–ć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚ÆćƒˆćÆć€ćć® URL ćŒåæœē­”ć—ćŖć‹ć£ćŸć‚‚ć®ćØć—ć¦ę‰±ć„ć¾ć™ć€‚ + ć“ć‚Œć‚‰ć‚’č‡Ŗåˆ†ć§ę›øćåæ…č¦ćÆć¾ć£ćŸćć‚ć‚Šć¾ć›ć‚“ć§ć—ćŸć€‚ę®‹ć‚‹ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰å¼•ę•°ćÆ 2 つ(`client_metadata_url` と `validate_resource_url`ļ¼‰ć§ć€ć“ć®ćƒ•ć‚”ć‚¤ćƒ«ć§ćÆć©ć”ć‚‰ć‚‚äøč¦ć§ć™ć€‚ēŸ„ć£ć¦ćŠćä¾”å€¤ćŒć‚ć‚‹ć®ćÆ `client_metadata_url` ć®ć»ć†ć§ć€äø‹ć«å°‚ē”Øć®ć‚»ć‚Æć‚·ćƒ§ćƒ³ćŒć‚ć‚Šć¾ć™ć€‚ ### 試してみる {#try-it} -ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć®ä¾‹ć®ć»ćØć‚“ć©ćÆć€ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć® `Client(server)` ć§ē¢ŗčŖć§ćć¾ć™ć€‚ć“ć‚ŒćÆé•ć„ć¾ć™ć€‚ć“ć®ćƒ•ćƒ­ćƒ¼ć®č¦ē‚¹ćÆ HTTP 恮 `401` ć§ć‚ć‚Šć€ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØć‚µćƒ¼ćƒćƒ¼ć®ć‚ć„ć ć«ćÆ HTTP ćŒć‚ć‚Šć¾ć›ć‚“ć€‚ +ćƒ†ć‚¹ćƒˆć§ä½æć†ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć® `Client(server)` ćÆć€ć“ć“ć§ćÆå½¹ć«ē«‹ć”ć¾ć›ć‚“ć€‚ć“ć®ćƒ•ćƒ­ćƒ¼ć®č¦ē‚¹ćÆ HTTP 恮 `401` ć§ć‚ć‚Šć€ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØćć®ć‚µćƒ¼ćƒćƒ¼ć®ć‚ć„ć ć«ćÆ HTTP ćŒćŖć„ć‹ć‚‰ć§ć™ć€‚ ćƒŖćƒć‚øćƒˆćƒŖć«ćÆå®Ÿéš›ć«å‹•ććƒćƒ¼ć‚øćƒ§ćƒ³ćŒåŒę¢±ć•ć‚Œć¦ć„ć¾ć™ć€‚`examples/servers/simple-auth/` ćÆć‚¹ć‚æćƒ³ćƒ‰ć‚¢ćƒ­ćƒ³ć®čŖåÆć‚µćƒ¼ćƒćƒ¼ćØäæč­·ć•ć‚ŒćŸ MCP ć‚µćƒ¼ćƒćƒ¼ć‚’å‹•ć‹ć—ć€`examples/clients/simple-auth-client/` ćÆć“ć®ćƒšćƒ¼ć‚øć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’å°ć•ćŖ CLI ć«č‚²ć¦ćŸć‚‚ć®ć§ć™ć€‚ćć® README 恫 2 ć¤ć®ć‚³ćƒžćƒ³ćƒ‰ćŒč¼‰ć£ć¦ć„ć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚’čµ·å‹•ć—ć€ćć‚Œć«åÆ¾ć—ć¦ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’å®Ÿč”Œć™ć‚Œć°ć€4 ć¤ć®ć‚¹ćƒ†ćƒƒćƒ—ćŒé€²ć‚“ć§ć„ćć®ć‚’č¦‹ć‚‰ć‚Œć¾ć™ć€‚ @@ -106,13 +108,14 @@ URL は HTTPS ć§ć€ćƒ«ćƒ¼ćƒˆä»„å¤–ć®ćƒ‘ć‚¹ć‚’ęŒć£ć¦ć„ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ `ClientCredentialsOAuthProvider` は同じ `httpx2.Auth` ć§ć€äŗŗé–“ćŒć„ćŖć„ć ć‘ć§ć™ć€‚ -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` å¤‰ć‚ć£ćŸē‚¹ćÆę¬”ć®ćØćŠć‚Šć§ć™ć€‚ * `OAuthClientMetadata` ć‚‚ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚`client_id` と `client_secret` ć‚’ęø”ć™ćØć€ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ćÆćć‚Œć‚‰ć‚’äø­åæƒć«ęœ€å°é™ć® `client_credentials` ē™»éŒ²ć‚’ēµ„ćæē«‹ć¦ć€å‹•ēš„ē™»éŒ²ć‚’å®Œå…Øć«ēœćć¾ć™ć€‚ +* `issuer` ć«ćÆć€ćć®ć‚Æćƒ¬ćƒ‡ćƒ³ć‚·ćƒ£ćƒ«ć‚’ē™ŗč”Œć—ćŸčŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’ęŒ‡å®šć—ć¾ć™ć€‚ćć®ć‚µćƒ¼ćƒćƒ¼ć® `/.well-known/oauth-authorization-server` ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆćŒčæ”ć™ `issuer` ć®å€¤ć‚’ä½æć£ć¦ćć ć•ć„ć€‚ē™ŗč¦‹ćÆäøŠćØåŒć˜ć‚ˆć†ć«å®Ÿč”Œć•ć‚Œć¾ć™ćŒć€ćƒˆćƒ¼ć‚Æćƒ³ćƒŖć‚Æć‚Øć‚¹ćƒˆćÆć€Œćć®ć€ē™ŗč”Œč€…ć®ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć‹ć‚‰ć—ć‹ēµ„ćæē«‹ć¦ć‚‰ć‚Œć¾ć›ć‚“ć€‚MCP ć‚µćƒ¼ćƒćƒ¼ćŒćć‚Œä»„å¤–ć®å “ę‰€ć‚’ęŒ‡ć—ć¦ć„ć‚Œć°ć€ćƒ•ćƒ­ćƒ¼ćÆä»£ć‚ć‚Šć« `OAuthFlowError` ć§ę­¢ć¾ć‚Šć¾ć™ć€‚ēœē•„ć™ć‚‹ć®ćÆéžęŽØå„Øć§ć€3.0 ć§ćÆåæ…é ˆć«ćŖć‚Šć¾ć™ļ¼ˆ**[éžęŽØå„Øć®ę©Ÿčƒ½](../deprecated.md#deprecated-sdk-helpers)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ļ¼‰ć€‚ćć‚Œć¾ć§ć®ć‚ć„ć ć€ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ćÆč­¦å‘Šć‚’å‡ŗć—ćŸć†ćˆć§ć€ē™ŗč¦‹ć§č¦‹ć¤ć‹ć£ćŸčŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’ćć®ć¾ć¾ä½æć„ć¾ć™ć€‚ * `scope` ćÆć‚¹ćƒšćƒ¼ć‚¹åŒŗåˆ‡ć‚Šć®ę–‡å­—åˆ—ć§ć€OAuth ć®é€šäæ”äøŠć®å½¢å¼ć§ć™ć€‚ * ćć®å…ˆćÆć™ć¹ć¦åŒć˜ć§ć™ć€‚åŒć˜ `TokenStorage`ć€åŒć˜ `httpx2.AsyncClient(auth=...)`ć€åŒć˜ `streamable_http_client` 恧恙怂 @@ -123,7 +126,7 @@ URL は HTTPS ć§ć€ćƒ«ćƒ¼ćƒˆä»„å¤–ć®ćƒ‘ć‚¹ć‚’ęŒć£ć¦ć„ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ !!! info `mcp.client.auth.extensions.client_credentials` にはもう 1 ć¤ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ćŒć‚ć‚Šć¾ć™ć€‚ - **`PrivateKeyJWTOAuthProvider`** ćÆć€å…±ęœ‰ć‚·ćƒ¼ć‚Æćƒ¬ćƒƒćƒˆć®ä»£ć‚ć‚Šć« JWT ć§čŖčØ¼ć™ć‚‹ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå‘ć‘ć§ć™ļ¼ˆ`private_key_jwt`ć€ć¤ć¾ć‚Šéµćƒšć‚¢ć‚„ćƒÆćƒ¼ć‚Æćƒ­ćƒ¼ćƒ‰ć‚¢ć‚¤ćƒ‡ćƒ³ćƒ†ć‚£ćƒ†ć‚£ć®ę–¹å¼ļ¼‰ć€‚ćƒ‘ć‚æćƒ¼ćƒ³ćÆåŒć˜ć§ć€1 つ構築して `auth=` ć«č¼‰ć›ć¾ć™ć€‚åŒć˜ćƒ¢ć‚øćƒ„ćƒ¼ćƒ«ć«ćÆć€ćć®ć‚¢ć‚µćƒ¼ć‚·ćƒ§ćƒ³ć‚’ēµ„ćæē«‹ć¦ć‚‹ 2 ć¤ć®ćƒ˜ćƒ«ćƒ‘ćƒ¼ć€`SignedJWTParameters` と `static_assertion_provider` ć‚‚å«ć¾ć‚Œć¦ć„ć¾ć™ć€‚ + **`PrivateKeyJWTOAuthProvider`** ćÆć€å…±ęœ‰ć‚·ćƒ¼ć‚Æćƒ¬ćƒƒćƒˆć®ä»£ć‚ć‚Šć« JWT ć§čŖčØ¼ć™ć‚‹ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå‘ć‘ć§ć™ļ¼ˆ`private_key_jwt`ć€ć¤ć¾ć‚Šéµćƒšć‚¢ć‚„ćƒÆćƒ¼ć‚Æćƒ­ćƒ¼ćƒ‰ć‚¢ć‚¤ćƒ‡ćƒ³ćƒ†ć‚£ćƒ†ć‚£ć®ę–¹å¼ļ¼‰ć€‚ćƒ‘ć‚æćƒ¼ćƒ³ćÆåŒć˜ć§ć€1 ć¤ę§‹ēÆ‰ć—ć¦ļ¼ˆåŒć˜ćēœē•„åÆčƒ½ćŖ `issuer` ć‚’å–ć‚Šć¾ć™ļ¼‰`auth=` ć«č¼‰ć›ć¾ć™ć€‚åŒć˜ćƒ¢ć‚øćƒ„ćƒ¼ćƒ«ć«ćÆć€ćć®ć‚¢ć‚µćƒ¼ć‚·ćƒ§ćƒ³ć‚’ēµ„ćæē«‹ć¦ć‚‹ 2 ć¤ć®ćƒ˜ćƒ«ćƒ‘ćƒ¼ć€`SignedJWTParameters` と `static_assertion_provider` ć‚‚å«ć¾ć‚Œć¦ć„ć¾ć™ć€‚ äŗŗé–“ćŒć„ćŖć„ēŠ¶ę³ćÆć‚‚ć† 1 ć¤ć‚ć‚Šć¾ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒä¼ę„­ć«å±žć—ć¦ć„ć¦ć€ć©ć® MCP ć‚µćƒ¼ćƒćƒ¼ć«åˆ°é”ć—ć¦ć‚ˆć„ć‹ć‚’ćƒ¦ćƒ¼ć‚¶ćƒ¼ć§ćÆćŖććć®ä¼ę„­ć®ć‚¢ć‚¤ćƒ‡ćƒ³ćƒ†ć‚£ćƒ†ć‚£ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ćŒę±ŗć‚ć‚‹å “åˆć§ć™ć€‚ć“ć‚ŒćÆē‹¬č‡Ŗć®äæ”é ¼ćƒ¢ćƒ‡ćƒ«ć‚’ęŒć¤åˆ„ć®ć‚°ćƒ©ćƒ³ćƒˆć§ć€å°‚ē”Øć®ćƒšćƒ¼ć‚ø **[ć‚¢ć‚¤ćƒ‡ćƒ³ćƒ†ć‚£ćƒ†ć‚£ć‚¢ć‚µćƒ¼ć‚·ćƒ§ćƒ³](identity-assertion.md)** ćŒć‚ć‚Šć¾ć™ć€‚ diff --git a/i18n/ja/pages/client/session-groups.md b/i18n/ja/pages/client/session-groups.md index 88c62acb..af1c4767 100644 --- a/i18n/ja/pages/client/session-groups.md +++ b/i18n/ja/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚°ćƒ«ćƒ¼ćƒ— {#session-groups} @@ -76,7 +76,7 @@ translation: ## まとめ {#recap} * `ClientSessionGroup` ćÆå¤šę•°ć®ć‚µćƒ¼ćƒćƒ¼ęŽ„ē¶šć‚’äæęŒć—ć€ćć‚Œć‚‰ć®ćƒ„ćƒ¼ćƒ«ć€ćƒŖć‚½ćƒ¼ć‚¹ć€ćƒ—ćƒ­ćƒ³ćƒ—ćƒˆć‚’ćć‚Œćžć‚Œ 1 恤恮 `dict` にまとめます。 -* ć‚µćƒ¼ćƒćƒ¼ć”ćØć« `connect_to_server(params)` ć‚’å‘¼ć³ć¾ć™ć€‚å—ć‘å–ć‚‹ć®ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć§ć‚ć‚Šć€`Client` ćŒå—ć‘å–ć‚‹ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚„ URL ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ +* ć‚µćƒ¼ćƒćƒ¼ć”ćØć« `connect_to_server(params)` ć‚’å‘¼ć³ć¾ć™ć€‚å—ć‘å–ć‚‹ć®ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć§ć‚ć‚Šć€`Client` ćŒå—ć‘å–ć‚‹ URL 悄 `Transport` ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ * `group.call_tool(name, arguments)` ćÆć€ę‰€ęœ‰ć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ćøć®ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć‚’ä»£ć‚ć‚Šć«č”Œć„ć¾ć™ć€‚ * åå‰ćÆć‚°ćƒ«ćƒ¼ćƒ—å…Øä½“ć§äø€ę„ć§ćŖć‘ć‚Œć°ćŖć‚Šć¾ć›ć‚“ć€‚`search` ćƒ„ćƒ¼ćƒ«ć‚’ęŒć¤ 2 ć¤ć®ć‚µćƒ¼ćƒćƒ¼ćÆć€ćć®ć¾ć¾ć§ćÆå…±å­˜ć§ćć¾ć›ć‚“ć€‚ * `component_name_hook=` ćÆē™»éŒ²ć•ć‚Œć‚‹ć™ć¹ć¦ć®åå‰ć‚’ę›øćę›ćˆć¾ć™ć€‚dict ć®ć‚­ćƒ¼ćÆå¤‰ć‚ć‚Šć¾ć™ćŒć€å®Ÿéš›ć«é€äæ”ć•ć‚Œć‚‹åå‰ćÆå¤‰ć‚ć‚Šć¾ć›ć‚“ć€‚ diff --git a/i18n/ja/pages/client/transports.md b/i18n/ja/pages/client/transports.md index 4c61766a..409d4916 100644 --- a/i18n/ja/pages/client/transports.md +++ b/i18n/ja/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ {#client-transports} @@ -11,30 +11,15 @@ translation: ćć‚Œćžć‚Œć®ć€Œć‚µćƒ¼ćƒćƒ¼ć€å“ļ¼ˆ`mcp.run()` ćŒä½•ć‚’ć™ć‚‹ć®ć‹ć€ä½•ć‚’ćƒ‡ćƒ—ćƒ­ć‚¤ć™ć‚‹ć®ć‹ļ¼‰ć«ć¤ć„ć¦ćÆć€**[ć‚µćƒ¼ćƒćƒ¼ć®å®Ÿč”Œ](../run/index.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ -## ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖ {#in-memory} - -ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ć‚’ęø”ć—ć¾ć™ć€‚ - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ć€ćƒćƒ¼ćƒˆć‚‚ć€é€šäæ”č·Æć‚’ęµć‚Œć‚‹ćƒć‚¤ćƒˆåˆ—ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØć‚µćƒ¼ćƒćƒ¼ćÆåŒć˜ćƒ—ćƒ­ć‚»ć‚¹å†…ć® 2 ć¤ć®ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć§ć™ćŒć€å‘¼ć³å‡ŗć—ćÆęœ¬ē‰©ć®ćƒ—ćƒ­ćƒˆć‚³ćƒ«å±¤ć‚’é€šć‚Šć¾ć™ć€‚`search_books` は、HTTP č¶Šć—ć®å “åˆćØć¾ć£ćŸćåŒć˜ć‚ˆć†ć«äø€č¦§ć«č¼‰ć‚Šć€ę¤œčØ¼ć•ć‚Œć€å‘¼ć³å‡ŗć•ć‚Œć¾ć™ć€‚ - -ćć®ćŸć‚ć€ć“ć‚ŒćÆåŒę™‚ć« 2 ć¤ć®å½¹å‰²ć‚’ęžœćŸć—ć¾ć™ć€‚ - -* **ćƒ†ć‚¹ćƒˆćƒćƒ¼ćƒć‚¹ć€‚** ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć®ä¾‹ćÆć™ć¹ć¦ć“ć®ę–¹ę³•ć§å®Ÿč”Œć•ć‚Œć¦ćŠć‚Šć€**[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ć®ćƒšćƒ¼ć‚øćÆć“ć®ćƒ‘ć‚æćƒ¼ćƒ³ć‚’äø­åæƒć«ēµ„ćæē«‹ć¦ć‚‰ć‚Œć¦ć„ć¾ć™ć€‚ -* **組み込み用の API怂** ć‚µćƒ¼ćƒćƒ¼ć‚’č‡Ŗåˆ†ć§ę§‹ēÆ‰ć™ć‚‹ć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ćŖć‚‰ć€ćć®ćƒ„ćƒ¼ćƒ«ć‚’å‘¼ć³å‡ŗć™ć®ć«ćƒćƒƒćƒˆćƒÆćƒ¼ć‚Æč¶Šć—ć®ēµŒč·ÆćÆåæ…č¦ć‚ć‚Šć¾ć›ć‚“ć€‚ - ## Streamable HTTP {#streamable-http} -URL ć®ę–‡å­—åˆ—ć‚’ęø”ć™ćØ **Streamable HTTP** ć«ćŖć‚Šć¾ć™ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ę™‚ć«ä½æć†ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć™ć€‚ +URL ć®ę–‡å­—åˆ—ć‚’ęø”ć™ćØ **Streamable HTTP** ć«ćŖć‚Šć¾ć™ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ę™‚ć«ä½æć†ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚ć‚Šć€ęœ€åˆć«éøć¶ć¹ććƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć™ć€‚ ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -ęœ¬ē•Ŗē”Øć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć“ć‚Œć§ć™ć¹ć¦ć§ć™ć€‚`Client` は URL 悒 `streamable_http_client(...)` ć§åŒ…ćæć€MCP ć«åæ…č¦ćŖčØ­å®šć‚’ę–½ć—ćŸ `httpx2.AsyncClient` ć®äøŠć«č¼‰ć›ć¦ćć‚Œć¾ć™ć€‚å…·ä½“ēš„ć«ćÆ `follow_redirects=True`态connect/write/pool ć®ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćŒ 30 ē§’ć€ćć—ć¦ć‚µćƒ¼ćƒćƒ¼ćŒćƒ¬ć‚¹ćƒćƒ³ć‚¹ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć‚’é–‹ć„ćŸć¾ć¾ć«ć™ć‚‹ć“ćØćŒć‚ć‚‹ćŸć‚ read ć®ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćŒ 300 秒です。 +ęœ¬ē•Ŗē”Øć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć“ć‚Œć§ć™ć¹ć¦ć§ć™ć€‚`Client` は URL 悒 `streamable_http_client(...)` ć§åŒ…ćæć€MCP ć«åæ…č¦ćŖčØ­å®šć‚’ę–½ć—ćŸ `httpx2.AsyncClient` ć®äøŠć«č¼‰ć›ć¦ćć‚Œć¾ć™ć€‚å…·ä½“ēš„ć«ćÆć€connect/write/pool ć®ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćŒ 30 ē§’ć€ćć—ć¦ć‚µćƒ¼ćƒćƒ¼ćŒćƒ¬ć‚¹ćƒćƒ³ć‚¹ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć‚’é–‹ć„ćŸć¾ć¾ć«ć™ć‚‹ć“ćØćŒć‚ć‚‹ćŸć‚ read ć®ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćŒ 300 秒です。 !!! check ę§‹ēÆ‰ć—ćŸć ć‘ć® `Client` ćÆęŽ„ē¶šć•ć‚Œć¦**いません**ć€‚ę§‹ēÆ‰ę™‚ć«č”Œć‚ć‚Œć‚‹ć®ćÆćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®éøęŠžć ć‘ć§ć€å®Ÿéš›ć«é–‹ćć®ćÆ `async with` ć§ć™ć€‚å…„ć‚‹å‰ć«ęŽ„ē¶šć«ę‰‹ć‚’ä¼øć°ć™ćØć€SDK ćŒćć®ć“ćØć‚’ę•™ćˆć¦ćć‚Œć¾ć™ć€‚ @@ -49,7 +34,7 @@ URL ć®ę–‡å­—åˆ—ć‚’ęø”ć™ćØ **Streamable HTTP** ć«ćŖć‚Šć¾ć™ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ `Authorization` ćƒ˜ćƒƒćƒ€ćƒ¼ć€Cookieć€ćƒ—ćƒ­ć‚­ć‚·ć€mTLSć€ć‚ć‚‹ć„ćÆåˆ„ć®ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćŒåæ…č¦ć«ćŖć£ćŸć‚‰ć€`httpx2.AsyncClient` ć‚’č‡Ŗåˆ†ć§ēµ„ćæē«‹ć¦ć¦ `streamable_http_client` に渔します。 -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -70,7 +55,25 @@ TLS 恫恤恄恦 1 点。`httpx2` ćÆć€åŒę¢±ć® CA ćƒŖć‚¹ćƒˆć§ćÆćŖćć€ć‚Ŗ HTTP ć«é–¢ć‚ć‚‹ć‚‚ć®ćÆć™ć¹ć¦ć€ęø”ć™ 1 恤恮 `httpx2.AsyncClient` ć«é›†ē“„ć•ć‚Œć¦ć„ć¾ć™ć€‚ !!! info - `httpx2` はおなじみの `httpx` 恮 API ć‚’ćć®ć¾ć¾äæć£ć¦ć„ć‚‹ć®ć§ć€`httpx` ć‚’ēŸ„ć£ć¦ć„ć‚Œć°ć€čŖčØ¼ć€ćƒ—ćƒ­ć‚­ć‚·ć€ć‚¤ćƒ™ćƒ³ćƒˆćƒ•ćƒƒć‚Æć€ćƒŖćƒˆćƒ©ć‚¤ć€ęŽ„ē¶šę•°ć®åˆ¶é™ć®ć‚„ć‚Šę–¹ćÆć™ć§ć«ēŸ„ć£ć¦ć„ć‚‹ć“ćØć«ćŖć‚Šć¾ć™ć€‚SDK ćÆćć®äøŠć«ä½•ć‚‚č¶³ć•ćšć€ä½•ć‚‚å¼•ćć¾ć›ć‚“ć€‚OAuth ćŒå·®ć—č¾¼ć¾ć‚Œć‚‹ć®ć‚‚ć“ć“ć§ć™ć€‚`httpx2.AsyncClient(auth=OAuthClientProvider(...))` ć®ć‚ˆć†ć«ę›øćć¾ć™ć€‚ćć®ćƒ•ćƒ­ćƒ¼å…Øä½“ć«ć¤ć„ć¦ćÆ **[OAuth ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](oauth-clients.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ + `httpx2` はおなじみの `httpx` 恮 API ć‚’ćć®ć¾ć¾äæć£ć¦ć„ć‚‹ć®ć§ć€`httpx` ć‚’ēŸ„ć£ć¦ć„ć‚Œć°ć€čŖčØ¼ć€ćƒ—ćƒ­ć‚­ć‚·ć€ć‚¤ćƒ™ćƒ³ćƒˆćƒ•ćƒƒć‚Æć€ćƒŖćƒˆćƒ©ć‚¤ć€ęŽ„ē¶šę•°ć®åˆ¶é™ć®ć‚„ć‚Šę–¹ćÆć™ć§ć«ēŸ„ć£ć¦ć„ć‚‹ć“ćØć«ćŖć‚Šć¾ć™ć€‚SDK ćÆćć®äøŠć«ä½•ć‚‚č¶³ć•ćšć€ä½•ć‚‚å¼•ćć¾ć›ć‚“ć€‚å”Æäø€ć®ä¾‹å¤–ćŒ[ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć®ę‰±ć„](#redirects)恧恙怂OAuth ćŒå·®ć—č¾¼ć¾ć‚Œć‚‹ć®ć‚‚ć“ć“ć§ć™ć€‚`httpx2.AsyncClient(auth=OAuthClientProvider(...))` ć®ć‚ˆć†ć«ę›øćć¾ć™ć€‚ćć®ćƒ•ćƒ­ćƒ¼å…Øä½“ć«ć¤ć„ć¦ćÆ **[OAuth ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](oauth-clients.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ + +### ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆ {#redirects} + +ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćÆęø”ć•ć‚ŒćŸ URL ć«ęŽ„ē¶šć—ć€ćć®ć‚ŖćƒŖć‚øćƒ³ć«ć ć‘ęŽ„ē¶šć—ć¾ć™ć€‚ + +* åŒć˜ć‚¹ć‚­ćƒ¼ćƒ ć€ćƒ›ć‚¹ćƒˆć€ćƒćƒ¼ćƒˆć«ćØć©ć¾ć‚‹ `307`/`308` ć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć«ćÆå¾“ć„ć¾ć™ć€‚åŒć˜ćƒ›ć‚¹ćƒˆäøŠć§ć® `http://` → `https://` ć‚‚åŒę§˜ć§ć™ć€‚ć‚ˆćć‚ć‚‹ `/mcp` → `/mcp/` ćØć„ć†ęœ«å°¾ć‚¹ćƒ©ćƒƒć‚·ćƒ„ć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚ÆćƒˆćÆć“ć‚Œć§ć‚«ćƒćƒ¼ć•ć‚Œć¾ć™ć€‚ +* ćć‚Œä»„å¤–ć®å “ę‰€ćøć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć«ćÆå¾“ć„**ません**ć€‚å‘¼ć³å‡ŗć—ćÆę¬”ć®ć‚Øćƒ©ćƒ¼ć§å¤±ę•—ć—ć¾ć™ć€‚ + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + ćć® URL ćŒę„å›³ć—ćŸć‚µćƒ¼ćƒćƒ¼ćŖć‚‰ć€čØ­å®šć«ćć® URL ć‚’ę›øć„ć¦ćć ć•ć„ć€‚ćć†ć§ćŖć‘ć‚Œć°ć€ć‚µćƒ¼ćƒćƒ¼ć‹ć€ćć®å‰ę®µć«ć‚ć‚‹ćƒ—ćƒ­ć‚­ć‚·ć®čØ­å®šćŒčŖ¤ć£ć¦ć„ć¾ć™ć€‚ + +ć“ć‚ŒćÆć€ęø”ć™ć©ć® `httpx2.AsyncClient` ć«ć‚‚å½“ć¦ćÆć¾ć‚Šć¾ć™ć€‚ćć® `follow_redirects` ć®čØ­å®šćÆć€MCP ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«ć¤ć„ć¦ćÆć©ć”ć‚‰ć®ę–¹å‘ć«ć‚‚å‚ē…§ć•ć‚Œć¾ć›ć‚“ć€‚SDK 恮 OAuth ćƒ—ćƒ­ćƒć‚¤ćƒ€ćƒ¼ć‚‚ć€č‡Ŗčŗ«ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«åŒć˜č¦å‰‡ć‚’é©ē”Øć—ć¾ć™ć€‚ + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` ćÆć€ć‚µćƒ¼ćƒćƒ¼ćŒč‡Ŗčŗ«ć®ēŸ„ć‚‰ćŖć„ TLS ēµ‚ē«Æćƒ—ćƒ­ć‚­ć‚·ć®čƒŒå¾Œć«ć‚ć‚Šć€`http://` ć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć‚’ē™ŗč”Œć—ć¦ć„ć‚‹ć“ćØć‚’ę„å‘³ć—ć¾ć™ć€‚ć“ć‚ŒćÆć‚µćƒ¼ćƒćƒ¼å“ć§ē›“ć™ć‹ļ¼ˆ**[ćƒ‡ćƒ—ćƒ­ć‚¤ćØć‚¹ć‚±ćƒ¼ćƒ«](../run/deploy.md#behind-a-tls-terminating-proxy)**ļ¼‰ć€ćƒ”ćƒƒć‚»ćƒ¼ć‚øćŒē¤ŗć™ćØćŠć‚Šć®ę­£ē¢ŗćŖ `https://…/` 恮 URL を使うことで解決します。 ## stdio {#stdio} @@ -91,6 +94,18 @@ TLS 恫恤恄恦 1 点。`httpx2` ćÆć€åŒę¢±ć® CA ćƒŖć‚¹ćƒˆć§ćÆćŖćć€ć‚Ŗ API ć‚­ćƒ¼ć‚’åæ…č¦ćØć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ćÆć€ćć“ć§ć‚­ćƒ¼ć‚’č¦‹ć¤ć‘ć‚‰ć‚Œć¾ć›ć‚“ć€‚`env=` ć§ę˜Žē¤ŗēš„ć«ęø”ć—ć¦ćć ć•ć„ć€‚ćć‚Œć‚‰ć®å¤‰ę•°ćÆčØ±åÆćƒŖć‚¹ćƒˆć®äøŠć«ćƒžćƒ¼ć‚øć•ć‚Œć¾ć™ć€‚äøŠć®ä¾‹ć§ `BOOKSHOP_API_KEY` ćŒć—ć¦ć„ć‚‹ć®ćŒć¾ć•ć«ćć‚Œć§ć™ć€‚ +## ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖ {#in-memory} + +ćƒ†ć‚¹ćƒˆć§ćÆć€ćƒ‡ćƒ—ćƒ­ć‚¤ć™ć‚‹ć‚‚ć®ć‚‚čµ·å‹•ć™ć‚‹ć‚‚ć®ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ć‚’ęø”ć—ć¾ć™ć€‚ + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ć€ćƒćƒ¼ćƒˆć‚‚ć€é€šäæ”č·Æć‚’ęµć‚Œć‚‹ćƒć‚¤ćƒˆåˆ—ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØć‚µćƒ¼ćƒćƒ¼ćÆåŒć˜ćƒ—ćƒ­ć‚»ć‚¹å†…ć® 2 ć¤ć®ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć§ć™ćŒć€å‘¼ć³å‡ŗć—ćÆęœ¬ē‰©ć®ćƒ—ćƒ­ćƒˆć‚³ćƒ«å±¤ć‚’é€šć‚Šć¾ć™ć€‚`search_books` は、HTTP č¶Šć—ć®å “åˆćØć¾ć£ćŸćåŒć˜ć‚ˆć†ć«äø€č¦§ć«č¼‰ć‚Šć€ę¤œčØ¼ć•ć‚Œć€å‘¼ć³å‡ŗć•ć‚Œć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ć®ćƒšćƒ¼ć‚øćÆć€ć“ć®ćƒ‘ć‚æćƒ¼ćƒ³ć‚’äø­åæƒć«ēµ„ćæē«‹ć¦ć‚‰ć‚Œć¦ć„ć¾ć™ć€‚ + +åŒć˜å½¢ćÆēµ„ćæč¾¼ćæē”Øć® API ćØć—ć¦ć‚‚ä½æćˆć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚’č‡Ŗåˆ†ć§ę§‹ēÆ‰ć™ć‚‹ć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ćŖć‚‰ć€ćƒćƒƒćƒˆćƒÆćƒ¼ć‚Æč¶Šć—ć®ēµŒč·ÆćŖć—ć«ćć®ćƒ„ćƒ¼ćƒ«ć‚’å‘¼ć³å‡ŗć›ć¾ć™ć€‚ + ## SSE {#sse} `mcp.client.sse` 恮 `sse_client(url)` は、Streamable HTTP ć«å–ć£ć¦ä»£ć‚ć‚‰ć‚ŒćŸ HTTP ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć™ć€‚ć¾ć ć“ć‚Œć‚’č©±ć™ć‚µćƒ¼ćƒćƒ¼ćØåÆ¾č©±ć™ć‚‹ć«ćÆć€åŒć˜ć‚ˆć†ć« `Client(sse_client("http://localhost:8000/sse"))` ćØåŒ…ćæć¾ć™ć€‚ćć—ć¦ć€ę–°ć—ć„ć‚‚ć®ć‚’ć“ć®äøŠć«ä½œć‚‰ćŖć„ć§ćć ć•ć„ć€‚ @@ -99,15 +114,16 @@ TLS 恫恤恄恦 1 点。`httpx2` ćÆć€åŒę¢±ć® CA ćƒŖć‚¹ćƒˆć§ćÆćŖćć€ć‚Ŗ `Client` ć‹ć‚‰č¦‹ć‚Œć°ć€äøŠčØ˜ćÆć™ć¹ć¦åŒć˜ć‚‚ć®ć§ć™ć€‚ -**ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ**とは、`(read, write)` ćØć„ć†ćƒ”ćƒƒć‚»ćƒ¼ć‚øć‚¹ćƒˆćƒŖćƒ¼ćƒ ć®ćƒšć‚¢ć‚’ yield ć™ć‚‹éžåŒęœŸć‚³ćƒ³ćƒ†ć‚­ć‚¹ćƒˆćƒžćƒćƒ¼ć‚øćƒ£ćƒ¼ć®ć“ćØć§ć™ć€‚ę­£å¼ć«ćÆ `mcp.client` 恮 `Transport` ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć§ć™ć€‚`Client` ćÆå¼•ę•°ć‚’åž‹ć§č§£ę±ŗć—ć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚ÆćƒˆćŖć‚‰ć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ć§ęŽ„ē¶šć—ć€`str` なら `streamable_http_client(url)` ć«ćŖć‚Šć€`StdioServerParameters` なら `stdio_client(params)` ć«ćŖć‚Šć€ćć‚Œä»„å¤–ćÆē›“ęŽ„ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćØć—ć¦å…„ć‚Šć¾ć™ć€‚ć“ć®ęœ€å¾Œć®č¦å‰‡ćŒć‚ć‚‹ć‹ć‚‰ć“ćć€`stdio_client(...)`态`streamable_http_client(...)`态`sse_client(...)` ćÆć™ć¹ć¦åŒć˜å “ę‰€ć«åŽć¾ć‚Šć€č‡Ŗåˆ†ć§ē‹¬č‡Ŗć®ć‚‚ć®ć‚’ę›øćć“ćØć‚‚ć§ćć¾ć™ć€‚ +**ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ**とは、`(read, write)` ćØć„ć†ćƒ”ćƒƒć‚»ćƒ¼ć‚øć‚¹ćƒˆćƒŖćƒ¼ćƒ ć®ćƒšć‚¢ć‚’ yield ć™ć‚‹éžåŒęœŸć‚³ćƒ³ćƒ†ć‚­ć‚¹ćƒˆćƒžćƒćƒ¼ć‚øćƒ£ćƒ¼ć®ć“ćØć§ć™ć€‚ę­£å¼ć«ćÆ `mcp.client` 恮 `Transport` ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć§ć™ć€‚`Client` ćÆå¼•ę•°ć‚’åž‹ć§č§£ę±ŗć—ć¾ć™ć€‚`str` なら `streamable_http_client(url)` ć«ćŖć‚Šć€`StdioServerParameters` なら `stdio_client(params)` ć«ćŖć‚Šć€ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚ÆćƒˆćŖć‚‰ć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ć§ęŽ„ē¶šć—ć€ćć‚Œä»„å¤–ćÆē›“ęŽ„ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćØć—ć¦å…„ć‚Šć¾ć™ć€‚ć“ć®ęœ€å¾Œć®č¦å‰‡ćŒć‚ć‚‹ć‹ć‚‰ć“ćć€`stdio_client(...)`态`streamable_http_client(...)`态`sse_client(...)` ćÆć™ć¹ć¦åŒć˜å “ę‰€ć«åŽć¾ć‚Šć€č‡Ŗåˆ†ć§ē‹¬č‡Ŗć®ć‚‚ć®ć‚’ę›øćć“ćØć‚‚ć§ćć¾ć™ć€‚ ## まとめ {#recap} -* `Client(mcp)`ļ¼ˆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆļ¼‰ćÆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ęŽ„ē¶šć—ć¾ć™ć€‚ćƒ†ć‚¹ćƒˆćØēµ„ćæč¾¼ćæć«ä½æć£ć¦ćć ć•ć„ć€‚ * `Client("http://.../mcp")`(URLļ¼‰ćÆć€ęœ¬ē•Ŗē”Øć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚ć‚‹ Streamable HTTP ć§ęŽ„ē¶šć—ć¾ć™ć€‚ * ćƒ˜ćƒƒćƒ€ćƒ¼ć€čŖčØ¼ć€ćƒ—ćƒ­ć‚­ć‚·ć€ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆćÆć€`streamable_http_client(url, http_client=...)` に渔す `httpx2.AsyncClient` ć«čØ­å®šć—ć¾ć™ć€‚`headers=` ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ +* ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć«å¾“ć†ć®ćÆć€URL č‡Ŗčŗ«ć®ć‚ŖćƒŖć‚øćƒ³å†…ļ¼ˆęœ«å°¾ć‚¹ćƒ©ćƒƒć‚·ćƒ„ć® `307`/`308`ļ¼‰ćØć€åŒć˜ćƒ›ć‚¹ćƒˆäøŠć® `http`→`https` ć ć‘ć§ć™ć€‚ćć‚Œä»„å¤–ćÆ `Redirect to … not followed` ć§å¤±ę•—ć—ć¾ć™ć€‚ęœ€ēµ‚ēš„ćŖ URL ć‚’čØ­å®šć—ć¦ćć ć•ć„ć€‚ * stdio は `Client(StdioServerParameters(...))` ć§ć™ć€‚č‡Ŗåˆ†ć§ `stdio_client(...)` ć«åŒ…ć‚€ć®ćÆć€å­ćƒ—ćƒ­ć‚»ć‚¹ć® stderr ć‚’ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć—ćŸć„ćØćć ć‘ć§ć™ć€‚ * ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ćŒå—ć‘å–ć‚‹ć®ćÆč‡Ŗåˆ†ć®ē’°å¢ƒć§ćÆćŖćć€čØ±åÆćƒŖć‚¹ćƒˆć«åŸŗć„ćē’°å¢ƒć§ć™ć€‚`env=` ć§ćć“ć«čæ½åŠ ć—ć¾ć™ć€‚ +* `Client(mcp)`ļ¼ˆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆļ¼‰ćÆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ęŽ„ē¶šć—ć¾ć™ć€‚ćƒ†ć‚¹ćƒˆć§ä½æć†ć‹ć€ć‚µćƒ¼ćƒćƒ¼ć‚’ę§‹ēÆ‰ć—ćŸć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ć«ćć®ć‚µćƒ¼ćƒćƒ¼ć‚’ēµ„ćæč¾¼ć‚€ćŸć‚ć«ä½æć£ć¦ćć ć•ć„ć€‚ * ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćØćÆć€`async with x as (read, write)` と書けるものすべてです。`Client` ćÆć€ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć§ć‚‚ URL 恧悂 `StdioServerParameters` ć§ć‚‚ćŖć„ć‚‚ć®ć‚’ć€ćć®ć¾ć¾ć“ć®ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć«ęø”ć—ć¾ć™ć€‚ * `Client` ć®ę§‹ēÆ‰ć§ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćŒéøć°ć‚Œć€`async with` ć§ćć‚ŒćŒé–‹ć‹ć‚Œć¾ć™ć€‚ diff --git a/i18n/ja/pages/deprecated.md b/i18n/ja/pages/deprecated.md index 8754ecc8..15d9942f 100644 --- a/i18n/ja/pages/deprecated.md +++ b/i18n/ja/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # éžęŽØå„Øć®ę©Ÿčƒ½ {#deprecated-features} -2026-07-28 ć®ä»•ę§˜ć§ćÆć€5 ć¤ć®ć‚‚ć®ćŒå½¹ē›®ć‚’ēµ‚ćˆć¾ć™ć€‚SDK ćÆä»Šć‚‚ćć® 5 ć¤ć™ć¹ć¦ć‚’å®Ÿč£…ć—ć¦ćŠć‚Šć€ćć®ć™ć¹ć¦ć«**éžęŽØå„Øć®č­¦å‘Š**ćŒä»˜ćć‚ˆć†ć«ćŖć‚Šć¾ć—ćŸć€‚SDK ć®ćƒ˜ćƒ«ćƒ‘ćƒ¼ćŒ 1 ć¤ć€ä»•ę§˜ćØćÆåˆ„ć®ē†ē”±ć§éžęŽØå„Øć«ćŖć£ć¦ćŠć‚Šć€[ćƒšćƒ¼ć‚øć®ęœ€å¾Œ](#deprecated-sdk-helpers)ć«ęŒ™ć’ć¦ć„ć¾ć™ć€‚ +2026-07-28 ć®ä»•ę§˜ć§ćÆć€5 ć¤ć®ć‚‚ć®ćŒå½¹ē›®ć‚’ēµ‚ćˆć¾ć™ć€‚SDK ćÆä»Šć‚‚ćć® 5 ć¤ć™ć¹ć¦ć‚’å®Ÿč£…ć—ć¦ćŠć‚Šć€ćć®ć™ć¹ć¦ć«**éžęŽØå„Øć®č­¦å‘Š**ćŒä»˜ćć‚ˆć†ć«ćŖć‚Šć¾ć—ćŸć€‚ć“ć‚ŒćØćÆåˆ„ć«ć€SDK ē‹¬č‡Ŗć®ē†ē”±ć§éžęŽØå„Øć«ćŖć£ćŸć‚‚ć®ć‚‚ć„ćć¤ć‹ć‚ć‚Šć€[ćƒšćƒ¼ć‚øć®ęœ€å¾Œ](#deprecated-sdk-helpers)ć«ęŒ™ć’ć¦ć„ć¾ć™ć€‚ äø‹ć®č”ØćÆć€éžęŽØå„Øć«ćŖć£ćŸę©Ÿčƒ½ćć‚Œćžć‚Œć«ć¤ć„ć¦ć€ćŖććŖć‚‹ē†ē”±ćØć€ä»£ć‚ć‚Šć«åœŸå°ć«ć™ć¹ćć‚‚ć®ć‚’ęŒ™ć’ć¦ć„ć¾ć™ć€‚ @@ -125,11 +125,13 @@ API ćÆć“ć‚Œć ć‘ć§ć™ć€‚ćƒ”ć‚½ćƒƒćƒ‰ć”ćØć®ć‚¹ć‚¤ćƒƒćƒćÆć‚ć‚Šć¾ć›ć‚“ ## éžęŽØå„Øć® SDK ćƒ˜ćƒ«ćƒ‘ćƒ¼ {#deprecated-sdk-helpers} -ć“ć‚Œć‚‰ćÆä»•ę§˜ć®å¤‰ę›“ć§ćÆćŖćć€ć‚ˆć‚Šć‚ˆć„ä»£ę›æćŒć‚ć‚‹ SDK ć®å†…éƒØå®Ÿč£…ć«ć™ćŽć¾ć›ć‚“ć€‚åŒć˜ `MCPDeprecationWarning` ć§č­¦å‘Šć—ć€3.0 ć§å‰Šé™¤ć•ć‚Œć¾ć™ć€‚ +ć“ć‚Œć‚‰ćÆä»•ę§˜ć®å¤‰ę›“ć§ćÆćŖćć€ć‚ˆć‚Šć‚ˆć„ä»£ę›æćŒć‚ć‚‹ SDK ć®ä½æć„ę–¹ć«ć™ćŽć¾ć›ć‚“ć€‚åŒć˜ `MCPDeprecationWarning` ć§č­¦å‘Šć—ć€3.0 ć§å¤ć„å½¢å¼ćŒå‰Šé™¤ć•ć‚Œć¾ć™ć€‚ | éžęŽØå„Ø | ä»£ć‚ć‚Šć«ć™ć‚‹ć“ćØ | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` を呼んでから `FuncMetadata.call_fn()` ć‚’å‘¼ć³ć¾ć™ć€‚ć“ć‚Œć‚’å‘¼ć‚“ć§ć„ćŸć®ćÆć€`FuncMetadata` ć‚’ē›“ęŽ„ę‰±ć†ć‚³ćƒ¼ćƒ‰ļ¼ˆćŸćØćˆć°ē‹¬č‡Ŗć® `Tool` ć‚µćƒ–ć‚Æćƒ©ć‚¹ļ¼‰ć ć‘ć§ć™ć€‚ | +| `validate_token_resource=` ć‚’ęŒ‡å®šć—ćŖć„ `AuthSettings(resource_server_url=...)` | ęŒ‡å®šć—ć¦ćć ć•ć„ć€‚`True` ć«ć™ć‚‹ćØć€ćƒ™ćƒŖćƒ•ć‚”ć‚¤ć‚¢ćƒ¼ćŒ `resource_server_url` å‘ć‘ć«ē™ŗč”Œć•ć‚ŒćŸćØå ±å‘Šć—ćŖć„ćƒ™ć‚¢ćƒ©ćƒ¼ćƒˆćƒ¼ć‚Æćƒ³ć‚’ć‚µćƒ¼ćƒćƒ¼ćŒę‹’å¦ć—ć¾ć™ć€‚`False` ćÆć€ćƒ™ćƒŖćƒ•ć‚”ć‚¤ć‚¢ćƒ¼ćŒćƒˆćƒ¼ć‚Æćƒ³ć®ć‚Ŗćƒ¼ćƒ‡ć‚£ć‚Øćƒ³ć‚¹ć‚’č‡Ŗåˆ†ć§ę¤œčØ¼ć™ć‚‹ć“ćØć‚’č”Øć—ć¾ć™ļ¼ˆ**[čŖåÆ](run/authorization.md#a-token-verifier)** ć‚’å‚ē…§ļ¼‰ć€‚ęœŖęŒ‡å®šć®å “åˆćÆ `False` ćØć—ć¦ęŒÆć‚‹čˆžć„ć¾ć™ć€‚3.0 では、`resource_server_url` ćŒčØ­å®šć•ć‚Œć¦ć„ć‚‹ćØććÆåøøć« `True` ćŒćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć«ćŖć‚Šć¾ć™ć€‚ | +| `issuer=` ć‚’ęŒ‡å®šć—ćŖć„ `ClientCredentialsOAuthProvider(...)` または `PrivateKeyJWTOAuthProvider(...)` | č³‡ę ¼ęƒ…å ±ć‚’ē™ŗč”Œć—ćŸčŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’ęŒ‡ć™ `issuer=` ć‚’ęø”ć—ć¦ćć ć•ć„ļ¼ˆ**[OAuth ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ä½œęˆ](client/oauth-clients.md#machine-to-machine)** ć‚’å‚ē…§ļ¼‰ć€‚ęŒ‡å®šć—ćŖć„ćØć€ć©ć®čŖåÆć‚µćƒ¼ćƒćƒ¼ćŒč³‡ę ¼ęƒ…å ±ć‚’å—ć‘å–ć‚‹ć‹ć‚’ MCP ć‚µćƒ¼ćƒćƒ¼ćŒę±ŗć‚ć‚‹ć“ćØć«ćŖć‚Šć¾ć™ć€‚3.0 ć§ćÆć“ć®ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰ćŒåæ…é ˆć«ćŖć‚Šć¾ć™ć€‚ | ## まとめ {#recap} @@ -138,7 +140,7 @@ API ćÆć“ć‚Œć ć‘ć§ć™ć€‚ćƒ”ć‚½ćƒƒćƒ‰ć”ćØć®ć‚¹ć‚¤ćƒƒćƒćÆć‚ć‚Šć¾ć›ć‚“ * éžęŽØå„ØćÆå‹§å‘Šć«ć™ćŽć¾ć›ć‚“ć€‚é€šäæ”äøŠć®å¤‰ę›“ćÆćŖćć€2026 å¹“ć‚ˆć‚Šå‰ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«åÆ¾ć—ć¦ćÆć™ć¹ć¦ćŒå¼•ćē¶šćå‹•ä½œć—ć¾ć™ć€‚ćć—ć¦ē›®ć«č¦‹ćˆć‚‹ `MCPDeprecationWarning` ćŒå‡ŗć¾ć™ļ¼ˆ`UserWarning` ćŖć®ć§ć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ęœ‰åŠ¹ć§ć™ļ¼‰ć€‚ * ć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ćØćƒ«ćƒ¼ćƒˆć«ćÆć•ć‚‰ć«ć€2026-07-28 ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«ćÆćŖć„ćƒćƒƒć‚Æćƒćƒ£ćƒćƒ«ćŒåæ…č¦ć§ć™ć€‚ē¾č”Œä»•ę§˜ć®ęŽ„ē¶šć§ćÆč­¦å‘Šć‚’å‡ŗć—ć€ćć®ć‚ćØä¾‹å¤–ć‚’é€å‡ŗć—ć¾ć™ć€‚ * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` ć§ć‚«ćƒ†ć‚“ćƒŖå…Øä½“ć‚’é»™ć‚‰ć›ć¾ć™ć€‚pytest 恧 `"error::mcp.MCPDeprecationWarning"` ć‚’ęŒ‡å®šć™ć‚Œć°ć€ćƒ†ć‚¹ćƒˆć®å¤±ę•—ć«å¤‰ć‚ć‚Šć¾ć™ć€‚ -* SDK ć®ćƒ˜ćƒ«ćƒ‘ćƒ¼ `FuncMetadata.call_fn_with_arg_validation()` ćÆć€ć“ć‚ŒćØćÆåˆ„ć«éžęŽØå„Øć«ćŖć£ć¦ćŠć‚Šć€3.0 ć§å‰Šé™¤ć•ć‚Œć¾ć™ć€‚ +* [SDK ē‹¬č‡Ŗć®éžęŽØå„Ø](#deprecated-sdk-helpers)ć‚‚åŒć˜ćƒ«ćƒ¼ćƒ«ć«å¾“ć„ć¾ć™ć€‚ä»ŠćÆč­¦å‘Šć‚’å‡ŗć—ć€3.0 ć§å¤ć„å½¢å¼ćŒćŖććŖć‚Šć¾ć™ć€‚ * ę–°ć—ć„ć‚³ćƒ¼ćƒ‰ćÆć€ć“ć‚Œć‚‰ć®ć©ć‚Œć®äøŠć«ć‚‚ēÆ‰ćć¹ćć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć®ć»ć‹ć®ćƒšćƒ¼ć‚øćÆć™ć¹ć¦ć€ē¾č”Œć® API を扱っています。 diff --git a/i18n/ja/pages/get-started/first-steps.md b/i18n/ja/pages/get-started/first-steps.md index 01798d6d..d12904a7 100644 --- a/i18n/ja/pages/get-started/first-steps.md +++ b/i18n/ja/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # ęœ€åˆć®ć‚¹ćƒ†ćƒƒćƒ— {#first-steps} @@ -17,7 +17,7 @@ translation: * **ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ**ćÆćƒ›ć‚¹ćƒˆć®äø­ć«ć‚ć‚Šć€MCP ć‚’č©±ć—ć¾ć™ć€‚ćƒ›ć‚¹ćƒˆćÆć€ęŽ„ē¶šć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ć”ćØć«ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ 1 ć¤ćšć¤å‹•ć‹ć—ć¾ć™ć€‚ * **ć‚µćƒ¼ćƒćƒ¼**は、この SDK ć§ä½œć‚‹ć‚‚ć®ć§ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«åÆ¾ć—ć¦ä½•ć‹ć‚’å…¬é–‹ć—ć¾ć™ć€‚ćƒ¢ćƒ‡ćƒ«ćØē›“ęŽ„ć‚„ć‚Šå–ć‚Šć™ć‚‹ć“ćØćÆę±ŗć—ć¦ć‚ć‚Šć¾ć›ć‚“ć€‚ -č‡Ŗåˆ†ć§ę›øćć®ćÆć‚µćƒ¼ćƒćƒ¼ć§ć™ć€‚ćƒ›ć‚¹ćƒˆćÆåˆ„ć®čŖ°ć‹ćŒä½œć‚‹č£½å“ć§ć™ć€‚SDK には `Client` ć‚‚ē”Øę„ć•ć‚Œć¦ć„ć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć®ćƒ†ć‚¹ćƒˆć«ä½æć†ć‚‚ć®ć§ć€ć“ć®ćƒšćƒ¼ć‚øć®å¾ŒåŠć«ć‚‚ē™»å “ć—ć¾ć™ć€‚ +č‡Ŗåˆ†ć§ę›øćć®ćÆć‚µćƒ¼ćƒćƒ¼ć§ć™ć€‚ćƒ›ć‚¹ćƒˆćÆåˆ„ć®čŖ°ć‹ćŒä½œć‚‹č£½å“ć§ć™ć€‚SDK には `Client` ć‚‚ē”Øę„ć•ć‚Œć¦ć„ć¾ć™ć€‚ćƒ›ć‚¹ćƒˆćŒ URL ć§ć‚µćƒ¼ćƒćƒ¼ć«ęŽ„ē¶šć—ćŸć‚Šć€ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ćØć—ć¦čµ·å‹•ć—ćŸć‚Šć™ć‚‹ćØćć«ä½æć†ć®ćØåŒć˜ć‚Æćƒ©ć‚¹ć§ć™ć€‚ć“ć®ćƒšćƒ¼ć‚øć®å¾ŒåŠć«ē™»å “ć—ć¾ć™ć—ć€ć‚µćƒ¼ćƒćƒ¼ć‚’ćƒ†ć‚¹ćƒˆć™ć‚‹ę‰‹ę®µć«ć‚‚ćŖć‚Šć¾ć™ć€‚ ## 3 ć¤ć®ćƒ—ćƒŖćƒŸćƒ†ć‚£ćƒ– {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector ć«ćÆć‚æćƒ–ćŒ 3 ć¤ć‚ć‚Šć¾ć—ćŸć€‚3 つあると、どうやっ ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒęŽ„ē¶šć™ć‚‹ćØć€ć‚µćƒ¼ćƒćƒ¼ćÆč‡Ŗčŗ«ć®**ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£**ć‚’å®£čØ€ć—ć¾ć™ć€‚ć©ć®ē³»ēµ±ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«åæœē­”ć™ć‚‹ć‹ć€ćØć„ć†ć“ćØć§ć™ć€‚ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć“ć®å®£čØ€ć‚’ć‚‚ćØć«ć€ćć‚‚ćć‚‚ä½•ć‚’č¦ę±‚ć™ć‚‹ć‹ć‚’ę±ŗć‚ć¾ć™ć€‚ć“ć®å®£čØ€ć‚’č‡Ŗåˆ†ć§ę›øć„ć¦ćÆć„ć¾ć›ć‚“ć€‚`MCPServer` ćŒä»£ć‚ć‚Šć«å®£čØ€ć—ć¾ć™ć€‚ -č‡Ŗåˆ†ć®ē›®ć§ē¢ŗć‹ć‚ć¦ćæć¾ć—ć‚‡ć†ć€‚SDK 恮 `Client` ćÆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚’ćć®ć¾ć¾å—ć‘å–ć‚Šć€**ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖ**ć§ęŽ„ē¶šć—ć¾ć™ļ¼ˆć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ćƒćƒ¼ćƒˆć‚‚ä½æć„ć¾ć›ć‚“ļ¼‰ć€‚ +č‡Ŗåˆ†ć®ē›®ć§ē¢ŗć‹ć‚ć¦ćæć¾ć—ć‚‡ć†ć€‚1 ć¤ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć§ `server.py` 悒 HTTP ć§å®Ÿč”Œć—ćŸć¾ć¾ć«ć—ć¦ćŠćć¾ć™ć€‚ -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +ćć—ć¦åˆ„ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ćć“ć«å‘ć‘ć¾ć™ć€‚ -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) ć“ć“ć«ćŖć„ć‚‚ć®ć«ć‚‚ę³Øē›®ć—ć¦ćć ć•ć„ć€‚`completions`ļ¼ˆćƒŖć‚½ćƒ¼ć‚¹ćƒ†ćƒ³ćƒ—ćƒ¬ćƒ¼ćƒˆćØćƒ—ćƒ­ćƒ³ćƒ—ćƒˆć®å¼•ę•°ć®č‡Ŗå‹•č£œå®Œļ¼‰ć«ćÆč‡Ŗåˆ†ć§ę›øććƒćƒ³ćƒ‰ćƒ©ćƒ¼ćŒåæ…č¦ć§ć™ćŒć€ć“ć®ć‚µćƒ¼ćƒćƒ¼ć«ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ćć®ćŸć‚ć“ć®ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ćÆå®£čØ€ć•ć‚Œćšć€č”Œå„€ć®ć‚ˆć„ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŖć‚‰č¦ę±‚ć‚‚ć—ć¾ć›ć‚“ć€‚ć‚Ŗćƒ—ć‚·ćƒ§ćƒ³ć®ć‚‚ć®ćÆć™ć¹ć¦ć“ć®ćƒ«ćƒ¼ćƒ«ć«å¾“ć„ć¾ć™ć€‚ē™»éŒ²ć™ć‚Œć°ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ćŒē¾ć‚Œć¾ć™ć€‚**[補完](../servers/completions.md)** ć®ćƒšćƒ¼ć‚øćŒćć‚Œć‚’å®ŸčØ¼ć—ć¦ć„ć¾ć™ć€‚ !!! info - `Client(mcp)` ćÆć€ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć®ć™ć¹ć¦ć®ć‚µćƒ³ćƒ—ćƒ«ć‚’ćƒ†ć‚¹ćƒˆć—ć¦ć„ć‚‹ć®ćØåŒć˜ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć€č‡Ŗåˆ†ć®ć‚µćƒ¼ćƒćƒ¼ć‚’ćƒ†ć‚¹ćƒˆć™ć‚‹ćØćć«ć‚‚ć“ć‚Œć‚’ä½æć„ć¾ć™ć€‚ć¾ć‚‹ć”ćØ 1 ćƒšćƒ¼ć‚øć‚’å‰²ć„ć¦ć„ć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](testing.md)** 恧恙怂 + 恓恮 `client.py` ćÆå®Œå…ØćŖ MCP ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć€å°‚ē”Øć®ćƒšćƒ¼ć‚øćŒ **[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/index.md)** ć§ć™ć€‚ćƒ†ć‚¹ćƒˆć§ćÆć‚æćƒ¼ćƒŸćƒŠćƒ«ć‚‚ćƒćƒ¼ćƒˆć‚‚ēœćć€`Client` ć«ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ć‚’ęø”ć—ć¾ć™ć€‚`Client(mcp)` です。こごらにもまるごと 1 ćƒšćƒ¼ć‚øć‚’å‰²ć„ć¦ć„ć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](testing.md)** 恧恙怂 ## ę›øć‹ćŖć‹ć£ćŸć‚‚ć® {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema怂`a: int, b: int` ćŒćć®ć¾ć¾ `add` ć®ć‚¹ć‚­ćƒ¼ćƒžć§ć™ć€‚ * ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć€‚`tools/list`态`resources/read`态`prompts/get` ćÆć€ć™ć¹ć¦ä»£ć‚ć‚Šć«å‡¦ē†ć•ć‚Œć¾ć™ć€‚ * ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ć®å®£čØ€ć€‚`MCPServer` ćŒä»£ć‚ć‚Šć«ä½œć‚Šć¾ć—ćŸć€‚ -* ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®ć‚³ćƒ¼ćƒ‰ć‚’ 1 č”Œć‚‚ć€‚ćƒćƒ¼ć‚øćƒ§ćƒ³ć®ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć€JSON-RPC ć®ćƒ•ćƒ¬ćƒ¼ćƒŸćƒ³ć‚°ć€ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ć®äŗ¤ę›ćÆć€ć™ć¹ć¦ `mcp dev` と `Client(mcp)` ć®å†…éƒØć§č”Œć‚ć‚Œć€ē›®ć«ć™ć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć§ć—ćŸć€‚ +* ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®ć‚³ćƒ¼ćƒ‰ć‚’ 1 č”Œć‚‚ć€‚ćƒćƒ¼ć‚øćƒ§ćƒ³ć®ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć€JSON-RPC ć®ćƒ•ćƒ¬ćƒ¼ćƒŸćƒ³ć‚°ć€ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ć®äŗ¤ę›ćÆć€ć™ć¹ć¦ `mcp dev` と `client.py` ć®å†…éƒØć§č”Œć‚ć‚Œć€ē›®ć«ć™ć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć§ć—ćŸć€‚ ć“ć®ęÆ”ēŽ‡ć“ććŒć€ć“ć® SDK ć®å­˜åœØę„ē¾©ć§ć™ć€‚ @@ -134,6 +132,6 @@ asyncio.run(main()) * ćƒ‡ć‚³ćƒ¬ćƒ¼ć‚æćƒ¼ćÆćƒ—ćƒŖćƒŸćƒ†ć‚£ćƒ–ć”ćØć« 1 恤恧恙怂`@mcp.tool()`态`@mcp.resource(uri)`态`@mcp.prompt()`ć€‚åå‰ć€čŖ¬ę˜Žć€ć‚¹ć‚­ćƒ¼ćƒžćÆé–¢ę•°ć‹ć‚‰å–ć‚‰ć‚Œć¾ć™ć€‚ * `{param}` を含む URI ćÆćƒŖć‚½ćƒ¼ć‚¹**ćƒ†ćƒ³ćƒ—ćƒ¬ćƒ¼ćƒˆ**ć‚’ä½œć‚Šć€å…·ä½“ēš„ćŖćƒŖć‚½ćƒ¼ć‚¹ćØćÆåˆ„ć«äø€č¦§č”Øē¤ŗć•ć‚Œć¾ć™ć€‚ * ć‚µćƒ¼ćƒćƒ¼ć®**ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£**ćÆä»£ć‚ć‚Šć«å®£čØ€ć•ć‚Œć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć‚µćƒ¼ćƒćƒ¼ćŒå®£čØ€ć—ćŸć‚‚ć®ć ć‘ć‚’č¦ę±‚ć—ć¾ć™ć€‚ -* `Client(mcp)` ćÆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć«ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ęŽ„ē¶šć—ć¾ć™ć€‚åˆę—„ć‹ć‚‰ä½æćˆć‚‹ćƒ†ć‚¹ćƒˆćƒćƒ¼ćƒć‚¹ć§ć™ć€‚ +* `Client("http://localhost:8000/mcp")` ćÆå®Ÿč”Œäø­ć®ć‚µćƒ¼ćƒćƒ¼ćØć‚„ć‚Šå–ć‚Šć—ć¾ć™ć€‚ä»£ć‚ć‚Šć«ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚’ęø”ć—ć¦ `Client(mcp)` ćØć™ć‚Œć°ć€åˆę—„ć‹ć‚‰ä½æćˆć‚‹ćƒ†ć‚¹ćƒˆćƒćƒ¼ćƒć‚¹ć«ćŖć‚Šć¾ć™ć€‚ 欔は **[å®Ÿéš›ć®ćƒ›ć‚¹ćƒˆć«ęŽ„ē¶šć™ć‚‹](real-host.md)** ć§ć™ć€‚ć“ć®ć‚µćƒ¼ćƒćƒ¼ć‚’ Claude Desktop 悄 IDE ć®äø­ć§ć€ęœ¬å½“ć«å‹•ć‹ć—ć¾ć™ć€‚ćć®ę¬”ćÆ **[ćƒ†ć‚¹ćƒˆ](testing.md)** 恧恙怂1 ćƒšćƒ¼ć‚øć€ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ 1 ć¤ć§ć€å‹•ćć‹ć©ć†ć‹ć‚’å½“ć¦ęŽØé‡ć™ć‚‹ć“ćØćÆć‚‚ć†ć‚ć‚Šć¾ć›ć‚“ć€‚ćć®ć‚ćØćÆå„ćƒ—ćƒŖćƒŸćƒ†ć‚£ćƒ–ć«å°‚ē”Øć®ćƒšćƒ¼ć‚øćŒć‚ć‚Šć€ć¾ćšćÆćƒ¢ćƒ‡ćƒ«ćŒå‹•ć‹ć™ć‚‚ć®ć€**[ćƒ„ćƒ¼ćƒ«](../servers/tools.md)** ć‹ć‚‰å§‹ć¾ć‚Šć¾ć™ć€‚ diff --git a/i18n/ja/pages/get-started/testing.md b/i18n/ja/pages/get-started/testing.md index 2636eec9..2b962251 100644 --- a/i18n/ja/pages/get-started/testing.md +++ b/i18n/ja/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # ćƒ†ć‚¹ćƒˆ {#testing} -Python SDK には、**ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ**ć‚’å‚™ćˆćŸ `Client` ć‚Æćƒ©ć‚¹ćŒä»˜å±žć—ć¦ć„ć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚’ęø”ć›ć°ć€ćć®ć‚µćƒ¼ćƒćƒ¼ć«ē›“ęŽ„ęŽ„ē¶šć—ć¾ć™ć€‚ +SDK 恮 `Client` ć‚Æćƒ©ć‚¹ćÆć€URL ć«ęŽ„ē¶šć—ćŸć‚Šć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚’čµ·å‹•ć—ćŸć‚Šć™ć‚‹ć®ćØåŒć˜ć‚Æćƒ©ć‚¹ć§ć™ćŒć€**ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖ**ć§ć‚‚ęŽ„ē¶šć§ćć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚’ęø”ć›ć°ć€ćć®ć‚µćƒ¼ćƒćƒ¼ćØē›“ęŽ„ć‚„ć‚Šå–ć‚Šć—ć¾ć™ć€‚ -ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ć€ćƒćƒ¼ćƒˆć‚‚č¦ć‚Šć¾ć›ć‚“ć€‚ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć™ć‚‰ä½æć„ć¾ć›ć‚“ć€‚FastAPI 恮 `TestClient` ćØåŒć˜ē™ŗęƒ³ć§ć™ć€‚ +ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ć‚‚ć€ćƒćƒ¼ćƒˆć‚‚č¦ć‚Šć¾ć›ć‚“ć€‚é€šäæ”č·Æć«ćÆä½•ć‚‚ęµć‚Œć¾ć›ć‚“ć€‚FastAPI 恮 `TestClient` ćØåŒć˜ē™ŗęƒ³ć§ć™ć€‚ ## åŸŗęœ¬ēš„ćŖä½æć„ę–¹ {#basic-usage} @@ -86,10 +86,10 @@ async def test_call_add_tool(client: Client): ćƒ†ć‚¹ćƒˆć§ćÆęœ‰åŠ¹ć«ć—ćŸć¾ć¾ć«ć—ć¦ćć ć•ć„ć€‚ęœ¬ē•Ŗć‚³ćƒ¼ćƒ‰ć§ćÆę„å‘³ć‚’ęŒć”ć¾ć›ć‚“ć€‚ -## ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ {#in-process-by-default} +## ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆäø–ä»£ć‚’å•ć‚ćŖć„ {#era-neutral-by-default} !!! note - `Client(mcp)` ćÆć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ć§ęŽ„ē¶šć—ć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆ**ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®äø–ä»£ć‚’å•ć„ć¾ć›ć‚“**ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚’čŖæć¹ć€é©åˆ‡ćŖćƒ—ćƒ­ćƒˆć‚³ćƒ«ēµŒč·Æć‚’éøć³ć¾ć™ć€‚ćƒ†ć‚¹ćƒˆćŒćƒ¬ć‚¬ć‚·ćƒ¼å›ŗęœ‰ć®ć‚»ćƒžćƒ³ćƒ†ć‚£ć‚Æć‚¹ļ¼ˆć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ć‚„ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ļ¼ˆelicitationļ¼‰ć®ćƒ—ćƒƒć‚·ćƒ„ć€`message_handler`ļ¼‰ć‚’ę¤œčØ¼ć™ć‚‹å “åˆćÆ `mode="legacy"` ć«å›ŗå®šć—ć€ćć®å “åˆćÆ `raise_exceptions=True` ć‚’å¤–ć—ć¦ćć ć•ć„ć€‚ćƒ¬ć‚¬ć‚·ćƒ¼ęŽ„ē¶šćÆćć‚‚ćć‚‚ć‚µćƒ‹ć‚æć‚¤ć‚ŗć‚’č”Œć‚ćšć€ć“ć®ćƒ•ćƒ©ć‚°ć‚’ä»˜ć‘ć‚‹ćØå¤±ę•—ćŒćƒ†ć‚¹ćƒˆć®äø­ć§ćÆćŖćć‚µćƒ¼ćƒćƒ¼ć‚æć‚¹ć‚Æć®äø­ć§å†é€å‡ŗć•ć‚Œć¦ć—ć¾ć†ć‹ć‚‰ć§ć™ć€‚ + `Client(mcp)` ćÆć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ć§ęŽ„ē¶šć—ć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆ**ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć®äø–ä»£ć‚’å•ć„ć¾ć›ć‚“**ć€‚ć‚µćƒ¼ćƒćƒ¼ć‚’čŖæć¹ć€é©åˆ‡ćŖćƒ—ćƒ­ćƒˆć‚³ćƒ«ēµŒč·Æć‚’éøć³ć¾ć™ć€‚ćƒ†ć‚¹ćƒˆćŒćƒ¬ć‚¬ć‚·ćƒ¼å›ŗęœ‰ć®ć‚»ćƒžćƒ³ćƒ†ć‚£ć‚Æć‚¹ļ¼ˆć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ć‚„ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ļ¼ˆelicitationļ¼‰ć®ćƒ—ćƒƒć‚·ćƒ„ć€`message_handler`ļ¼‰ć‚’ę¤œčØ¼ć™ć‚‹å “åˆćÆ `mode="legacy"` ć«å›ŗå®šć—ć€ćć®ćØććÆ `raise_exceptions=True` ć‚’å¤–ć—ć¦ćć ć•ć„ć€‚ćƒ¬ć‚¬ć‚·ćƒ¼ęŽ„ē¶šćÆćć‚‚ćć‚‚ć‚µćƒ‹ć‚æć‚¤ć‚ŗć‚’č”Œć‚ćšć€ć“ć®ćƒ•ćƒ©ć‚°ć‚’ä»˜ć‘ć‚‹ćØå¤±ę•—ćŒćƒ†ć‚¹ćƒˆć®äø­ć§ćÆćŖćć‚µćƒ¼ćƒćƒ¼ć‚æć‚¹ć‚Æć®äø­ć§å†é€å‡ŗć•ć‚Œć¦ć—ć¾ć†ć‹ć‚‰ć§ć™ć€‚ 恓恮 1 č”Œć“ććŒć€ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆćŒć€ŒęŽ²č¼‰ć—ć¦ć„ć‚‹ä¾‹ćÆå‹•ćć€ćØē“„ęŸć§ćć‚‹ē†ē”±ć§ć‚‚ć‚ć‚Šć¾ć™ć€‚ć™ć¹ć¦ć®ć‚µćƒ³ćƒ—ćƒ«ćƒ•ć‚”ć‚¤ćƒ«ćÆ SDK č‡Ŗčŗ«ć®ćƒ†ć‚¹ćƒˆć‚¹ć‚¤ćƒ¼ćƒˆć§å®Ÿč”Œć•ć‚Œć¦ćŠć‚Šć€ćć®ć»ć¼ć™ć¹ć¦ćŒć¾ć•ć«ć“ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ēµŒē”±ć—ć¦ć„ć¾ć™ć€‚SDK ćŒč‡Ŗåˆ†č‡Ŗčŗ«ć«åÆ¾ć—ć¦ä½æć£ć¦ć„ć‚‹ć®ćØåŒć˜ćƒ„ćƒ¼ćƒ«ć‚’ä½æć£ć¦ć„ć‚‹ć‚ć‘ć§ć™ć€‚ diff --git a/i18n/ja/pages/handlers/multi-round-trip.md b/i18n/ja/pages/handlers/multi-round-trip.md index dc7bb0a4..20126801 100644 --- a/i18n/ja/pages/handlers/multi-round-trip.md +++ b/i18n/ja/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ļ¼ˆmulti-round-tripļ¼‰ćƒŖć‚Æć‚Øć‚¹ćƒˆ {#multi-round-trip-requests} @@ -162,7 +162,7 @@ TTLć€ćƒ—ćƒŖćƒ³ć‚·ćƒ‘ćƒ«ć®ęŸēø›ć€ćƒŖć‚Æć‚Øć‚¹ćƒˆć®ęŸēø›ćÆć‚³ćƒ¼ćƒ‡ćƒƒć‚Æ ## 2026-07-28 ć®ēµęžœåž‹ {#a-2026-07-28-result} -`InputRequiredResult` ćÆćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ **2026-07-28** ć«ć—ć‹å­˜åœØć—ć¾ć›ć‚“ć€‚ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć® `Client(server)` ćÆćć‚Œć‚’ä»£ć‚ć‚Šć«ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć—ć¦ćć‚Œć¾ć™ć€‚é€šäæ”č·Æč¶Šć—ć§ćÆ `mode="auto"` ćŒćć‚Œć‚’ę¤œå‡ŗć—ć¾ć™ć€‚ęŽ„ē¶šå¾Œć€`client.protocol_version` ć§ä½•ćŒå¾—ć‚‰ć‚ŒćŸć‹ćŒåˆ†ć‹ć‚Šć¾ć™ć€‚ +`InputRequiredResult` ćÆćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ **2026-07-28** ć«ć—ć‹å­˜åœØć—ć¾ć›ć‚“ć€‚`Client` ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć® `mode="auto"` ćÆć€ć©ć®ęŽ„ē¶šć§ć‚‚ćć‚Œć‚’ę¤œå‡ŗć—ć¾ć™ć€‚ęŽ„ē¶šå¾Œć€`client.protocol_version` ć§ä½•ćŒå¾—ć‚‰ć‚ŒćŸć‹ćŒåˆ†ć‹ć‚Šć¾ć™ć€‚ !!! warning 2026 ć‚ˆć‚Šå‰ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«ćÆ `InputRequiredResult` ć‚’å…„ć‚Œć‚‹å “ę‰€ćŒć‚ć‚Šć¾ć›ć‚“ć€‚`mode="legacy"` ć®ęŽ„ē¶šć§ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć‹ć‚‰ć“ć‚Œć‚’čæ”ć™ćØć€ćƒ©ćƒ³ćƒŠćƒ¼ćÆćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć•ć‚ŒćŸćƒćƒ¼ć‚øćƒ§ćƒ³ć«ć‚·ćƒŖć‚¢ćƒ©ć‚¤ć‚ŗć§ććšć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćÆ `-32603` *"Handler returned an invalid result"* ć‚Øćƒ©ćƒ¼ćŒčæ”ć‚Šć¾ć™ć€‚äø”ę–¹ć®äø–ä»£ć«åÆ¾åæœć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ćÆć€ć“ć‚Œć‚’ä½æć†å‰ć« `ctx.protocol_version` ć‚’ćƒć‚§ćƒƒć‚Æć—ćŖć‘ć‚Œć°ćŖć‚Šć¾ć›ć‚“ć€‚ diff --git a/i18n/ja/pages/handlers/progress.md b/i18n/ja/pages/handlers/progress.md index 6d695d1d..9729ced6 100644 --- a/i18n/ja/pages/handlers/progress.md +++ b/i18n/ja/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # é€²ę— {#progress} @@ -29,19 +29,17 @@ translation: ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€`call_tool` 恫 `progress_callback=` を渔すことで、**呼び出しごとに**ć‚Ŗćƒ—ćƒˆć‚¤ćƒ³ć—ć¾ć™ć€‚ -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,23 +54,27 @@ anyio.run(main) ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćÆ `async` é–¢ę•°ć§ć€ć‚µćƒ¼ćƒćƒ¼ćŒå ±å‘Šć—ćŸć‚‚ć®ć‚’ćć®ć¾ć¾å—ć‘å–ć‚Šć¾ć™ć€‚`progress`态`total`态`message` 恧恙怂 !!! info - `Client(mcp)` ćÆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć«ćƒ”ćƒ¢ćƒŖå†…ć§ē›“ęŽ„ęŽ„ē¶šć—ć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](../get-started/testing.md)** ć®ćƒšćƒ¼ć‚øć®åœŸå°ć«ćŖć£ć¦ć„ć‚‹ć®ćØåŒć˜ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć™ć€‚`progress_callback` は、`Client` ćŒć©ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć‚’ä½æć£ć¦ć„ć¦ć‚‚åŒć˜ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć§ć™ć€‚ć“ć‚Œć‹ć‚‰ē›®ć«ć™ć‚‹ć€Œć‚æć‚¤ćƒŸćƒ³ć‚°ć€ćÆćƒ”ćƒ¢ćƒŖå†…ęŽ„ē¶šć®ć‚‚ć®ć§ć™ć€‚ćƒ”ćƒ¢ćƒŖå†…ęŽ„ē¶šćÆć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æć‚’ć‚¤ćƒ³ćƒ©ć‚¤ćƒ³ć§å®Ÿč”Œć™ć‚‹ćŸć‚ć€ć™ć¹ć¦ć®å ±å‘ŠćŒ `call_tool` ćŒčæ”ć‚‹å‰ć«å±Šćć¾ć™ć€‚å®Ÿéš›ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ćÆé€šēŸ„ćØēµęžœć®åˆ°ē€é †ćÆäæčØ¼ć•ć‚Œćšć€é…ć„ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćÆ `call_tool` ćŒčæ”ć£ćŸć‚ćØć‚‚ć¾ć å®Ÿč”Œäø­ć®ć“ćØćŒć‚ć‚Šć¾ć™ć€‚ + `progress_callback` は、`Client` ć«ä½•ć‚’ęø”ć—ćŸć‹ć«ć‹ć‹ć‚ć‚‰ćšåŒć˜ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć§ć™ć€‚ć“ć“ć§ć®ć‚ˆć†ćŖ URL 恧悂态`StdioServerParameters` ć§ć‚‚ć€ćƒ†ć‚¹ćƒˆć§ć®ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć§ć‚‚å¤‰ć‚ć‚Šć¾ć›ć‚“ć€‚ćŸć ć—ć€å®Ÿéš›ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ćÆć‚æć‚¤ćƒŸćƒ³ć‚°ć«ę³Øę„ć—ć¦ćć ć•ć„ć€‚é€šēŸ„ćÆćć‚Œćžć‚Œćƒ¬ć‚¹ćƒćƒ³ć‚¹ćØćÆåˆ„ć«å˜ē‹¬ć§å±ŠććŸć‚ć€é…ć„ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćÆ `call_tool` ćŒčæ”ć£ćŸć‚ćØć‚‚ć¾ć å®Ÿč”Œäø­ć®ć“ćØćŒć‚ć‚Šć¾ć™ć€‚ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æć‚’ć‚¤ćƒ³ćƒ©ć‚¤ćƒ³ć§å®Ÿč”Œć—ć€ć™ć¹ć¦ć®å ±å‘ŠćŒå…ˆć«å±Šćć“ćØć‚’äæčØ¼ć™ć‚‹ć®ćÆć€ćƒ—ćƒ­ć‚»ć‚¹å†…ć®ćƒ†ć‚¹ćƒˆęŽ„ē¶šć ć‘ć§ć™ć€‚ ### 試してみる {#try-it} -`client.py` 悒 `server.py` ć®éš£ć«ē½®ć„ć¦ć€å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ +`server.py` 悒 HTTP ć§é…äæ”ć—ć€åˆ„ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -ć‚µćƒ¼ćƒćƒ¼å“ć® `await ctx.report_progress(...)` ćÆćć‚Œćžć‚Œć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć§é †ē•Ŗć©ćŠć‚Šć« `show` 恮 1 å›žć®å‘¼ć³å‡ŗć—ć«ćŖć‚Šć€2 č”ŒćØć‚‚ `call_tool` ćŒčæ”ć‚‹**å‰ć«**å‡ŗåŠ›ć•ć‚Œć¾ć—ćŸć€‚é€²ę—ćÆēµęžœć«ć¾ćØć‚ć‚‰ć‚Œć‚‹ć®ć§ćÆćŖćć€ćƒ„ćƒ¼ćƒ«ćŒć¾ć å‹•ć„ć¦ć„ć‚‹é–“ć«ć‚¹ćƒˆćƒŖćƒ¼ćƒŸćƒ³ć‚°ć•ć‚Œć¾ć™ć€‚ +ć‚µćƒ¼ćƒćƒ¼å“ć® `await ctx.report_progress(...)` ćÆćć‚Œćžć‚Œć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć§é †ē•Ŗć©ćŠć‚Šć« `show` 恮 1 å›žć®å‘¼ć³å‡ŗć—ć«ćŖć‚Šć¾ć—ćŸć€‚é€²ę—ćÆēµęžœć«ć¾ćØć‚ć‚‰ć‚Œć‚‹ć®ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ćƒ„ćƒ¼ćƒ«ćŒć¾ć å‹•ć„ć¦ć„ć‚‹é–“ć«ć‚¹ćƒˆćƒŖćƒ¼ćƒŸćƒ³ć‚°ć•ć‚Œć¾ć™ć€‚ !!! warning `progress_callback` は `Client` ć§ćÆćŖćć€**呼び出し**ć«å±žć—ć¾ć™ć€‚ćć®ćŸć‚ć®ć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æćƒ¼å¼•ę•°ćÆć‚ć‚Šć¾ć›ć‚“ć€‚å‘¼ć³å‡ŗć—ć”ćØć«åæ…č¦ćŖć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒé•ć†ć‹ć‚‰ć§ć™ć€‚ć‚ć‚‹å‘¼ć³å‡ŗć—ćÆćƒ€ć‚¦ćƒ³ćƒ­ćƒ¼ćƒ‰ćƒćƒ¼ć‚’å‹•ć‹ć—ć€ę¬”ć®å‘¼ć³å‡ŗć—ćÆćƒ­ć‚°ć® 1 č”Œć‚’å‡ŗć—ć¾ć™ć€‚ diff --git a/i18n/ja/pages/protocol-versions.md b/i18n/ja/pages/protocol-versions.md index 1e2a6b51..8eb5f6bf 100644 --- a/i18n/ja/pages/protocol-versions.md +++ b/i18n/ja/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # ćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ {#protocol-versions} @@ -11,9 +11,17 @@ MCP には 2 ć¤ć®äø–ä»£ćŒć‚ć‚Šć¾ć™ć€‚ `Client` ćŒä»£ć‚ć‚Šć«ćƒć‚“ć‚·ć‚Øćƒ¼ć‚·ćƒ§ćƒ³ć—ć¦ćć‚Œć‚‹ć®ć§ć€ę°—ć«ć™ć‚‹åæ…č¦ćÆć»ćØć‚“ć©ć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć®ćƒšćƒ¼ć‚øć§ę‰±ć†ć®ćÆć€ćć‚Œć‚’åˆ¶å¾”ć™ć‚‹ćŸć£ćŸ 1 ć¤ć®ć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æćƒ¼å¼•ę•° `mode=` ćØć€ćć‚Œć‚’å¤‰ę›“ć™ć‚‹ 3 ć¤ć®å “é¢ć§ć™ć€‚ +ć“ć®ćƒšćƒ¼ć‚øć®ć‚¹ćƒ‹ćƒšćƒƒćƒˆćÆć©ć‚Œć‚‚ć€**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](client/index.md)** ć«å‡ŗć¦ćć‚‹ Bookshop 恮 `server.py` ćØé€šäæ”ć™ć‚‹ `client.py` ć§ć™ć€‚ć¾ćš 1 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć§ćć®ć‚µćƒ¼ćƒćƒ¼ć‚’čµ·å‹•ć—ć¦ćć ć•ć„ć€‚ + +```console +uv run mcp run server.py --transport streamable-http +``` + +ꬔ恫态2 ć¤ē›®ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć§å„ć‚¹ćƒ‹ćƒšćƒƒćƒˆć‚’ `python client.py` ć§å®Ÿč”Œć—ć¾ć™ć€‚ + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,11 +39,11 @@ MCP には 2 ć¤ć®äø–ä»£ćŒć‚ć‚Šć¾ć™ć€‚ ę©Ÿčƒ½ćØć—ć¦ćÆć“ć‚Œć ć‘ć§ć™ć€‚`Client` は 1 ć¤ć€ć‚µćƒ¼ćƒćƒ¼ćÆć©ć®äø–ä»£ć§ć‚‚ć‚ˆćć€ć‚³ćƒ¼ćƒ‰ć«åˆ†å²ćÆč¦ć‚Šć¾ć›ć‚“ć€‚ !!! info - `MCPServer` ćÆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć€stdio态Streamable HTTP ć®ć©ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚‚ `server/discover` ć«åæœē­”ć—ć¾ć™ć€‚ćć®ćŸć‚ć€č‡Ŗåˆ†ć®ć‚µćƒ¼ćƒćƒ¼ćŒē›øę‰‹ćŖć‚‰ `auto` ćÆåæ…ćš `2026-07-28` ć«ćŖć‚Šć¾ć™ć€‚ćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒē™ŗå‹•ć™ć‚‹ć®ćÆ 2026 å¹“ć‚ˆć‚Šå‰ć®ęœ¬ē‰©ć®ć‚µćƒ¼ćƒćƒ¼ćŒē›øę‰‹ć®ćØćć ć‘ć§ć€ćć‚ŒćÆć¾ć•ć«ćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚Æć—ć¦ć»ć—ć„å “é¢ć§ć™ć€‚ + `MCPServer` は Streamable HTTP态stdioć€ćć—ć¦ćƒ†ć‚¹ćƒˆć§ä½æć†ć‚¤ćƒ³ćƒ—ćƒ­ć‚»ć‚¹ęŽ„ē¶šć®ć©ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚‚ `server/discover` ć«åæœē­”ć—ć¾ć™ć€‚ćć®ćŸć‚ć€č‡Ŗåˆ†ć®ć‚µćƒ¼ćƒćƒ¼ćŒē›øę‰‹ćŖć‚‰ `auto` ćÆåæ…ćš `2026-07-28` ć«ćŖć‚Šć¾ć™ć€‚ćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚ÆćŒē™ŗå‹•ć™ć‚‹ć®ćÆ 2026 å¹“ć‚ˆć‚Šå‰ć®ęœ¬ē‰©ć®ć‚µćƒ¼ćƒćƒ¼ćŒē›øę‰‹ć®ćØćć ć‘ć§ć€ćć‚ŒćÆć¾ć•ć«ćƒ•ć‚©ćƒ¼ćƒ«ćƒćƒƒć‚Æć—ć¦ć»ć—ć„å “é¢ć§ć™ć€‚ ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP には 2 ć¤ć®äø–ä»£ćŒć‚ć‚Šć¾ć™ć€‚ `mode` ć«ćÆę–°äø–ä»£ć®ćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ę–‡å­—åˆ—ć‚‚ęŒ‡å®šć§ćć¾ć™ć€‚ē¾ę™‚ē‚¹ć§ćć®é›†åˆćÆć”ć‚‡ć†ć© `["2026-07-28"]` 恧恙怂 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 ćŖć®ć§å–ć£ć¦ćŠćć¾ć—ć‚‡ć†ć€‚`auto` ć§ęŽ„ē¶šć—ćŸå¾Œć€`client.session.discover_result` ć«ćÆć‚µćƒ¼ćƒćƒ¼ćŒé€ć£ćŸ `DiscoverResult` ćŒćć®ć¾ć¾å…„ć£ć¦ć„ć¾ć™ć€‚`supported_versions`态`capabilities`态`instructions`ć€ćć—ć¦ć‚µćƒ¼ćƒćƒ¼ćŒēµęžœć® `_meta` ć«åˆ»ć‚“ć č­˜åˆ„ęƒ…å ±ć§ć™ć€‚ę¬”å›žćÆćć‚Œć‚’ `prior_discover=` として渔します。 -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ja/pages/run/asgi.md b/i18n/ja/pages/run/asgi.md index f49b8864..7853bd4d 100644 --- a/i18n/ja/pages/run/asgi.md +++ b/i18n/ja/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # ę—¢å­˜ć®ć‚¢ćƒ—ćƒŖć«ēµ„ćæč¾¼ć‚€ {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette 恮 `Host` ćƒ«ćƒ¼ćƒˆć‚‚åŒć˜ć‚ˆć†ć«å‹•ćć¾ć™ć€‚`Mount("/", ...)` --8<-- "docs_src/asgi/tutorial004.py" ``` -ć“ć‚Œć§ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆ `/notes/mcp` ć§ćÆćŖć `/notes` ć«ęŽ„ē¶šć—ć¾ć™ć€‚ +ć“ć‚Œć§ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆ `/notes/mcp` ć§ćÆćŖć `/notes/` ć«ęŽ„ē¶šć—ć¾ć™ć€‚ ## ćƒ–ćƒ©ć‚¦ć‚¶ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå‘ć‘ć® CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette 恮 `Host` ćƒ«ćƒ¼ćƒˆć‚‚åŒć˜ć‚ˆć†ć«å‹•ćć¾ć™ć€‚`Mount("/", ...)` * ćƒ–ćƒ©ć‚¦ć‚¶ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćÆ CORS ćŒåæ…č¦ć§ć™ć€‚`Mcp-*` ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒ˜ćƒƒćƒ€ćƒ¼ć®ćŸć‚ć® `allow_headers` ćØć€ćƒ¬ć‚¹ćƒćƒ³ć‚¹ć®ćŸć‚ć® `expose_headers=["Mcp-Session-Id"]` 恧恙怂 * `@mcp.custom_route()` ćÆć€čŖčØ¼ćŖć—ć®ē“ ć® HTTP ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć‚’ `/mcp` ć®éš£ć«čæ½åŠ ć—ć¾ć™ć€‚ -ć‚µćƒ¼ćƒćƒ¼ć«å®Ÿéš›ć® URL ć§åˆ°é”ć§ćć‚‹ć‚ˆć†ć«ćŖć£ćŸć‚‰ć€**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/index.md)** ćÆć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć®ä»£ć‚ć‚Šć«ćć® URL ć‚’ä½æć£ć¦ęŽ„ē¶šć—ć¾ć™ć€‚ +ć‚µćƒ¼ćƒćƒ¼ć«å®Ÿéš›ć® URL ć§åˆ°é”ć§ćć‚‹ć‚ˆć†ć«ćŖć£ćŸć‚‰ć€**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/index.md)** ćÆćć® URL ć‚’ä½æć£ć¦ć‚µćƒ¼ćƒćƒ¼ć«ęŽ„ē¶šć—ć¾ć™ć€‚ diff --git a/i18n/ja/pages/run/authorization.md b/i18n/ja/pages/run/authorization.md index a1795ada..df1f0470 100644 --- a/i18n/ja/pages/run/authorization.md +++ b/i18n/ja/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # čŖåÆ {#authorization} @@ -23,12 +23,12 @@ OAuth ć®ē”ØčŖžć§ć„ćˆć°ć€ć‚µćƒ¼ćƒćƒ¼ćÆ**ćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼**恧恙 ęœ‰åŠ¹ćŖćƒˆćƒ¼ć‚Æćƒ³ćŒć©ć‚“ćŖå½¢ć‚’ć—ć¦ć„ć‚‹ć‹ć«ć¤ć„ć¦ć€SDK ćÆä½•ć®å‰ęć‚‚ęŒć”ć¾ć›ć‚“ć€‚**`TokenVerifier`** ć‚’å®Ÿč£…ć—ć¦ć€ć“ć”ć‚‰ć‹ć‚‰ä¼ćˆć¾ć™ć€‚ -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` ćÆéžåŒęœŸćƒ”ć‚½ćƒƒćƒ‰ć‚’ 1 ć¤ć ć‘ęŒć¤ćƒ—ćƒ­ćƒˆć‚³ćƒ«ć§ć™ć€‚`verify_token` は `Authorization` ćƒ˜ćƒƒćƒ€ćƒ¼ć‹ć‚‰å–ć‚Šå‡ŗć—ćŸē”Ÿć®ćƒˆćƒ¼ć‚Æćƒ³ć‚’å—ć‘å–ć‚Šć€ęœ‰åŠ¹ćŖć‚‰ **`AccessToken`** ć‚’ć€ē„”åŠ¹ćŖć‚‰ `None` ć‚’čæ”ć—ć¾ć™ć€‚å®Ÿč£…ć™ć‚‹ć‚‚ć®ćÆć»ć‹ć«ć‚ć‚Šć¾ć›ć‚“ć€‚ -* ć“ć®ä¾‹ć§ćÆćƒˆćƒ¼ć‚Æćƒ³ć‚’ćƒ†ćƒ¼ćƒ–ćƒ«ć‹ć‚‰å¼•ć„ć¦ć„ć¾ć™ć€‚å®Ÿéš›ć®ć‚‚ć®ćÆ JWT ć®ē½²åć‚’ę¤œčØ¼ć™ć‚‹ć‹ć€čŖåÆć‚µćƒ¼ćƒćƒ¼ć®ćƒˆćƒ¼ć‚Æćƒ³ć‚¤ćƒ³ćƒˆćƒ­ć‚¹ćƒšć‚Æć‚·ćƒ§ćƒ³ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć‚’å‘¼ć³å‡ŗć—ć¾ć™ć€‚ćć®ć‚³ćƒ¼ćƒ‰ćÆč‡Ŗåˆ†ć§ę›øćć¾ć™ć€‚SDK ćÆćć‚Œć‚’å‘¼ć³å‡ŗć™ć ć‘ć§ć™ć€‚ +* ć“ć®ä¾‹ć§ćÆćƒˆćƒ¼ć‚Æćƒ³ć‚’ćƒ†ćƒ¼ćƒ–ćƒ«ć‹ć‚‰å¼•ć„ć¦ć„ć¾ć™ć€‚å„ć‚Øćƒ³ćƒˆćƒŖć«ćÆć€ćć®ćƒˆćƒ¼ć‚Æćƒ³ćŒć©ć®ćƒŖć‚½ćƒ¼ć‚¹å‘ć‘ć«ē™ŗč”Œć•ć‚ŒćŸć‹ćŒčØ˜éŒ²ć•ć‚Œć¦ć„ć¾ć™ć€‚å®Ÿéš›ć®ć‚‚ć®ćÆ JWT ć®ē½²åć‚’ę¤œčØ¼ć™ć‚‹ć‹ć€čŖåÆć‚µćƒ¼ćƒćƒ¼ć®ćƒˆćƒ¼ć‚Æćƒ³ć‚¤ćƒ³ćƒˆćƒ­ć‚¹ćƒšć‚Æć‚·ćƒ§ćƒ³ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć‚’å‘¼ć³å‡ŗć—ć€ćƒˆćƒ¼ć‚Æćƒ³ćŒčŖ°å‘ć‘ć«ē™ŗč”Œć•ć‚ŒćŸć‹ļ¼ˆ`aud`)を `AccessToken.resource` ć§å ±å‘Šć—ć¾ć™ć€‚ćć®ć‚³ćƒ¼ćƒ‰ćÆč‡Ŗåˆ†ć§ę›øćć¾ć™ć€‚SDK ćÆćć‚Œć‚’å‘¼ć³å‡ŗć™ć ć‘ć§ć™ć€‚ * `token_verifier=` と `auth=` ćÆåæ…ćšć‚»ćƒƒćƒˆć§ęø”ć—ć¾ć™ć€‚ē‰‡ę–¹ć ć‘ęø”ć™ćØć€`MCPServer(...)` ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ 1 つも処理しないうごに `ValueError` を送出します。 `AuthSettings` ćÆćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼ć®č”Øå‘ćć®é””ć§ć™ć€‚ @@ -36,6 +36,10 @@ OAuth ć®ē”ØčŖžć§ć„ćˆć°ć€ć‚µćƒ¼ćƒćƒ¼ćÆ**ćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼**恧恙 * `issuer_url`ļ¼šćƒˆćƒ¼ć‚Æćƒ³ć‚’ē™ŗč”Œć™ć‚‹čŖåÆć‚µćƒ¼ćƒćƒ¼ć€‚ * `resource_server_url`ļ¼šć“ć® MCP ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć®å…¬é–‹ URLć€‚ćƒˆćƒ¼ć‚Æćƒ³ćŒć€Œć©ć®ć€ćƒŖć‚½ćƒ¼ć‚¹å‘ć‘ć‹ć‚’ē¤ŗć™åå‰ć§ć‚ć‚Šć€ćƒ‡ć‚£ć‚¹ć‚«ćƒćƒŖćƒ¼ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆćŒē½®ć‹ć‚Œć‚‹å “ę‰€ć§ć‚‚ć‚ć‚Šć¾ć™ć€‚ * `required_scopes`ļ¼šć™ć¹ć¦ć®ćƒˆćƒ¼ć‚Æćƒ³ćŒć“ć‚Œć‚‰ć‚’ć™ć¹ć¦ęŒć£ć¦ć„ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚ +* `validate_token_resource`:`AccessToken.resource` が `resource_server_url` ć§ćŖć„ćƒˆćƒ¼ć‚Æćƒ³ć‚’ć™ć¹ć¦ę‹’å¦ć—ć¾ć™ć€‚`resource_server_url` ć‚’čØ­å®šć—ćŸć¾ć¾ć“ć‚Œć‚’ęœŖčØ­å®šć«ć™ć‚‹ćØč­¦å‘Šļ¼ˆ`MCPDeprecationWarning`ļ¼‰ćŒå‡ŗć¦ć€`False` ćØć—ć¦å‹•ä½œć—ć¾ć™ć€‚3.0 ć§ćÆćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćŒ `True` ć«ćŖć‚Šć¾ć™ć€‚ + * čŖåÆć‚µćƒ¼ćƒćƒ¼ćŒć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®č¦ę±‚ć—ćŸ `resource` ć«ćƒˆćƒ¼ć‚Æćƒ³ć‚’ēµć³ä»˜ć‘ć‚‹å “åˆćÆęœ‰åŠ¹ć«ć—ć¦ćć ć•ć„ć€‚MCP ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć“ć‚Œć‚’åøøć«é€ć‚Šć¾ć™ć€‚`resource_server_url` ćÆć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒęŽ„ē¶šć™ć‚‹ URL ćØę­£ē¢ŗć«äø€č‡“ć•ć›ć¦ćć ć•ć„ć€‚ + * čŖåÆć‚µćƒ¼ćƒćƒ¼ćŒē‹¬č‡Ŗć®ć‚Ŗćƒ¼ćƒ‡ć‚£ć‚Øćƒ³ć‚¹č­˜åˆ„å­ļ¼ˆAuth0 恮 API č­˜åˆ„å­ć€Entra ć®ć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ IDļ¼‰ć‚’ä½æć†å “åˆćÆē„”åŠ¹ć®ć¾ć¾ć«ć—ć€ä»£ć‚ć‚Šć«ćƒ™ćƒŖćƒ•ć‚”ć‚¤ć‚¢ćƒ¼ć®äø­ć§ `aud` ć‚’ē¢ŗčŖć—ć¦ć€ć“ć®ć‚µćƒ¼ćƒćƒ¼å‘ć‘ć§ćŖć„ćƒˆćƒ¼ć‚Æćƒ³ć«ćÆ `None` ć‚’čæ”ć—ć¦ćć ć•ć„ć€‚ + * `aud` ćŒćƒŖć‚¹ćƒˆć®å “åˆćÆć€`resource_server_url` ćØē­‰ć—ć„ć‚Øćƒ³ćƒˆćƒŖć‚’ `resource` ć«å…„ć‚Œć¦ćć ć•ć„ć€‚ !!! tip SDK ćƒŖćƒć‚øćƒˆćƒŖć® `examples/servers/simple-auth/` ć«ćÆć€å®Ÿéš›ć®čŖåÆć‚µćƒ¼ćƒćƒ¼ć® [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć‚’å‘¼ć³å‡ŗć™ `IntrospectionTokenVerifier` ćŒć‚ć‚Šć¾ć™ć€‚ęœ¬ē•Ŗē”Øć®ćƒ™ćƒŖćƒ•ć‚”ć‚¤ć‚¢ćƒ¼ć®å¤šććÆć“ć®å½¢ć«ćŖć‚Šć¾ć™ć€‚ @@ -85,7 +89,7 @@ OAuth ć®ē”ØčŖžć§ć„ćˆć°ć€ć‚µćƒ¼ćƒćƒ¼ćÆ**ćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼**恧恙 ć©ć®ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć®äø­ć§ć‚‚ć€**`get_access_token()`** ćÆē¾åœØć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«åÆ¾ć—ć¦ćƒ™ćƒŖćƒ•ć‚”ć‚¤ć‚¢ćƒ¼ćŒčæ”ć—ćŸ `AccessToken` 恧恙怂 -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK ćŒęä¾›ć™ć‚‹ć®ćÆćƒŖć‚½ćƒ¼ć‚¹ć‚µćƒ¼ćƒćƒ¼ć®åŠåˆ†ć€ć¤ć¾ć‚Šę¤œčØ¼ć€ * `token_verifier=` と `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` ćÆåæ…ćšć‚»ćƒƒćƒˆć§ęø”ć—ć¾ć™ć€‚ * SDK は [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata 悒 `/.well-known/oauth-protected-resource/...` ć§å…¬é–‹ć—ć€ęœŖčŖčØ¼ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«ćÆć€ćć“ć‚’ęŒ‡ć™ `WWW-Authenticate` ćƒ˜ćƒƒćƒ€ćƒ¼ä»˜ćć® 401 ć§åæœē­”ć—ć¾ć™ć€‚ćƒ‡ć‚£ć‚¹ć‚«ćƒćƒŖćƒ¼ć®ä»•ēµ„ćæćÆć“ć‚Œć ć‘ć§ć™ć€‚ * ć©ć®ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć§ć‚‚ć€`get_access_token()` ć‚’å‘¼ć¹ć°čŖ°ćŒå‘¼ć³å‡ŗć—ć¦ć„ć‚‹ć‹ćŒć‚ć‹ć‚Šć¾ć™ć€‚ -* čŖåÆćÆ HTTP ć®é–¢åæƒäŗ‹ć§ć™ć€‚`stdio` ćØć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćć‚Œć‚’ē›®ć«ć™ć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć€‚ +* čŖåÆćÆ HTTP ć®é–¢åæƒäŗ‹ć§ć™ć€‚`stdio` ćØć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ćƒ†ć‚¹ćƒˆć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćć‚Œć‚’ē›®ć«ć™ć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć€‚ ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®åŠåˆ†ļ¼ˆčŖåÆć‚µćƒ¼ćƒćƒ¼ć‚’č¦‹ć¤ć‘ć¦ćƒˆćƒ¼ć‚Æćƒ³ć‚’å–å¾—ć—ć¦ćć‚Œć‚‹éƒØåˆ†ļ¼‰ć«ć¤ć„ć¦ćÆć€**[OAuth ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆ](../client/oauth-clients.md)**ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ćć—ć¦ć€ćƒ¦ćƒ¼ć‚¶ćƒ¼ć«å°‹ć­ć‚‹ä»£ć‚ć‚Šć« ID ć‚’ć€Œć‚¢ć‚µćƒ¼ćƒˆć€ć™ć‚‹ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ć¤ć„ć¦ćÆć€**[ID ć‚¢ć‚µćƒ¼ć‚·ćƒ§ćƒ³](../client/identity-assertion.md)**ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ diff --git a/i18n/ja/pages/run/deploy.md b/i18n/ja/pages/run/deploy.md index dd665f9e..6a1529a2 100644 --- a/i18n/ja/pages/run/deploy.md +++ b/i18n/ja/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # ćƒ‡ćƒ—ćƒ­ć‚¤ćØć‚¹ć‚±ćƒ¼ćƒ« {#deploy-scale} @@ -42,6 +42,22 @@ translation: ć“ć®ę–‡čØ€ćÆć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć§ćÆč¦‹ć¤ć‹ć‚Šć¾ć›ć‚“ć€‚`421` は JSON-RPC ć‚Øćƒ©ćƒ¼ć§ćÆćŖććƒ—ćƒ¬ćƒ¼ćƒ³ćƒ†ć‚­ć‚¹ćƒˆć® HTTP ćƒ¬ć‚¹ćƒćƒ³ć‚¹ćŖć®ć§ć€MCP ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆę±Žē”Øēš„ćŖćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć‚Øćƒ©ćƒ¼ć‚’é€å‡ŗć—ć¾ć™ć€‚ę°—ć«å…„ć‚‰ćŖć‹ć£ćŸćƒ›ć‚¹ćƒˆåćÆ**ć‚µćƒ¼ćƒćƒ¼**ć®ćƒ­ć‚°ć«ć€č­¦å‘ŠćØć—ć¦ 1 č”Œå‡ŗć‚‹ć ć‘ć§ć™ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ć—ćŸć°ć‹ć‚Šć®ć‚µćƒ¼ćƒćƒ¼ćŒć™ć¹ć¦ć®ęŽ„ē¶šć‚’ę‹’å¦ć™ć‚‹ćŖć‚‰ć€ćć†ć§ćŖć„ćØčØ¼ę˜Žć•ć‚Œć‚‹ć¾ć§ćÆ Host ć®čØ±åÆćƒŖć‚¹ćƒˆćŒåŽŸå› ć§ć™ć€‚**[ćƒˆćƒ©ćƒ–ćƒ«ć‚·ćƒ„ćƒ¼ćƒ†ć‚£ćƒ³ć‚°](../troubleshooting.md)** ć‚‚ć“ć“ć‹ć‚‰å§‹ć¾ć‚Šć¾ć™ć€‚ +## TLS ēµ‚ē«Æćƒ—ćƒ­ć‚­ć‚·ć®čƒŒå¾Œć§ {#behind-a-tls-terminating-proxy} + +TLS ćŒćƒ—ćƒ­ć‚­ć‚·ļ¼ˆć‚¤ćƒ³ć‚°ćƒ¬ć‚¹ć€ćƒ­ćƒ¼ćƒ‰ćƒćƒ©ćƒ³ć‚µćƒ¼ć€Caddy态nginxļ¼‰ć§ēµ‚ē«Æć—ć€ćć®čƒŒå¾Œć§ uvicorn ćŒå¹³ę–‡ć® HTTP ć‚’é…äæ”ć™ć‚‹å “åˆćÆć€ćƒ—ćƒ­ć‚­ć‚·ć® `X-Forwarded-*` ćƒ˜ćƒƒćƒ€ćƒ¼ć‚’äæ”é ¼ć™ć‚‹ć‚ˆć† uvicorn ć«ęŒ‡ē¤ŗć—ć¾ć™ć€‚ + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +ć“ć‚ŒćŒćŖć„ćØć€ć‚¢ćƒ—ćƒŖćÆč‡Ŗåˆ†ćŒ `http://` ć§é…äæ”ć•ć‚Œć¦ć„ć‚‹ćØę€ć„č¾¼ćæć€ē™ŗč”Œć™ć‚‹ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆļ¼ˆć‚ˆćć‚ć‚‹ć®ćÆ `/mcp` → `/mcp/`)はすべて `http://…` ć‚’ęŒ‡ć—ć¾ć™ć€‚Python ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€HTTPS ć‚Øćƒ³ćƒ‰ćƒć‚¤ćƒ³ćƒˆć‹ć‚‰å¹³ę–‡ć® HTTP ćøć®ćƒŖćƒ€ć‚¤ćƒ¬ć‚Æćƒˆć«ćÆå¾“ć‚ćšć€ćć®ę—Øć‚’ä¼ćˆć¾ć™ć€‚ + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆå“ć®åæœę€„å‡¦ē½®ćÆć€ć‚µćƒ¼ćƒćƒ¼ćŒé…äæ”ć™ć‚‹ę­£ē¢ŗćŖ URL(`https://mcp.example.com/mcp/`ć€ęœ«å°¾ć®ć‚¹ćƒ©ćƒƒć‚·ćƒ„č¾¼ćæļ¼‰ć‚’čØ­å®šć—ć¦ć€ćƒŖćƒ€ć‚¤ćƒ¬ć‚ÆćƒˆćŒčµ·ććŖć„ć‚ˆć†ć«ć™ć‚‹ć“ćØć§ć™ć€‚ę ¹ęœ¬ēš„ćŖč§£ę±ŗē­–ćÆäøŠć®ćƒ•ćƒ©ć‚°ć§ć™ć€‚`FORWARDED_ALLOW_IPS` ćÆćć®ē’°å¢ƒå¤‰ę•°ē‰ˆć®ę›øćę–¹ć§ć™ć€‚`*` ćÆć™ć¹ć¦ć®ćƒ›ćƒƒćƒ—ć‚’äæ”é ¼ć—ć¾ć™ćŒć€ćć‚ŒćŒę­£ć—ć„ć®ćÆćƒ—ćƒ­ć‚­ć‚·ä»„å¤–ć‹ć‚‰ uvicorn ć«åˆ°é”ć§ććŖć„å “åˆć ć‘ć§ć™ć€‚ + ## ćƒÆćƒ¼ć‚«ćƒ¼ćØć€ć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ć«ć™ć‚‹åæ…č¦ćŒć‚ć‚‹ć®ćÆčŖ°ć‹ {#workers-and-who-has-to-be-sticky} ćƒ›ć‚¹ćƒˆåćŒåæœē­”ć™ć‚‹ć‚ˆć†ć«ćŖć£ćŸć‚‰ć€ćć®čƒŒå¾Œć«č¤‡ę•°ć®ćƒÆćƒ¼ć‚«ćƒ¼ć‚’ē½®ćć¾ć™ć€‚ćć®ćŸć‚ć® SDK ć®čØ­å®šé …ē›®ćÆć‚ć‚Šć¾ć›ć‚“ć€‚Starlette ć‚¢ćƒ—ćƒŖćÆć€ć©ć‚“ćŖ ASGI ć‚¢ćƒ—ćƒŖćØć‚‚åŒć˜ę–¹ę³•ć§ć‚¹ć‚±ćƒ¼ćƒ«ć—ć¾ć™ć€‚fork ć®ä»•ę–¹ć‚’ēŸ„ć£ć¦ć„ć‚‹ć‚‚ć®ć«ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć‚’ęø”ć™ć ć‘ć§ć™ć€‚ @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## まとめ {#recap} * ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆć€ć“ć®ć‚¢ćƒ—ćƒŖćÆ localhost å®›ć¦ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«ć ć‘åæœē­”ć—ć¾ć™ć€‚`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ćŒå…¬é–‹ę™‚ć®é–¢é–€ć§ć™ć€‚ć“ć‚Œć‚’ęø”ć™ć¾ć§ćÆć€ęœ¬ē‰©ć®ćƒ›ć‚¹ćƒˆåć®čƒŒå¾Œć§ćÆć™ć¹ć¦ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćŒ `421` ć«ćŖć‚Šć€ē†ē”±ćÆć‚µćƒ¼ćƒćƒ¼ć®ćƒ­ć‚°ć«ć—ć‹å‡ŗć¾ć›ć‚“ć€‚ +* TLS ēµ‚ē«Æćƒ—ćƒ­ć‚­ć‚·ć®čƒŒå¾Œć§ćÆć€uvicorn 悒 `--proxy-headers --forwarded-allow-ips=...` ä»˜ćć§å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ćć†ć—ćŖć„ćØćƒŖćƒ€ć‚¤ćƒ¬ć‚ÆćƒˆćŒ `http://` ć‚’ęŒ‡ć—ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ę‹’å¦ć•ć‚Œć¾ć™ć€‚ * 2026-07-28 ć§ćÆć‚»ćƒƒć‚·ćƒ§ćƒ³ćÆćŖćć€ćƒ­ćƒ¼ćƒ‰ćƒćƒ©ćƒ³ć‚µćƒ¼ćŒć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ć«ć™ć¹ćåÆ¾č±”ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚`stateless_http=True` ćŒćƒ¬ć‚¬ć‚·ćƒ¼å°‚ē”Øć®čØ­å®šé …ē›®ćŖć®ćÆć€ćƒ¢ćƒ€ćƒ³ćŖćƒŖć‚Æć‚Øć‚¹ćƒˆćÆć“ć®ćƒ•ćƒ©ć‚°ćŒčŖ­ć¾ć‚Œć‚‹å‰ć«ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć•ć‚Œć€åæœē­”ć•ć‚Œć‚‹ć‹ć‚‰ć§ć™ć€‚ * ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć® `requestState` ć®éµćÆć€ćƒ—ćƒ­ć‚»ć‚¹ć”ćØć«ē”Ÿęˆć•ć‚Œć‚‹ `os.urandom(32)` ć§ć™ć€‚åˆ„ć®ćƒÆćƒ¼ć‚«ćƒ¼ć«å±Šć„ćŸćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ć®ćƒŖćƒˆćƒ©ć‚¤ćÆć€`-32602` *"Invalid or expired requestState"* で失敗します。 * 解決策は `RequestStateSecurity(keys=[...])` **と**ć€ć™ć¹ć¦ć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć§åŒć˜ć‚µćƒ¼ćƒćƒ¼åć«ć™ć‚‹ć“ćØć§ć™ć€‚åå‰ćÆćƒˆćƒ¼ć‚Æćƒ³ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć® audience ć‚Æćƒ¬ćƒ¼ćƒ ć§ć™ć€‚éµć‚‚åŒć˜ć€åå‰ć‚‚åŒć˜ć€‚ diff --git a/i18n/ja/pages/run/index.md b/i18n/ja/pages/run/index.md index feaac858..7dc38bee 100644 --- a/i18n/ja/pages/run/index.md +++ b/i18n/ja/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # ć‚µćƒ¼ćƒćƒ¼ć®å®Ÿč”Œ {#running-your-server} @@ -72,6 +72,8 @@ Inspector ćÆęœ¬ē‰©ć®ćƒ›ć‚¹ćƒˆćØć¾ć£ćŸćåŒć˜ć“ćØć‚’ć—ć¾ć™ć€‚`server * `json_response=True`ļ¼šå„ POST 恫 SSE ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć§ćÆćŖćå˜äø€ć® JSON ćƒœćƒ‡ć‚£ć§åæœē­”ć—ć¾ć™ć€‚ć“ć®ćƒœćƒ‡ć‚£ć«ćÆćƒ¬ć‚¹ćƒćƒ³ć‚¹ć—ć‹å…„ć‚‹ä½™åœ°ćŒć‚ć‚Šć¾ć›ć‚“ć€‚ćć®ćŸć‚ć€ćƒŖć‚Æć‚Øć‚¹ćƒˆć®é€”äø­ć§ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’å‘¼ć³čæ”ć™ćƒ„ćƒ¼ćƒ«ļ¼ˆ`ctx.elicit()` ć‚„ć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ļ¼‰ćÆć€ć“ć®åŒŗé–“ć§ `NoBackChannelError` ć‚’é€å‡ŗć—ć¾ć™ć€‚é€²č”Œäø­ć®å‘¼ć³å‡ŗć—ć«ē“ć„ćé€šēŸ„ļ¼ˆ`ctx.report_progress()` ć«ć‚ˆć‚‹é€²ę—ć‚„å‘¼ć³å‡ŗć—ć”ćØć®ćƒ­ć‚°ćƒ”ćƒƒć‚»ćƒ¼ć‚øļ¼‰ćÆē “ę£„ć•ć‚Œć¾ć™ćŒć€ē‹¬ē«‹ć—ćŸ `GET` ć‚¹ćƒˆćƒŖćƒ¼ćƒ ćÆē„”é–¢äæ‚ćŖé€šēŸ„ć‚’å¼•ćē¶šćé‹ć³ć¾ć™ć€‚ * `stateless_http=True`ļ¼šćƒŖć‚Æć‚Øć‚¹ćƒˆć”ćØć«ę–°ć—ć„ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć‚’ä½œć‚Šć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’čæ½č·”ć—ć¾ć›ć‚“ć€‚ * `max_request_body_size`ļ¼šå—ć‘ä»˜ć‘ć‚‹ćƒŖć‚Æć‚Øć‚¹ćƒˆćƒœćƒ‡ć‚£ć®ęœ€å¤§ć‚µć‚¤ć‚ŗļ¼ˆćƒć‚¤ćƒˆå˜ä½ļ¼‰ć§ć™ć€‚ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆ 4 MiB ć§ć€ć“ć‚Œć‚ˆć‚Šå¤§ćć„ćƒŖć‚Æć‚Øć‚¹ćƒˆćÆćƒ‘ćƒ¼ć‚¹ć‚„ć‚»ćƒƒć‚·ćƒ§ćƒ³ä½œęˆć®å‰ć« HTTP 413 ć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ę­£å½“ćŖ MCP ćƒ”ćƒƒć‚»ćƒ¼ć‚øćŒć“ć®ć‚µć‚¤ć‚ŗć‚’č¶…ćˆć‚‹å “åˆć«ć ć‘å¼•ćäøŠć’ć¦ćć ć•ć„ć€‚ +* `session_idle_timeout`ļ¼šćƒ¬ć‚¬ć‚·ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒå‡¦ē†äø­ć®ć‚‚ć®ć‚’ä½•ć‚‚ęŒćŸćŖć„ć¾ć¾å¾…ę©Ÿć§ćć‚‹ē§’ę•°ć§ć€ć“ć‚Œć‚’éŽćŽć‚‹ćØć‚µćƒ¼ćƒćƒ¼ćŒć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’é–‰ć˜ć¾ć™ć€‚ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆ 1800 恧恙怂`None` ć§ē„”åŠ¹ć«ćŖć‚Šć¾ć™ć€‚[ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®å­˜ē¶šęœŸé–“ćØäøŠé™](legacy-clients.md#session-lifetime-and-limits) ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ +* `max_sessions`:1 ć¤ć®ćƒ—ćƒ­ć‚»ć‚¹ćŒåŒę™‚ć«äæęŒć™ć‚‹ćƒ¬ć‚¬ć‚·ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®ę•°ć§ć™ć€‚ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćÆ 10 000 恧恙怂`None` ć§äøŠé™ćŒćŖććŖć‚Šć¾ć™ć€‚åŒć˜ć‚»ć‚Æć‚·ćƒ§ćƒ³ć§ę‰±ć£ć¦ć„ć¾ć™ć€‚ * `event_store`态`retry_interval`态`transport_security`ļ¼šå†é–‹åÆčƒ½ę€§ćØ DNS ćƒŖćƒć‚¤ćƒ³ćƒ‡ć‚£ćƒ³ć‚°äæč­·ć§ć™ć€‚localhost ä»„å¤–ć®å “ę‰€ć«ćƒ‡ćƒ—ćƒ­ć‚¤ć™ć‚‹ć¾ć§ćÆå¾Œå›žć—ć§ć‹ć¾ć„ć¾ć›ć‚“ć€‚`transport_security` については **[ćƒ‡ćƒ—ćƒ­ć‚¤ćØć‚¹ć‚±ćƒ¼ćƒ«](deploy.md)** で扱います。 !!! warning diff --git a/i18n/ja/pages/run/legacy-clients.md b/i18n/ja/pages/run/legacy-clients.md index 4de105d5..bf2690d4 100644 --- a/i18n/ja/pages/run/legacy-clients.md +++ b/i18n/ja/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®åÆ¾åæœ {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK ćÆć™ć¹ć¦ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ `MCP-Protocol-Version` ćƒ˜ćƒƒćƒ€ćƒ¼ć§ęŒÆ ## 1 ć¤ć®ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ć§äø”ę–¹ć®äø–ä»£ {#one-handler-both-eras} -ćƒ¦ćƒ¼ć‚¶ćƒ¼ć«ä½•ć‹ć‚’å°‹ć­ć‚‹åæ…č¦ćŒć‚ć‚‹ćƒ„ćƒ¼ćƒ«ćØć€ćć‚Œć‚’å‘¼ć³å‡ŗć™äø”ę–¹ć®äø–ä»£ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ē¤ŗć—ć¾ć™ć€‚ +ćƒ¦ćƒ¼ć‚¶ćƒ¼ć«ä½•ć‹ć‚’å°‹ć­ć‚‹åæ…č¦ćŒć‚ć‚‹ćƒ„ćƒ¼ćƒ«ć‚’ē¤ŗć—ć¾ć™ć€‚ -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` ć«ćÆć€ćƒ¢ćƒ‡ćƒ«ćŒęø”ć—ć¦ć“ćŖć‹ć£ćŸć‚‚ć®ćŒ 1 ć¤åæ…č¦ć§ć™ć€‚ä½•å†Šäŗˆē“„ć™ć‚‹ć‹ć§ć™ć€‚ćƒ„ćƒ¼ćƒ«ćÆćć‚Œć‚’ `Annotated[..., Resolve(ask_quantity)]` ć§å®£čØ€ć—ć¾ć™ļ¼ˆč©³ć—ććÆ **[ä¾å­˜é–¢äæ‚](../handlers/dependencies.md)** ć‚’å‚ē…§ć—ć¦ćć ć•ć„ļ¼‰ć€‚`reserve` ć®äø­ć«ćÆć€ćƒćƒ¼ć‚øćƒ§ćƒ³ć‚’ęŒ‡å®šć™ć‚‹ē®‡ę‰€ć‚‚ć€ć‚±ć‚¤ćƒ‘ćƒ“ćƒŖćƒ†ć‚£ć‚’ē¢ŗčŖć™ć‚‹ē®‡ę‰€ć‚‚ć€åˆ†å²ć™ć‚‹ē®‡ę‰€ć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ -2 ć¤ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆåŒć˜ `mcp` ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆć«åÆ¾ć—ć¦**åŒę™‚ć«**é–‹ć‹ć‚Œć¦ć„ć¾ć™ć€‚`mode="legacy"` は `initialize` ćƒćƒ³ćƒ‰ć‚·ć‚§ć‚¤ć‚Æć‚’å®Ÿč”Œć—ć¾ć™ć€‚2026 å¹“ć‚ˆć‚Šå‰ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé–‹ćć®ćØć¾ć£ćŸćåŒć˜ęŽ„ē¶šć§ć™ć€‚ć‚‚ć†äø€ę–¹ćÆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®ć¾ć¾ć§ć€`2026-07-28` ć«ćŖć‚Šć¾ć™ć€‚ +恓悌悒 HTTP ć§å…¬é–‹ć—ć¾ć™ć€‚ē¶šć„ć¦ć€äø”ę–¹ć®äø–ä»£ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćć‚Œć‚’å‘¼ć³å‡ŗć™ę§˜å­ć§ć™ć€‚ + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +2 ć¤ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€å‹•ä½œäø­ć®åŒć˜ć‚µćƒ¼ćƒćƒ¼ć«åÆ¾ć—ć¦**åŒę™‚ć«**é–‹ć‹ć‚Œć¦ć„ć¾ć™ć€‚`mode="legacy"` は `initialize` ćƒćƒ³ćƒ‰ć‚·ć‚§ć‚¤ć‚Æć‚’å®Ÿč”Œć—ć¾ć™ć€‚2026 å¹“ć‚ˆć‚Šå‰ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé–‹ćć®ćØć¾ć£ćŸćåŒć˜ęŽ„ē¶šć§ć™ć€‚ć‚‚ć†äø€ę–¹ćÆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®ć¾ć¾ć§ć€`2026-07-28` ć«ćŖć‚Šć¾ć™ć€‚åˆ„ć®ć‚æćƒ¼ćƒŸćƒŠćƒ«ć‹ć‚‰ `python client.py` ć‚’å®Ÿč”Œć—ć¦ćć ć•ć„ć€‚ ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK ćÆć™ć¹ć¦ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ `MCP-Protocol-Version` ćƒ˜ćƒƒćƒ€ćƒ¼ć§ęŒÆ !!! warning `event_store=` ćÆč§£ę±ŗē­–ć«č¦‹ćˆć¾ć™ćŒć€ćć†ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć‚ŒćÆ**å†é–‹åÆčƒ½ę€§**ļ¼ˆć€ŒåŒć˜ć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«å†ęŽ„ē¶šć™ć‚‹ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ć€å–ć‚Šć“ć¼ć—ćŸ SSE ć‚¤ćƒ™ćƒ³ćƒˆć‚’å†é€ć™ć‚‹ę©Ÿčƒ½ļ¼‰ć§ć‚ć£ć¦ć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚¹ćƒˆć‚¢ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚åˆ„ć®ćƒ—ćƒ­ć‚»ć‚¹ć‹ć‚‰ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«åˆ°é”ć§ćć‚‹ć‚ˆć†ć«ćÆć—ć¾ć›ć‚“ć€‚ +## ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®ęœ‰åŠ¹ęœŸé–“ćØäøŠé™ {#session-lifetime-and-limits} + +ćƒ¬ć‚¬ć‚·ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ćÆę°øé ć«ćÆē”Ÿćē¶šć‘ć¾ć›ć‚“ć—ć€1 ć¤ć®ćƒ—ćƒ­ć‚»ć‚¹ćŒē„”åˆ¶é™ć«ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’ęŠ±ćˆć‚‹ć“ćØć‚‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć‚Œć‚’åˆ¶å¾”ć™ć‚‹čØ­å®šćŒ 2 ć¤ć‚ć‚Šć¾ć™ć€‚ć©ć”ć‚‰ć‚‚ `run()`态`streamable_http_app()`态`Server.streamable_http_app()` ć®ć‚­ćƒ¼ćƒÆćƒ¼ćƒ‰å¼•ę•°ć§ć™ć€‚ćƒ¢ćƒ€ćƒ³ćŖļ¼ˆ`2026-07-28` ć®ļ¼‰ęŽ„ē¶šćØ `stateless_http=True` ć«ćÆć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒćŖć„ćŸć‚ć€ć©ć”ć‚‰ć®čØ­å®šć‚‚é©ē”Øć•ć‚Œć¾ć›ć‚“ć€‚ + +| 設定 | ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆ | 動作 | ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‹ć‚‰č¦‹ćˆć‚‹ć‚‚ć® | ē„”åŠ¹ć«ć™ć‚‹ć«ćÆ | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 åˆ†ļ¼‰ | å‡¦ē†äø­ć®ć‚‚ć®ćŒä½•ć‚‚ćŖć„ēŠ¶ę…‹ćŒćć®ę™‚é–“ē¶šć„ćŸć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’é–‰ć˜ć¾ć™ć€‚ | `404 Session not found`。もう一度 `initialize` ć™ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚ | `None` | +| `max_sessions` | `10_000` | ćć®ę•°ć‚’č¶…ćˆć¦ę–°ć—ć„ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’é–‹ćć“ćØć‚’ę‹’å¦ć—ć¾ć™ć€‚ę—¢å­˜ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć«ćÆę‰‹ć‚’ä»˜ć‘ćšć€ä½•ć‚‚čæ½ć„å‡ŗć—ć¾ć›ć‚“ć€‚ | `503 Too many open sessions`态JSON-RPC ć‚³ćƒ¼ćƒ‰ćÆ `-32603` 恧恙怂 | `None` | + +ć€Œå‡¦ē†äø­ć€ćØč¦‹ćŖć•ć‚Œć‚‹ć‚‚ć®ćÆę¬”ć®ćØćŠć‚Šć§ć™ć€‚ + +* 開恄恦恄悋 `GET` ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć€‚SDK ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆ 1 ęœ¬ć‚’é–‹ć„ćŸć¾ć¾ć«ć™ć‚‹ćŸć‚ć€ęŽ„ē¶šäø­ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒęœŸé™åˆ‡ć‚Œć«ćŖć‚‹ć“ćØćÆć‚ć‚Šć¾ć›ć‚“ć€‚ +* ć¾ć åæœē­”äø­ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć€‚ć‚æć‚¤ćƒ ć‚¢ć‚¦ćƒˆć‚ˆć‚Šé•·ćå‹•ććƒ„ćƒ¼ćƒ«å‘¼ć³å‡ŗć—ćŒäø­ę–­ć•ć‚Œć‚‹ć“ćØćÆćŖćć€ć‚«ć‚¦ćƒ³ćƒˆćƒ€ć‚¦ćƒ³ćÆćć®å‘¼ć³å‡ŗć—ćŒēµ‚ć‚ć£ć¦ć‹ć‚‰å§‹ć¾ć‚Šć¾ć™ć€‚ +* ćć‚Œä»„å¤–ć«ćÆć‚ć‚Šć¾ć›ć‚“ć€‚ćƒŖć‚Æć‚Øć‚¹ćƒˆćØćƒŖć‚Æć‚Øć‚¹ćƒˆć®é–“ćÆę™‚čØˆćŒé€²ćæć¾ć™ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³äøŠć®ć©ć‚“ćŖćƒŖć‚Æć‚Øć‚¹ćƒˆć§ć‚‚ę™‚čØˆćÆćƒŖć‚»ćƒƒćƒˆć•ć‚Œć€`ping` ć‚‚ä¾‹å¤–ć§ćÆć‚ć‚Šć¾ć›ć‚“ć€‚äø€åŗ¦ęœŸé™åˆ‡ć‚Œć«ćŖć£ćŸć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’å¾©ę“»ć•ć›ć‚‹ę‰‹ę®µćÆć‚ć‚Šć¾ć›ć‚“ć€‚ + +`DELETE` ć§ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’ēµ‚äŗ†ć—ćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć€ćć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’å³åŗ§ć«č§£ę”¾ć—ć¾ć™ć€‚ęœ€åˆć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćŒę‹’å¦ć•ć‚ŒćŸć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚‚åŒę§˜ć§ć™ć€‚ + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +ć©ć”ć‚‰ć®å‡ŗę„äŗ‹ć‚‚ć‚µćƒ¼ćƒćƒ¼ć®ćƒ­ć‚°ć«ę®‹ć‚Šć¾ć™ć€‚ęœŸé™åˆ‡ć‚ŒćÆ `INFO` ćƒ¬ćƒ™ćƒ«ć® `Session idle timeout` ć§ć™ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚’é–‹ćć®ć‚’ę‹’å¦ć—ćŸå “åˆćÆ `WARNING` ćƒ¬ćƒ™ćƒ«ć® `Refusing to open a new session: sessions are already open` 恧恙怂 + +ć“ć‚Œć‚‰ć®äøŠé™ćÆćƒ—ćƒ­ć‚»ć‚¹ć”ćØć§ć™ć€‚ćƒÆćƒ¼ć‚«ćƒ¼ćŒ 4 ć¤ćŖć‚‰äøŠé™ćÆ `max_sessions` 恮 4 å€ć§ć€å„ćƒÆćƒ¼ć‚«ćƒ¼ćÆč‡Ŗåˆ†ć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ć ć‘ć‚’ęœŸé™åˆ‡ć‚Œć«ć—ć¾ć™ć€‚ + ## å”Æäø€ć®ć‚¹ć‚¤ćƒƒćƒļ¼š`stateless_http` {#the-one-knob-stateless_http} ć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ę€§ćØć„ć†ć‚³ć‚¹ćƒˆć‚’ę‰•ć„ćŸććŖć„ćŖć‚‰ć€å¤‰ę›“ć§ćć‚‹ć‚‚ć®ćŒć”ć‚‡ć†ć© 1 ć¤ć ć‘ć‚ć‚Šć¾ć™ć€‚ @@ -73,7 +108,7 @@ SDK ćÆć™ć¹ć¦ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’ `MCP-Protocol-Version` ćƒ˜ćƒƒćƒ€ćƒ¼ć§ęŒÆ `json_response=True` ćÆćć®ć‚¹ć‚¤ćƒƒćƒć§ćÆć‚ć‚Šć¾ć›ć‚“ćŒć€ć€Œć™ć¹ć¦ć®ć€ćƒ¬ć‚¬ć‚·ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ć§åŒć˜ć‚³ć‚¹ćƒˆć®åŠåˆ†ć‚’č² ć„ć¾ć™ć€‚1 恤恮 JSON ćƒœćƒ‡ć‚£ć§åæœē­”ć•ć‚Œć‚‹ `POST` ć«ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć‚¹ć‚³ćƒ¼ćƒ—ć®ćƒćƒ£ćƒćƒ«ē”Øć®ć‚¹ćƒˆćƒŖćƒ¼ćƒ ćŒćŖć„ćŸć‚ć€ćƒŖć‚Æć‚Øć‚¹ćƒˆé€”äø­ć® `ctx.elicit()` は同じ `NoBackChannelError` ć‚’é€å‡ŗć—ć€ćć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć«ēµć³ä»˜ć„ćŸé€šēŸ„ćÆęØć¦ć‚‰ć‚Œć¾ć™ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®ć‚¹ć‚æćƒ³ćƒ‰ć‚¢ćƒ­ćƒ³ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć«ćÆå½±éŸæć—ć¾ć›ć‚“ć€‚ē„”é–¢äæ‚ćŖé€šēŸ„ćÆå¼•ćē¶šćå±Šćć¾ć™ć€‚ !!! check - ć‚ćˆć¦é–“é•ć£ćŸć“ćØć‚’ć—ć¦ćæć¾ć—ć‚‡ć†ć€‚`reserve` ćÆć€å…ˆć»ć©äø”ę–¹ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«åæœē­”ć—ćŸćć®ćƒ„ćƒ¼ćƒ«ć§ć™ć€‚ć“ć‚Œć‚’ `stateless_http=True` ć§ćƒ‡ćƒ—ćƒ­ć‚¤ć—ć€åŒć˜ 2 ć¤ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ HTTP ć§ęŽ„ē¶šć—ć¦ć€ćć‚Œćžć‚Œć‹ć‚‰å‘¼ć³å‡ŗć—ć¦ćć ć•ć„ć€‚ + ć‚ćˆć¦é–“é•ć£ćŸć“ćØć‚’ć—ć¦ćæć¾ć—ć‚‡ć†ć€‚`reserve` ćÆć€å…ˆć»ć©äø”ę–¹ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«åæœē­”ć—ćŸćć®ćƒ„ćƒ¼ćƒ«ć§ć™ć€‚ć“ć‚Œć‚’ `stateless_http=True` ć§ćƒ‡ćƒ—ćƒ­ć‚¤ć—ć€åŒć˜ 2 ć¤ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ęŽ„ē¶šć—ć¦ć€ćć‚Œćžć‚Œć‹ć‚‰å‘¼ć³å‡ŗć—ć¦ćć ć•ć„ć€‚ ćƒ¢ćƒ€ćƒ³ćŖć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćÆå¼•ćē¶šć `Reserved 2 of 'Dune'.` ćŒčæ”ć‚Šć¾ć™ć€‚ćƒ¢ćƒ€ćƒ³ćŖēµŒč·ÆćÆå¤‰ć‚ć£ć¦ć„ć¾ć›ć‚“ć€‚ diff --git a/i18n/ja/pages/troubleshooting.md b/i18n/ja/pages/troubleshooting.md index 77f5d874..d351427d 100644 --- a/i18n/ja/pages/troubleshooting.md +++ b/i18n/ja/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # ćƒˆćƒ©ćƒ–ćƒ«ć‚·ćƒ„ćƒ¼ćƒ†ć‚£ćƒ³ć‚° {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +ćć‚Œć‚‰ć®é …ē›®ćÆ `http://localhost:8000/mcp` ć§ć“ć®ć‚µćƒ¼ćƒćƒ¼ć«ć‚¢ć‚Æć‚»ć‚¹ć™ć‚‹ć®ć§ć€HTTP ć§čµ·å‹•ć—ćŸć¾ć¾ć«ć—ć¦ćŠć„ć¦ćć ć•ć„ć€‚ + +```console +uv run mcp run server.py --transport streamable-http +``` + ć“ć®ćƒšćƒ¼ć‚øć§å¼•ē”Øć—ć¦ć„ć‚‹ć‚Øćƒ©ćƒ¼ćÆęœ¬ē‰©ć§ć™ć€‚SDK č‡Ŗčŗ«ć®ćƒ†ć‚¹ćƒˆć‚¹ć‚¤ćƒ¼ćƒˆćŒć€ćć®ć™ć¹ć¦ć‚’å†ē¾ć—ć¦ć„ć¾ć™ć€‚ ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé€ć£ćŸ `Mcp-Session-Id` ć‚’ć‚µćƒ¼ćƒćƒ¼ćŒčŖč­˜ć—ć¦ć„ć¾ć›ć‚“ć€‚ć»ć¼ē¢ŗå®Ÿć«ć€ć‚µćƒ¼ćƒćƒ¼ćŒ**å†čµ·å‹•ć—ćŸ**ļ¼ˆć¾ćŸćÆåˆ„ć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć«ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć•ć‚ŒćŸļ¼‰ć®ćŒåŽŸå› ć§ć™ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³ćÆć€ćć® 1 ć¤ć®ćƒ—ćƒ­ć‚»ć‚¹ć®ćƒ”ćƒ¢ćƒŖć®äø­ć«ć‚ć‚Šć¾ć™ć€‚ +ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé€ć£ćŸ `Mcp-Session-Id` ć‚’ć‚µćƒ¼ćƒćƒ¼ćŒčŖč­˜ć—ć¦ć„ć¾ć›ć‚“ć€‚ć‚µćƒ¼ćƒćƒ¼ćŒ**å†čµ·å‹•ć—ćŸ**ļ¼ˆć¾ćŸćÆåˆ„ć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć«ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć•ć‚ŒćŸļ¼‰ć‹ć€`session_idle_timeout`ļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ 30 åˆ†ļ¼‰ć®é–“ć«é€²č”Œäø­ć®ć‚‚ć®ćŒä½•ć‚‚ćŖćć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒ**ęœŸé™åˆ‡ć‚Œć«ćŖć£ćŸ**恋恮恩恔悉恋恧恙怂[ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®åÆæå‘½ćØäøŠé™](run/legacy-clients.md#session-lifetime-and-limits)ć‚’å‚ē…§ć—ć¦ćć ć•ć„ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³ćÆć€ćć® 1 ć¤ć®ćƒ—ćƒ­ć‚»ć‚¹ć®ćƒ”ćƒ¢ćƒŖć®äø­ć«ć‚ć‚Šć¾ć™ć€‚ ęŽ¢ć™ć¹ćć‚µćƒ¼ćƒćƒ¼ć®ćƒć‚°ćÆć‚ć‚Šć¾ć›ć‚“ć€‚HTTP ćƒ¬ć‚¹ćƒćƒ³ć‚¹ćÆ `404` ć§ć€ćć®ćƒœćƒ‡ć‚£ćÆ JSON-RPCć€Œć§ć™ć€ć€‚ćć®ćŸć‚äøŠć® `421` とは違い、python 恮 `Client` ćÆć“ć‚Œć‚’ćć®ć¾ć¾č¦‹ć›ć¦ćć‚Œć¾ć™ć€‚ @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ē›“ć—ę–¹ćÆå†ęŽ„ē¶šć§ć™ć€‚`async with Client(...)` ćƒ–ćƒ­ćƒƒć‚Æć‚’ęŠœć‘ć¦ę–°ć—ć„ćƒ–ćƒ­ćƒƒć‚Æć«å…„ć‚Œć°ć€ę–°ć—ć„ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć•ć‚Œć¾ć™ć€‚é•·ćē”Ÿćć‚‹ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć§ć‚ć‚Œć°ć€å‘¼ć³å‡ŗć—ć®å‘Øć‚Šć§ `MCPError` ć‚’ę•ć¾ćˆć€ę­»ć‚“ć ć‚»ćƒƒć‚·ćƒ§ćƒ³ć®äø­ć§ćƒŖćƒˆćƒ©ć‚¤ć™ć‚‹ć®ć§ćÆćŖćć€ć“ć®ćƒ”ćƒƒć‚»ćƒ¼ć‚øć‚’č¦‹ćŸć‚‰å†ęŽ„ē¶šć™ć‚‹ć“ćØć«ćŖć‚Šć¾ć™ć€‚ -å†čµ·å‹•ć€ŒćŖć—ć§ć€ć“ć‚ŒćŒčµ·ćć‚‹ćŖć‚‰ć€ć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŖć—ć§č¤‡ę•°ć®ćƒÆćƒ¼ć‚«ćƒ¼ć‚’å‹•ć‹ć—ć¦ć„ć¾ć™ć€‚ćƒÆćƒ¼ć‚«ćƒ¼ć”ćØć«ē‹¬č‡Ŗć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ćƒ†ćƒ¼ćƒ–ćƒ«ć‚’ęŒć¤ć®ć§ć€é–“é•ć£ćŸćƒÆćƒ¼ć‚«ćƒ¼ć«ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć•ć‚ŒćŸćƒŖć‚Æć‚Øć‚¹ćƒˆćÆć“ć“ć«č”Œćē€ćć¾ć™ć€‚ć“ć®č©±ćØćć® 2 ć¤ć®ē›“ć—ę–¹ļ¼ˆć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć€ć¾ćŸćÆ `stateless_http=True`)は、**[ćƒ‡ćƒ—ćƒ­ć‚¤ćØć‚¹ć‚±ćƒ¼ćƒ«](run/deploy.md)** と **[ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®ęä¾›](run/legacy-clients.md)** ćŒę‹…å½“ć—ć¦ć„ć¾ć™ć€‚ +å†čµ·å‹•ć€ŒćŖć—ć§ć€ć€ć‹ć¤ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćć‚Œć»ć©é•·ćé™ć‹ć ć£ćŸć‚ć‘ć§ć‚‚ćŖć„ć®ć«ć“ć‚ŒćŒčµ·ćć‚‹ćŖć‚‰ć€ć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŖć—ć§č¤‡ę•°ć®ćƒÆćƒ¼ć‚«ćƒ¼ć‚’å‹•ć‹ć—ć¦ć„ć¾ć™ć€‚ćƒÆćƒ¼ć‚«ćƒ¼ć”ćØć«ē‹¬č‡Ŗć®ć‚»ćƒƒć‚·ćƒ§ćƒ³ćƒ†ćƒ¼ćƒ–ćƒ«ć‚’ęŒć¤ć®ć§ć€é–“é•ć£ćŸćƒÆćƒ¼ć‚«ćƒ¼ć«ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć•ć‚ŒćŸćƒŖć‚Æć‚Øć‚¹ćƒˆćÆć“ć“ć«č”Œćē€ćć¾ć™ć€‚ć“ć®č©±ćØćć® 2 ć¤ć®ē›“ć—ę–¹ļ¼ˆć‚¹ćƒ†ć‚£ćƒƒć‚­ćƒ¼ćƒ«ćƒ¼ćƒ†ć‚£ćƒ³ć‚°ć€ć¾ćŸćÆ `stateless_http=True`)は、**[ćƒ‡ćƒ—ćƒ­ć‚¤ćØć‚¹ć‚±ćƒ¼ćƒ«](run/deploy.md)** と **[ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®ęä¾›](run/legacy-clients.md)** ćŒę‹…å½“ć—ć¦ć„ć¾ć™ć€‚ -ć‚µćƒ¼ćƒćƒ¼é‹ē”Øč€…å‘ć‘ć«ćÆć€åÆ¾åæœć™ć‚‹ćƒ­ć‚°č”ŒćÆ `Rejected request with unknown or expired session ID: ` 恧恙怂`INFO` ć§čØ˜éŒ²ć•ć‚Œć‚‹ć®ć§ć€é€šåøøć® `WARNING` ć®ć—ćć„å€¤ć§ćÆč¦‹ćˆć¾ć›ć‚“ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ē›“å¾Œć«ć¾ćØć¾ć£ć¦å‡ŗć‚‹ć®ćÆę­£åøøć§ć™ć€‚ęŽ„ē¶šäø­ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒć™ć¹ć¦å†ęŽ„ē¶šć—ć¦ć„ć‚‹ć®ć§ć™ć€‚ +ć‚µćƒ¼ćƒćƒ¼é‹ē”Øč€…å‘ć‘ć«ćÆć€åÆ¾åæœć™ć‚‹ćƒ­ć‚°č”ŒćÆ `Rejected request with unknown or expired session ID: ` 恧恙怂`INFO` ć§čØ˜éŒ²ć•ć‚Œć‚‹ć®ć§ć€é€šåøøć® `WARNING` ć®ć—ćć„å€¤ć§ćÆč¦‹ćˆć¾ć›ć‚“ć€‚ćƒ‡ćƒ—ćƒ­ć‚¤ē›“å¾Œć«ć¾ćØć¾ć£ć¦å‡ŗć‚‹ć®ćÆę­£åøøć§ć™ć€‚ęŽ„ē¶šäø­ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒć™ć¹ć¦å†ęŽ„ē¶šć—ć¦ć„ć‚‹ć®ć§ć™ć€‚ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒęœŸé™åˆ‡ć‚Œć«ćŖć£ćŸå “åˆćÆć€ćć®č”Œć®å‰ć« `Session idle timeout` ćŒåŒć˜ć `INFO` で出ます。 ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ć‚µćƒ¼ćƒćƒ¼ćÆćƒ¦ćƒ¼ć‚¶ćƒ¼ć«ä½•ć‹ć‚’å°‹ć­ćŸć„ć®ć«ć€ć“ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆå°‹ć­ć‚‰ć‚Œć‚‹ć“ćØćŒć§ćć‚‹ćØäø€åŗ¦ć‚‚čØ€ć£ć¦ć„ć¾ć›ć‚“ć€‚ -ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ļ¼ˆelicitationļ¼‰ć®ćƒŖć‚¾ćƒ«ćƒćƒ¼ćÆć€ęŽ„ē¶šäø­ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćƒ•ć‚©ćƒ¼ćƒ ć®ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć‚’å®£čØ€ć—ć¦ć„ćŖć„å “åˆć€ęœ€åˆć®ę™‚ē‚¹ć§ę‹’å¦ć—ć¾ć™ć€‚`e.error.data` ć«ćÆć€č¶³ć‚ŠćŖć„ć‚‚ć®ćŒę­£ē¢ŗć«ę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚ +恓恮 Bistro ćÆć€äŗˆē“„ć™ć‚‹å‰ć«ćƒŖć‚¾ćƒ«ćƒćƒ¼ć‚’é€šć˜ć¦å°‹ć­ć¾ć™ć€‚ + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather ć‚µćƒ¼ćƒćƒ¼ć®ä»£ć‚ć‚Šć«ć“ć‚Œć‚’ęä¾›ć—ć€`elicitation_callback` ć‚’ęø”ć—ć¦ć„ćŖć„ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‹ć‚‰ `book_table` ć‚’å‘¼ć‚“ć§ćć ć•ć„ć€‚ęŽ„ē¶šäø­ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒćƒ•ć‚©ćƒ¼ćƒ ć®ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ļ¼ˆelicitationļ¼‰ć‚’äø€åŗ¦ć‚‚å®£čØ€ć—ć¦ć„ćŖć„ć®ć§ć€ćƒŖć‚¾ćƒ«ćƒćƒ¼ćÆęœ€åˆć®ę™‚ē‚¹ć§ę‹’å¦ć—ć¾ć™ć€‚`e.error.data` ć«ćÆć€č¶³ć‚ŠćŖć„ć‚‚ć®ćŒę­£ē¢ŗć«ę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚ ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: ćƒćƒ³ćƒ‰ćƒ©ćƒ¼ćŒćƒŖć‚Æć‚Øć‚¹ćƒˆć®é€”äø­ć§ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«é€£ēµ”ć‚’å–ć‚ć†ćØć—ć¾ć—ćŸćŒć€ćć®ęŽ„ē¶šć®å‘¼ć³å‡ŗć—ć«ćÆć€ć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć‚’é‹ć¹ć‚‹ćƒćƒ£ćƒćƒ«ćŒć‚ć‚Šć¾ć›ć‚“ć€‚å‘¼ć³å‡ŗć—ć‚’ćć“ć«ē½®ćć‚µćƒ¼ćƒćƒ¼čØ­å®šćÆ 3 ć¤ć‚ć‚Šć¾ć™ć€‚ -**`2026-07-28` ć®ęŽ„ē¶šć€‚ć©ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚‚ć€åøøć«ć€‚** ćƒ¢ćƒ€ćƒ³ćŖćƒ—ćƒ­ćƒˆć‚³ćƒ«ć«ćÆć‚µćƒ¼ćƒćƒ¼čµ·ē‚¹ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćŒćć‚‚ćć‚‚å­˜åœØć—ćŖć„ć®ć§ć€ć‚µćƒ¼ćƒćƒ¼ćÆä½•ć‹ć‚’é€ć‚‹å‰ć«ę‹’å¦ć—ć¾ć™ć€‚ćƒ„ćƒ¼ćƒ«ć®äø­ć® `ctx.elicit()` ćŒć€ć“ć‚Œć«å‡ŗä¼šć†å…øåž‹ēš„ćŖēµŒč·Æć§ć™ļ¼ˆ`Client(server)` ćÆé ¼ć¾ć‚ŒćŖćć¦ć‚‚ `2026-07-28` ć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć™ć‚‹ć®ć§ć€ęœ€åˆć®ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖćƒ†ć‚¹ćƒˆć§å‡ŗä¼šć„ć¾ć™ļ¼‰ć€‚`elicitation_callback=` ć‚’ęø”ć—ć¦ć‚‚ä½•ć‚‚å¤‰ć‚ć‚Šć¾ć›ć‚“ć€‚ē­”ćˆć‚‹ć¹ććƒŖć‚Æć‚Øć‚¹ćƒˆćŒć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«å±Šćć“ćØćŒćŖć„ć‹ć‚‰ć§ć™ć€‚ +**`2026-07-28` ć®ęŽ„ē¶šć€‚ć©ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć§ć‚‚ć€åøøć«ć€‚** ćƒ¢ćƒ€ćƒ³ćŖćƒ—ćƒ­ćƒˆć‚³ćƒ«ć«ćÆć‚µćƒ¼ćƒćƒ¼čµ·ē‚¹ć®ćƒŖć‚Æć‚Øć‚¹ćƒˆćŒćć‚‚ćć‚‚å­˜åœØć—ćŖć„ć®ć§ć€ć‚µćƒ¼ćƒćƒ¼ćÆä½•ć‹ć‚’é€ć‚‹å‰ć«ę‹’å¦ć—ć¾ć™ć€‚ćƒ„ćƒ¼ćƒ«ć®äø­ć® `ctx.elicit()` ćŒć€ć“ć‚Œć«å‡ŗä¼šć†å…øåž‹ēš„ćŖēµŒč·Æć§ć™ć€‚ćŸć„ć¦ć„ćÆć€ćć®ćƒ„ćƒ¼ćƒ«ć®ęœ€åˆć®ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖ**[ćƒ†ć‚¹ćƒˆ](get-started/testing.md)**ć§å‡ŗä¼šć„ć¾ć™ć€‚`Client(mcp)` ćÆé ¼ć¾ć‚ŒćŖćć¦ć‚‚ `2026-07-28` ć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć™ć‚‹ć‹ć‚‰ć§ć™ć€‚`elicitation_callback=` ć‚’ęø”ć—ć¦ć‚‚ä½•ć‚‚å¤‰ć‚ć‚Šć¾ć›ć‚“ć€‚ē­”ćˆć‚‹ć¹ććƒŖć‚Æć‚Øć‚¹ćƒˆćŒć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«å±Šćć“ćØćŒćŖć„ć‹ć‚‰ć§ć™ć€‚ ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * ć‚µćƒ¼ćƒćƒ¼ćƒ­ć‚°ć® `Tool already exists:` ćÆć€åŒåć® 2 ć¤ć®ćƒ„ćƒ¼ćƒ«ćŒ 1 ć¤ć«ę½°ć‚ŒćŸå”Æäø€ć®åˆå›³ć§ć™ć€‚ * 1 恤恮 421态3 ć¤ć®ē¶“ć‚Šļ¼š`Server returned an error response`(python 恮 `Client`)、`421 Misdirected Request` / `Invalid Host header`ļ¼ˆćć‚Œä»„å¤–ć™ć¹ć¦ļ¼‰ć€`Invalid Host header: `ļ¼ˆć‚µćƒ¼ćƒćƒ¼ćƒ­ć‚°ļ¼‰ć€‚ē›“ć—ę–¹ļ¼š`transport_security=TransportSecuritySettings(allowed_hosts=[...])`怂 * `Task group is not initialized` -> ćƒžć‚¦ćƒ³ćƒˆć•ć‚ŒćŸć‚¢ćƒ—ćƒŖć§ć€ćƒ›ć‚¹ćƒˆć®ćƒ©ć‚¤ćƒ•ć‚¹ćƒ‘ćƒ³ćŒ `mcp.session_manager.run()` に兄っていません。 -* `Session not found` -> ć‚µćƒ¼ćƒćƒ¼ćŒå†čµ·å‹•ć—ć¾ć—ćŸć€‚å†ęŽ„ē¶šć—ć¦ćć ć•ć„ć€‚ +* `Session not found` -> ć‚µćƒ¼ćƒćƒ¼ćŒå†čµ·å‹•ć—ćŸć‹ć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŒęœŸé™åˆ‡ć‚Œć«ćŖć‚Šć¾ć—ćŸļ¼ˆ`session_idle_timeout`ļ¼‰ć€‚å†ęŽ„ē¶šć—ć¦ćć ć•ć„ć€‚ * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` ć«ćÆć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®ćƒćƒ£ćƒćƒ«ćŒåæ…č¦ć§ć™ć€‚`2026-07-28` ć®ęŽ„ē¶šć«ćÆćć‚ŒćŒę±ŗć—ć¦ćŖćć€`stateless_http=True` ćÆćƒ¬ć‚¬ć‚·ćƒ¼ć®ćƒćƒ£ćƒćƒ«ć‚’å„Ŗć„ć€`json_response=True` ćÆćƒŖć‚Æć‚Øć‚¹ćƒˆć‚¹ć‚³ćƒ¼ćƒ—ć®ćƒćƒ£ćƒćƒ«ć‚’å„Ŗć„ć¾ć™ć€‚ćƒŖć‚¾ćƒ«ćƒćƒ¼ć‚’ä½æć£ć¦ćć ć•ć„ļ¼ˆćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ćÆć€ćƒćƒ£ćƒćƒ«ć‚’äæęŒć™ć‚‹ć‚µćƒ¼ćƒćƒ¼ć‚‚åæ…č¦ć§ć™ļ¼‰ć€‚éš£ć® `Method not found` ćÆć€ē›øę‰‹å“ć®ćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒŖćƒ“ć‚øćƒ§ćƒ³ć«ćŖć„ćƒ”ć‚½ćƒƒćƒ‰ćøć®ćƒŖć‚Æć‚Øć‚¹ćƒˆć§ć™ć€‚ * `Client did not declare the form elicitation capability ...` と `Elicitation not supported` -> ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć« `elicitation_callback=` ćŒč¶³ć‚Šć¾ć›ć‚“ć€‚ * `Invalid or expired requestState` ćÆć€é€šäæ”äøŠć§ćÆę±ŗć—ć¦ē†ē”±ć‚’čØ€ć„ć¾ć›ć‚“ć€‚ć‚µćƒ¼ćƒćƒ¼ćƒ­ć‚°ćŒčØ€ć„ć¾ć™ć€‚`unknown key` ćÆć€ćƒÆćƒ¼ć‚«ćƒ¼é–“ć§ `RequestStateSecurity(keys=[...])` ć‚’å…±ęœ‰ć›ć‚ˆćØć„ć†ę„å‘³ć§ć™ć€‚ diff --git a/i18n/ja/pages/whats-new.md b/i18n/ja/pages/whats-new.md index eb226e26..71aacc66 100644 --- a/i18n/ja/pages/whats-new.md +++ b/i18n/ja/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 ć®ę–°ę©Ÿčƒ½ {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 では 3 ć¤ć®å±¤ćŒå…„ć‚Œå­ć«ćŖć£ć¦ć„ć¾ć—ćŸć€‚ē”Ÿć®ć‚¹ćƒˆćƒŖćƒ¼ćƒ ć‚’čæ”ć™ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ć‚³ćƒ³ćƒ†ć‚­ć‚¹ćƒˆćƒžćƒćƒ¼ć‚øćƒ£ćƒ¼ć€ćć‚Œć‚’åŒ…ć‚€ `ClientSession`ć€ćć—ć¦ę‰‹ć§å‘¼ć³å‡ŗć™ `await session.initialize()` 恧恙怂v2 ć«ć‚ć‚‹ć®ćÆć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆ 1 恤恧恙怂 -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` ćŒå—ć‘å–ć‚‹ć®ćÆć€ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆļ¼ˆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćŖć—ć€‚ćƒ†ć‚¹ćƒˆć§ä½æć†å½¢ć§ć™ļ¼‰ć€URL(Streamable HTTP)、`StdioServerParameters`(stdio ć®ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ļ¼‰ć€ć¾ćŸćÆ `sse_client(...)` ć®ć‚ˆć†ćŖćć‚Œä»„å¤–ć®ä»»ę„ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ć‚³ćƒ³ćƒ†ć‚­ć‚¹ćƒˆćƒžćƒćƒ¼ć‚øćƒ£ćƒ¼ć§ć™ć€‚`async with` ć«å…„ć‚‹ćØęŽ„ē¶šć—ć€ć‚µćƒ¼ćƒćƒ¼ćŒć©ć®äø–ä»£ć‚’č©±ć™ć‹ć«ć‹ć‹ć‚ć‚‰ćšćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć—ć¾ć™ć€‚ćć®å¾ŒćÆ `client.server_capabilities` と `client.protocol_version` ćŒćć®ć¾ć¾ä½æćˆć€ć‚µćƒ¼ćƒćƒ¼ćŒč‡Ŗčŗ«ć‚’åä¹—ć‚‹å “åˆćÆ `client.server_info` ć‚‚ä½æćˆć¾ć™ļ¼ˆ2026 å¹“äø–ä»£ć§ćÆč­˜åˆ„ęƒ…å ±ćŒēœē•„åÆčƒ½ćŖć®ć§ć€`Implementation | None` ć«ćŖć‚Šć¾ć—ćŸļ¼‰ć€‚v1 ć§ē™»éŒ²ć—ćŸć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ćØć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć®ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćÆå¼•ćē¶šćå‹•ä½œć—ć¾ć™ļ¼ˆć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æć®ęœ¬ä½“ć«ćÆć€ć“ć®ćƒšćƒ¼ć‚øć®ć»ć‹ć®é …ē›®ćØåŒć˜ snake_case ćøć®å±žę€§åć®å¤‰ę›“ćŒåŠć³ć¾ć™ļ¼‰ć€‚åŠ ćˆć¦ 2026 å½¢å¼ć®ć€Œēµęžœć«åŸ‹ć‚č¾¼ć¾ć‚ŒćŸćƒŖć‚Æć‚Øć‚¹ćƒˆć€ļ¼ˆå¾Œčæ°ļ¼‰ć«ć‚‚åæœē­”ć™ć‚‹ć‚ˆć†ć«ćŖć‚Šć€1 ć¤ćšć¤ć§ćÆćŖćäø¦č”Œć—ć¦å®Ÿč”Œć•ć‚Œć¾ć™ć€‚ä½Žćƒ¬ćƒ™ćƒ«ć®ć‚¤ćƒ³ć‚æćƒ¼ćƒ•ć‚§ćƒ¼ć‚¹ćŒåæ…č¦ćŖäŗŗć®ćŸć‚ć« `ClientSession` ćÆä»Šć‚‚äø‹ć«ć‚ć‚Šć€`client.session` ć§å–ć‚Šå‡ŗć›ć¾ć™ć€‚ćŸć ć—ć“ć”ć‚‰ć‚‚å¤‰ć‚ć£ć¦ć„ć¾ć™ļ¼ˆę–°ć—ć„ćƒ‡ć‚£ć‚¹ćƒ‘ćƒƒćƒćƒ£ćƒ¼ć‚Øćƒ³ć‚øćƒ³ć®äøŠć§å‹•ćć€č‡Ŗčŗ«ć®ć‚·ć‚°ćƒćƒćƒ£ć‚‚äø€éƒØå¤‰ć‚ć‚Šć¾ć—ćŸļ¼‰ć€‚äø‹ć‚Šć¦ć„ćå‰ć«**[ē§»č”Œć‚¬ć‚¤ćƒ‰](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**ć‚’čŖ­ć‚“ć§ćć ć•ć„ć€‚ +`Client` ćŒå—ć‘å–ć‚‹ć®ćÆć€URL(Streamable HTTP)、`StdioServerParameters`(stdio ć®ć‚µćƒ–ćƒ—ćƒ­ć‚»ć‚¹ļ¼‰ć€`sse_client(...)` ć®ć‚ˆć†ćŖćć‚Œä»„å¤–ć®ä»»ę„ć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆć®ć‚³ćƒ³ćƒ†ć‚­ć‚¹ćƒˆćƒžćƒćƒ¼ć‚øćƒ£ćƒ¼ć€ć¾ćŸćÆćƒ†ć‚¹ćƒˆć§ä½æć†ć‚µćƒ¼ćƒćƒ¼ć‚Ŗćƒ–ć‚øć‚§ć‚Æćƒˆćć®ć‚‚ć®ļ¼ˆć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆćŖć—ļ¼‰ć§ć™ć€‚`async with` ć«å…„ć‚‹ćØęŽ„ē¶šć—ć€ć‚µćƒ¼ćƒćƒ¼ćŒć©ć®äø–ä»£ć‚’č©±ć™ć‹ć«ć‹ć‹ć‚ć‚‰ćšćƒ—ćƒ­ćƒˆć‚³ćƒ«ćƒćƒ¼ć‚øćƒ§ćƒ³ć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć—ć¾ć™ć€‚ćć®å¾ŒćÆ `client.server_capabilities` と `client.protocol_version` ćŒćć®ć¾ć¾ä½æćˆć€ć‚µćƒ¼ćƒćƒ¼ćŒč‡Ŗčŗ«ć‚’åä¹—ć‚‹å “åˆćÆ `client.server_info` ć‚‚ä½æćˆć¾ć™ļ¼ˆ2026 å¹“äø–ä»£ć§ćÆč­˜åˆ„ęƒ…å ±ćŒēœē•„åÆčƒ½ćŖć®ć§ć€`Implementation | None` ć«ćŖć‚Šć¾ć—ćŸļ¼‰ć€‚v1 ć§ē™»éŒ²ć—ćŸć‚µćƒ³ćƒ—ćƒŖćƒ³ć‚°ćØć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć®ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚ÆćÆå¼•ćē¶šćå‹•ä½œć—ć¾ć™ļ¼ˆć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æć®ęœ¬ä½“ć«ćÆć€ć“ć®ćƒšćƒ¼ć‚øć®ć»ć‹ć®é …ē›®ćØåŒć˜ snake_case ćøć®å±žę€§åć®å¤‰ę›“ćŒåŠć³ć¾ć™ļ¼‰ć€‚åŠ ćˆć¦ 2026 å½¢å¼ć®ć€Œēµęžœć«åŸ‹ć‚č¾¼ć¾ć‚ŒćŸćƒŖć‚Æć‚Øć‚¹ćƒˆć€ļ¼ˆå¾Œčæ°ļ¼‰ć«ć‚‚åæœē­”ć™ć‚‹ć‚ˆć†ć«ćŖć‚Šć€1 ć¤ćšć¤ć§ćÆćŖćäø¦č”Œć—ć¦å®Ÿč”Œć•ć‚Œć¾ć™ć€‚ä½Žćƒ¬ćƒ™ćƒ«ć®ć‚¤ćƒ³ć‚æćƒ¼ćƒ•ć‚§ćƒ¼ć‚¹ćŒåæ…č¦ćŖäŗŗć®ćŸć‚ć« `ClientSession` ćÆä»Šć‚‚äø‹ć«ć‚ć‚Šć€`client.session` ć§å–ć‚Šå‡ŗć›ć¾ć™ć€‚ćŸć ć—ć“ć”ć‚‰ć‚‚å¤‰ć‚ć£ć¦ć„ć¾ć™ļ¼ˆę–°ć—ć„ćƒ‡ć‚£ć‚¹ćƒ‘ćƒƒćƒćƒ£ćƒ¼ć‚Øćƒ³ć‚øćƒ³ć®äøŠć§å‹•ćć€č‡Ŗčŗ«ć®ć‚·ć‚°ćƒćƒćƒ£ć‚‚äø€éƒØå¤‰ć‚ć‚Šć¾ć—ćŸļ¼‰ć€‚äø‹ć‚Šć¦ć„ćå‰ć«**[ē§»č”Œć‚¬ć‚¤ćƒ‰](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**ć‚’čŖ­ć‚“ć§ćć ć•ć„ć€‚ **[Client](client/index.md)** で瓹介し、**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ćƒˆćƒ©ćƒ³ć‚¹ćƒćƒ¼ćƒˆ](client/transports.md)**恧 4 ć¤ć®ęŽ„ē¶šå½¢ę…‹ć‚’ć€**[ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æ](client/callbacks.md)**ć§ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æćć®ć‚‚ć®ć‚’ę‰±ć„ć¾ć™ć€‚**[ćƒ†ć‚¹ćƒˆ](get-started/testing.md)**では、v1 恮 `create_connected_server_and_client_session()` ćƒ˜ćƒ«ćƒ‘ćƒ¼ć«ä»£ć‚ć‚‹ć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć®ćƒ‘ć‚æćƒ¼ćƒ³ć‚’ē¤ŗć—ć¾ć™ć€‚ @@ -166,11 +166,15 @@ Streamable HTTP では、2026 の経路に `Mcp-Session-Id` ćŒć‚ć‚Šć¾ć›ć‚“ ä»£ć‚ć‚Šć®ä»•ēµ„ćæćÆå‘¼ć³å‡ŗć—ć®å‘ćć‚’é€†ć«ć—ć¾ć™ć€‚ćƒ¦ćƒ¼ć‚¶ćƒ¼ć‹ć‚‰ä½•ć‹ć‚’åæ…č¦ćØć™ć‚‹ćƒ„ćƒ¼ćƒ«ćÆč³Ŗå•ć‚’ć€Œčæ”ć—ć€ļ¼ˆ`InputRequiredResult`ļ¼‰ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćÆć“ć‚Œć¾ć§ćØåŒć˜ć‚³ćƒ¼ćƒ«ćƒćƒƒć‚Æć§ćć‚Œć«ē­”ćˆć€ē­”ćˆć‚’ę·»ćˆć¦å‘¼ć³å‡ŗć—ćŒå†č©¦č”Œć•ć‚Œć¾ć™ć€‚ćć®ćƒ«ćƒ¼ćƒ—ćÆ `Client` ćŒå›žć—ć¾ć™ć€‚ć‚µćƒ¼ćƒćƒ¼å“ć§ēµęžœć‚’č‡Ŗåˆ†ć§ēµ„ćæē«‹ć¦ć‚‹ć“ćØćÆć‚ć£ćŸć«ć‚ć‚Šć¾ć›ć‚“ć€‚**[ä¾å­˜é–¢äæ‚](handlers/dependencies.md)**ćŒć‚„ć£ć¦ćć‚Œć‚‹ć‹ć‚‰ć§ć™ć€‚ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć‚’ `Resolve(ask_quantity)` ć§ę³Øé‡ˆć—ć¾ć™ļ¼ˆ`ask_quantity` ćÆč‡Ŗåˆ†ć§ę›øćę™®é€šć®é–¢ę•°ć§ć™ļ¼‰ć€‚ć™ć‚‹ćØ SDK ćÆęŽ„ē¶šćŒåÆ¾åæœć—ć¦ć„ć‚‹ä»•ēµ„ćæć€ć¤ć¾ć‚Šćƒ¬ć‚¬ć‚·ćƒ¼ć‚»ćƒƒć‚·ćƒ§ćƒ³ćŖć‚‰ćć®å “ć§é€ć‚‹ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ćƒŖć‚Æć‚Øć‚¹ćƒˆć€2026 ćŖć‚‰ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ć§č³Ŗå•ć—ć¾ć™ć€‚ćƒ„ćƒ¼ćƒ«ęœ¬ä½“ćÆ 1 つ、世代は且方です。 -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -ć“ć®ćƒ•ć‚”ć‚¤ćƒ« 1 ć¤ć«č¦ē‚¹ćŒč©°ć¾ć£ć¦ć„ć¾ć™ć€‚1 ć¤ć®ć‚µćƒ¼ćƒćƒ¼ć€`Resolve` ć«ę”Æćˆć‚‰ć‚ŒćŸ 1 ć¤ć®ćƒ„ćƒ¼ćƒ«ć€ćć—ć¦ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØę–°äø–ä»£ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®äø”ę–¹ćŒć‚¤ćƒ³ćƒ”ćƒ¢ćƒŖć§ē­”ćˆć‚’å—ć‘å–ć‚Šć¾ć™ć€‚ä»•ēµ„ćæļ¼ˆSDK ćŒå°å°ćØę¤œčØ¼ć‚’č”Œć† `request_state` を含む)は**[ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ćƒŖć‚Æć‚Øć‚¹ćƒˆ](handlers/multi-round-trip.md)**ćŒčŖ¬ę˜Žć—ć€č³Ŗå•ć®ć—ć‹ćŸćÆ**[ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³](handlers/elicitation.md)**ćŒę‰±ć„ć¾ć™ć€‚ +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +恓恮 2 ć¤ć®ćƒ•ć‚”ć‚¤ćƒ«ć«č¦ē‚¹ćŒč©°ć¾ć£ć¦ć„ć¾ć™ć€‚1 ć¤ć®ć‚µćƒ¼ćƒćƒ¼ć€`Resolve` ć«ę”Æćˆć‚‰ć‚ŒćŸ 1 ć¤ć®ćƒ„ćƒ¼ćƒ«ć€ćć—ć¦ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćØę–°äø–ä»£ć®ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć®äø”ę–¹ćŒć€åŒć˜å®Ÿč”Œäø­ć®ć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰ē­”ćˆć‚’å—ć‘å–ć‚Šć¾ć™ļ¼ˆ**[ćƒ¬ć‚¬ć‚·ćƒ¼ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćøć®åÆ¾åæœ](run/legacy-clients.md)**ćŒć“ć® 2 ć¤ć®ćƒ•ć‚”ć‚¤ćƒ«ć‚’é †ć«ćŸć©ć‚Šć¾ć™ļ¼‰ć€‚ä»•ēµ„ćæļ¼ˆSDK ćŒå°å°ćØę¤œčØ¼ć‚’č”Œć† `request_state` を含む)は**[ćƒžćƒ«ćƒćƒ©ć‚¦ćƒ³ćƒ‰ćƒˆćƒŖćƒƒćƒ—ćƒŖć‚Æć‚Øć‚¹ćƒˆ](handlers/multi-round-trip.md)**ćŒčŖ¬ę˜Žć—ć€č³Ŗå•ć®ć—ć‹ćŸćÆ**[ć‚ØćƒŖć‚·ćƒ†ćƒ¼ć‚·ćƒ§ćƒ³](handlers/elicitation.md)**ćŒę‰±ć„ć¾ć™ć€‚ !!! warning "ē§»ę¤ć—ćŸ v1 ć‚µćƒ¼ćƒćƒ¼ć®å‹•ä½œćŒå¤‰ć‚ć‚‹å”Æäø€ć®å “ę‰€" ęœ€åˆć«ć¶ć¤ć‹ć‚‹ć®ćÆč‡Ŗåˆ†ć®ćƒ†ć‚¹ćƒˆć§ć™ć€‚`Client(mcp)` ćÆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ v2 ć‚µćƒ¼ćƒćƒ¼ć«åÆ¾ć—ć¦ 2026-07-28 ć‚’ćƒć‚“ć‚·ć‚Øćƒ¼ćƒˆć™ć‚‹ć®ć§ć€`ctx.elicit()` ć‚’å‘¼ć¶ćƒ„ćƒ¼ćƒ«ćÆ v1 ć§é€šć£ć¦ć„ćŸćƒ†ć‚¹ćƒˆć§å¤±ę•—ć—ć¾ć™ć€‚č³Ŗå•ć‚’ `Resolve(...)` ćƒ‘ćƒ©ćƒ”ćƒ¼ć‚æćƒ¼ć«ē§»ć™ļ¼ˆäø–ä»£ć‚’ć¾ćŸć„ć§ä½æćˆć¾ć™ļ¼‰ć‹ć€ęœ¬å½“ć«ćƒ—ćƒƒć‚·ćƒ„åž‹ć®å‹•ä½œćŒę¬²ć—ć„ćŖć‚‰ćƒ†ć‚¹ćƒˆć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‚’ `mode="legacy"` ć«å›ŗå®šć—ć¦ćć ć•ć„ć€‚ diff --git a/i18n/ko/pages/advanced/apps.md b/i18n/ko/pages/advanced/apps.md index bb040b9d..2b10642a 100644 --- a/i18n/ko/pages/advanced/apps.md +++ b/i18n/ko/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDKėŠ” ģ“ė„¼ ė‚“ģž„ `Apps` ķ™•ģž„(`io.modelcontextprotocol/ui`)으딜 ģ œź³µķ•© ## ģ–¼źµ“ģ„ 가진 ģ‹œź³„ {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ HTML ģžģ²“ėŠ” ķ˜øģŠ¤ķŠøģ˜ `postMessage`넼 ģˆ˜ģ‹ ķ•˜ź³  결과넼 ķ‘œģ‹œķ•©ė‹ˆ ėŖØėøģ€ `content`넼 ģ½ź³ , iframeģ€ ģ‚¬ėžŒģ„ ģœ„ķ•œ ź²ƒģž…ė‹ˆė‹¤. UI넼 ģ§€ģ›ķ•˜ėŠ” ķ˜øģŠ¤ķŠøė„ ģ—¬ģ „ķžˆ ķ…ģŠ¤ķŠø 결과넼 ėŖØėøģ— ģ „ė‹¬ķ•˜ė©°, ķ…ģŠ¤ķŠø ģ „ģš© ķ“ė¼ģ“ģ–øķŠøėŠ” **오직** ź·ø ķ…ģŠ¤ķŠøė§Œ ė°›ģŠµė‹ˆė‹¤. ė”°ė¼ģ„œ ķ‘œģ¤€ ķŒØķ„“ģ€ ė„źµ¬ ķ•˜ė‚˜ģ— 답 ė‘˜ģž…ė‹ˆė‹¤. `get_time`ģ„ ė‹¤ģ‹œ ģ‚“ķŽ“ė³“ģ„øģš”. -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)`ėŠ” ķ“ė¼ģ“ģ–øķŠøź°€ `io.modelcontextprotocol/ui` ķ™•ģž„ģ„ ģ„ ģ–øķ–ˆź³  **ė™ģ‹œģ—** `mimeTypes` 설정에 `text/html;profile=mcp-app`ģ„ ė‚˜ģ—“ķ–ˆģ„ ė•Œė§Œ `True`ģž…ė‹ˆė‹¤. ģ“ ķ•„ė“œėŠ” ķ•„ģˆ˜ģ“ėÆ€ė”œ ģƒėžµķ•œ ķ“ė¼ģ“ģ–øķŠøėŠ” ķ•“ė‹¹ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ź°™ģ€ ķŒŒģ¼ģ˜ `main()`ģ“ ģ„ ģ–øķ•˜ėŠ” ź²ƒģ“ ė°”ė”œ ģ“ź²ƒģž…ė‹ˆė‹¤. ķ˜‘ģƒģ˜ ķ“ė¼ģ“ģ–øķŠø ģŖ½ ģ ˆė°˜ģ„ ģ„ ģ–øķ•˜ė©“ ķ’ė¶€ķ•œ ė‹µģ“ ėŒģ•„ģ˜µė‹ˆė‹¤. +`client_supports_apps(ctx)`ėŠ” ķ“ė¼ģ“ģ–øķŠøź°€ `io.modelcontextprotocol/ui` ķ™•ģž„ģ„ ģ„ ģ–øķ–ˆź³  **ė™ģ‹œģ—** `mimeTypes` 설정에 `text/html;profile=mcp-app`ģ„ ė‚˜ģ—“ķ–ˆģ„ ė•Œė§Œ `True`ģž…ė‹ˆė‹¤. ģ“ ķ•„ė“œėŠ” ķ•„ģˆ˜ģ“ėÆ€ė”œ ģƒėžµķ•œ ķ“ė¼ģ“ģ–øķŠøėŠ” ķ•“ė‹¹ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ķ˜‘ģƒģ˜ ķ“ė¼ģ“ģ–øķŠø ģŖ½ ģ ˆė°˜ģ€ ė‹¤ģŒź³¼ ź°™ģŠµė‹ˆė‹¤. + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py`넼 HTTP딜 ģ œź³µķ•œ ė‹¤ģŒ, 두 번째 ķ„°ėÆøė„ģ—ģ„œ ķ“ė¼ģ“ģ–øķŠøė„¼ ģ‹¤ķ–‰ķ•˜ģ„øģš”. + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +ķ’ė¶€ķ•œ ė‹µģ“ ėŒģ•„ģ™”ģŠµė‹ˆė‹¤. `Client` ķ˜øģ¶œģ—ģ„œ `extensions=[APPS_SUPPORT]`넼 빼멓 ź°™ģ€ ķ”„ė”œź·øėžØģ“ ėŒ€ģ‹  `The time is 2026-06-26T12:00:00Z.`ė¼ź³  ģ¶œė „ķ•˜ė©°, ķ…ģŠ¤ķŠø ģ „ģš© ķ“ė¼ģ“ģ–øķŠøź°€ 볓게 ė˜ėŠ” ź²ƒģ€ ģ–øģ œė‚˜ ģ“ź²ƒėæģž…ė‹ˆė‹¤. !!! warning `"[Rendered UI]"` ź°™ģ€ ģžė¦¬ ķ‘œģ‹œģžė„¼ ģœ ģ¼ķ•œ content딜 ė°˜ķ™˜ķ•˜ģ§€ ė§ˆģ„øģš”. ėŒ€ģ²“ ķ…ģŠ¤ķŠøź°€ 쓸모없다멓, ź·ø ė„źµ¬ėŠ” ėŖØė“  ķ…ģŠ¤ķŠø ģ „ģš© ķ“ė¼ģ“ģ–øķŠøģ™€ ėŖØėø ģžģ²“ģ— ģ“øėŖØģ—†ėŠ” ė„źµ¬ź°€ ė©ė‹ˆė‹¤. ģ œėŒ€ė”œ 된 ė¬øģž„ģ„ ģž‘ģ„±ķ•˜ģ„øģš”. diff --git a/i18n/ko/pages/advanced/extensions.md b/i18n/ko/pages/advanced/extensions.md index a269ffc2..6a26bcde 100644 --- a/i18n/ko/pages/advanced/extensions.md +++ b/i18n/ko/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # ķ™•ģž„ {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string 쓸모 ģžˆėŠ” ź°€ģž„ ģž‘ģ€ ķ™•ģž„ģ€ ė„źµ¬ ķ•˜ė‚˜ģ™€ 설정 ė§µ ķ•˜ė‚˜ģž…ė‹ˆė‹¤. -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()`ėŠ” `capabilities.extensions["com.example/stamps"]`에 ź“‘ź³ ė˜ėŠ” ź°’ģž…ė‹ˆė‹¤. 설정 ģ—†ģ“ ķ™•ģž„ģ„ ź“‘ź³ ķ•˜ė ¤ė©“ `{}`(źø°ė³øź°’)ģ„ ė°˜ķ™˜ķ•˜ģ„øģš”. * ķ™•ģž„ģ€ ģ„œė²„ė„¼ ģ ˆėŒ€ 전달받지 ģ•ŠģŠµė‹ˆė‹¤. 기여할 ė‚“ģš©ģ„ ė°ģ“ķ„°ė”œ ģ„ ģ–øķ•˜ź³ , `MCPServer`ź°€ ģ“ė„¼ ģ†Œė¹„ķ•©ė‹ˆė‹¤. 변경할 `self.server` ź°™ģ€ ź²ƒģ€ ģ—†ģŠµė‹ˆė‹¤. -그리고 `main()`ģ“ ź·ø ģ¦ź±°ģž…ė‹ˆė‹¤. `mcp`에 ė°”ė”œ ģ—°ź²°ķ•˜ėŠ” ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠøģž…ė‹ˆė‹¤. +HTTP딜 ģ„œė¹„ģŠ¤ķ•˜ė©“, ķ“ė¼ģ“ģ–øķŠøź°€ ź³§ ź·ø ģ¦ź±°ź°€ ė©ė‹ˆė‹¤. -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +ģ“ ķŽ˜ģ“ģ§€ģ˜ ėŖØė“  `server.py`ėŠ” ģ“ ėŖ…ė ¹ģœ¼ė”œ ģ„œė¹„ģŠ¤ķ•˜ź³ , ėŖØė“  `client.py`ėŠ” 두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`딜 ź·ø ģ˜†ģ—ģ„œ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. + ### ģžģ²“ ė©”ģ„œė“œ ģ œź³µķ•˜źø° {#serving-your-own-methods} ķ™•ģž„ģ€ **새딜욓 ģš”ģ²­ ė©”ģ„œė“œ**넼 ė“±ė”ķ•  수 ģžˆģŠµė‹ˆė‹¤. ģ‚¬ģ–‘ģ˜ ė™ģ‚¬ ģ˜†ģ—ģ„œ ķ•Øź»˜ ģ„œė¹„ģŠ¤ė˜ėŠ” ģžģ²“ ė™ģ‚¬ģž…ė‹ˆė‹¤. -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### ķ“ė¼ģ“ģ–øķŠø ģø” {#the-client-side} -ź°™ģ€ ķŒŒģ¼ģ˜ `main()`ģ“ ķ“ė¼ģ“ģ–øķŠø ģŖ½ ģ“ģ•¼źø°ģ˜ ģ „ė¶€ģ“ė©°, 두 ė¶€ė¶„ģ„ 모두 ė‹“ź³  ģžˆģŠµė‹ˆė‹¤. +ķ“ė¼ģ“ģ–øķŠøėŠ” ė…ė¦½ėœ ķ”„ė”œź·øėžØģ“ė©°, ķ“ė¼ģ“ģ–øķŠø ģŖ½ ģ“ģ•¼źø°ģ˜ 두 ė¶€ė¶„ģ„ 모두 ė‹“ź³  ģžˆģŠµė‹ˆė‹¤. -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])`ź°€ ķ™•ģž„ģ„ ģ„ ģ–øķ•©ė‹ˆė‹¤. ģ„ ģ–øģ€ `ClientCapabilities.extensions`ź°€ ė©ė‹ˆė‹¤. 2026-07-28 ģ—°ź²°ģ—ģ„œėŠ” ģ“ ė§µģ“ ģš”ģ²­ė³„ `_meta` ė“‰ķˆ¬ģ— 실려 ģ“ė™ķ•˜ėÆ€ė”œ ģ„œė²„ėŠ” **ėŖØė“ ** ģš”ģ²­ģ—ģ„œ ģ“ė„¼ ė“…ė‹ˆė‹¤. ė ˆź±°ģ‹œ ģ—°ź²°ģ—ģ„œėŠ” `initialize` ķ•øė“œģ…°ģ“ķ¬ģ— ģ‹¤ė¦½ė‹ˆė‹¤. ģ„œė²„ ģ½”ė“œėŠ” ģ–“ėŠ ģŖ½ģøģ§€ ģ‹ ź²½ ģ“°ģ§€ ģ•ŠģŠµė‹ˆė‹¤. `require_client_extension(ctx, ...)`와 `ctx.session.check_client_capability(...)`ėŠ” 두 경딜 ėŖØė‘ģ—ģ„œ ģ˜¬ė°”ė„ø 출처넼 ģ½ģŠµė‹ˆė‹¤. * ė²¤ė” ė©”ģ„œė“œėŠ” ķ•œ 계층 ģ•„ėž˜ģø `client.session.send_request(...)`딜 ė‚“ė ¤ź°‘ė‹ˆė‹¤. `Client`ėŠ” 사양 ė™ģ‚¬ģ— ėŒ€ķ•“ģ„œė§Œ ģ¼źø‰ ė©”ģ„œė“œė„¼ ź°–ģ¶„ė‹ˆė‹¤. `send_request`ėŠ” ėŖØė“  `Request` ģ„œėøŒķ“ėž˜ģŠ¤ė„¼ ė°›ģœ¼ėÆ€ė”œ ė²¤ė” ģš”ģ²­ģ€ ź·øėŒ€ė”œ ķ†µź³¼ķ•©ė‹ˆė‹¤. +* `SearchRequest`와 여기에 ģ‹¤ė¦¬ėŠ” 두 ėŖØėøģ€ ķ™•ģž„ģ˜ ģ™€ģ“ģ–“ ź³„ģ•½ģ“ėÆ€ė”œ ķ“ė¼ģ“ģ–øķŠøź°€ 직접 ģ„ ģ–øķ•©ė‹ˆė‹¤. 공개 ė°°ķ¬ė˜ėŠ” ķ™•ģž„ģ“ė¼ė©“ ģ–‘ģŖ½ģ“ ķ•Øź»˜ ģž„ķ¬ķŠøķ•˜ėŠ” ķŒØķ‚¤ģ§€ģ— ė‹“ģ•„ ģ œź³µķ•©ė‹ˆė‹¤. ### `tools/call` ź°€ė”œģ±„źø° {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## ķ“ė¼ģ“ģ–øķŠø ķ™•ģž„ ģ‚¬ģš©ķ•˜źø° {#using-a-client-extension} -**ķ“ė¼ģ“ģ–øķŠø ķ™•ģž„**ģ€ ģ†Œė¹„ķ•˜ėŠ” ģŖ½ģ—ģ„œ ė³ø ź°™ģ€ ź³„ģ•½ģœ¼ė”œ, ķ•˜ė‚˜ģ˜ ģ‹ė³„ģž ģ•„ėž˜ ė¬¶ģø ķ“ė¼ģ“ģ–øķŠø ģø” ė™ģž‘ģ˜ ė¬¶ģŒģž…ė‹ˆė‹¤. `Client(extensions=[...])`에 ģøģŠ¤ķ„“ģŠ¤ė„¼ ģ „ė‹¬ķ•˜ź³  ķ‰ģ†Œģ²˜ėŸ¼ ė„źµ¬ė„¼ ķ˜øģ¶œķ•˜ģ„øģš”. +**ķ“ė¼ģ“ģ–øķŠø ķ™•ģž„**ģ€ ģ†Œė¹„ķ•˜ėŠ” ģŖ½ģ—ģ„œ ė³ø ź°™ģ€ ź³„ģ•½ģœ¼ė”œ, ķ•˜ė‚˜ģ˜ ģ‹ė³„ģž ģ•„ėž˜ ė¬¶ģø ķ“ė¼ģ“ģ–øķŠø ģø” ė™ģž‘ģ˜ ė¬¶ģŒģž…ė‹ˆė‹¤. ģ—¬źø°ģ„œ ģ„œė²„ėŠ” `buy`에 ģƒķ’ˆ ėŒ€ģ‹  ģ •ģ‚°ķ•  ģ˜ģˆ˜ģ¦ģœ¼ė”œ ģ‘ė‹µķ•˜ė©°, ź·øź²ƒė„ ķ™•ģž„ģ„ ģ„ ģ–øķ•œ ķ“ė¼ģ“ģ–øķŠøģ—ź²Œė§Œ ź·øė ‡ź²Œ ķ•©ė‹ˆė‹¤. -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +ķ“ė¼ģ“ģ–øķŠøģ—ģ„œėŠ” `Client(extensions=[...])`에 ģøģŠ¤ķ„“ģŠ¤ė„¼ ģ „ė‹¬ķ•˜ź³  ķ‰ģ†Œģ²˜ėŸ¼ ė„źµ¬ė„¼ ķ˜øģ¶œķ•˜ģ„øģš”. + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)`ģ€ 다넸 ėŖØė“  호출처럼 ķ‰ė²”ķ•œ `CallToolResult`넼 ė°˜ķ™˜ķ•©ė‹ˆė‹¤. ķ™•ģž„ģ“ 바꾼 ź²ƒģ€ ģ“ė ‡ģŠµė‹ˆė‹¤. ģ“ģ œ ģ„œė²„ėŠ” `buy`에 ģµœģ¢… ź²°ź³¼ ėŒ€ģ‹  `receipt` **ź²°ź³¼ ķ˜•ķƒœ**딜 ģ‘ė‹µķ•  수 ģžˆź³ , `call_tool`ģ“ ė°˜ķ™˜ķ•˜źø° 전에 `Receipts`ź°€ ģ“ė„¼ ė§ˆė¬“ė¦¬ķ•©ė‹ˆė‹¤(ģ—¬źø°ģ„œėŠ” ķ›„ģ† 호출딜 ģ˜ģˆ˜ģ¦ģ„ ģ •ģ‚°ķ•©ė‹ˆė‹¤). 호출 ģ§€ģ ģ—ģ„œėŠ” ģ•„ė¬“ź²ƒė„ ė‹¬ė¼ģ§€ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ķ™•ģž„ģ„ 빼멓 ģ“ 중 ģ–“ė–¤ ź²ƒė„ ģ”“ģž¬ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ„œė²„ģ˜ ź“€ė¬øģ€ ķ™•ģž„ģ„ ģ„ ģ–øķ•˜ģ§€ ģ•Šģ€ ķ“ė¼ģ“ģ–øķŠøė„¼ ź±°ė¶€ķ•˜ź³ (오넘 -32021), ź“€ė¬øģ„ ź±“ė„ˆė›°ėŠ” ģ„œė²„ź°€ 볓낸 ķ“ė ˆģž„ėœ ķ˜•ķƒœėŠ” ģøģ‹ė˜ģ§€ ģ•Šģ€ `resultType`에 ėŒ€ķ•“ ģ‚¬ģ–‘ģ“ ģš”źµ¬ķ•˜ėŠ” ź·øėŒ€ė”œ ź²€ģ¦ģ— ģ‹¤ķŒØķ•©ė‹ˆė‹¤. ģ™€ģ“ģ–“ ģ–‘ ė ėŖØė‘ģ—ģ„œ 기본적으딜 꺼져 ģžˆģŠµė‹ˆė‹¤. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ķ“ė¼ģ“ģ–øķŠø ķ™•ģž„ ģž‘ģ„±ķ•˜źø° {#writing-a-client-extension} `ClientExtension`ģ„ ģ„œėøŒķ“ėž˜ģ‹±ķ•˜ź³  ķ•„ģš”ķ•œ 것만 ģž¬ģ •ģ˜ķ•˜ģ„øģš”. źø°ģ—¬ ģ¢…ė„˜ėŠ” 세 ź°€ģ§€ģ“ė©° 각각 źø°ė³ø źµ¬ķ˜„ģ“ ģžˆģŠµė‹ˆė‹¤. `settings()`, `claims()`, `notifications()`ģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * ģ‹ė³„ģžėŠ” ģ„œė²„ģ˜ 것과 ź°™ģ€ ė¬øė²•ģ„ ė”°ė„“ė©° ķ“ėž˜ģŠ¤ź°€ ģ •ģ˜ė  ė•Œ ź²€ģ¦ė©ė‹ˆė‹¤. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### ķ™•ģž„ ė™ģ‚¬ {#extension-verbs} -ķ™•ģž„ģ˜ ģžģ²“ ģš”ģ²­ ė©”ģ„œė“œģ—ėŠ” ķ“ė¼ģ“ģ–øķŠø ģø” ė“±ė”ģ“ ķ•„ģš” ģ—†ģŠµė‹ˆė‹¤. ė²¤ė” ģš”ģ²­ ķƒ€ģž…ģ€ `mcp.types.Request`넼 ģ„œėøŒķ“ėž˜ģ‹±ķ•˜ź³ , [ģžģ²“ ė©”ģ„œė“œ ģ œź³µķ•˜źø°](#serving-your-own-methods)ģ—ģ„œģ²˜ėŸ¼ `client.session.send_request`넼 ź±°ģ¹©ė‹ˆė‹¤. ķ•œ 가지가 ė” ģžˆģŠµė‹ˆė‹¤. params 키가 `Mcp-Name` ķ—¤ė”ģ— 실려야 ķ•  ė•Œ(tasks ź°™ģ€ ķ™•ģž„ ģ‚¬ģ–‘ģ€ ģžģ‹ ģ˜ ė™ģ‚¬ģ— ģ“ė„¼ ģš”źµ¬ķ•©ė‹ˆė‹¤) ģš”ģ²­ ķƒ€ģž…ģ“ `name_param`ģ„ ģ„ ģ–øķ•©ė‹ˆė‹¤. +ķ™•ģž„ģ˜ ģžģ²“ ģš”ģ²­ ė©”ģ„œė“œģ—ėŠ” ķ“ė¼ģ“ģ–øķŠø ģø” ė“±ė”ģ“ ķ•„ģš” ģ—†ģŠµė‹ˆė‹¤. ė²¤ė” ģš”ģ²­ ķƒ€ģž…ģ€ `mcp.types.Request`넼 ģ„œėøŒķ“ėž˜ģ‹±ķ•˜ź³ , [ģžģ²“ ė©”ģ„œė“œ ģ œź³µķ•˜źø°](#serving-your-own-methods)ģ—ģ„œģ²˜ėŸ¼ `client.session.send_request`넼 ź±°ģ¹©ė‹ˆė‹¤. ķ™•ģž„ģ“ ģ“ė¦„ ė¶™ģ€ ģž‘ģ—…ģ— ź“€ķ•œ ė™ģ‚¬ ķ•˜ė‚˜ė„¼ ģ„œė¹„ģŠ¤ķ•˜ėŠ” ģ„œė²„ė„¼ 예딜 들얓 ė³“ź² ģŠµė‹ˆė‹¤. -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +ķ“ė¼ģ“ģ–øķŠø ģŖ½ģ—ėŠ” ķ•œ 가지가 ģ¶”ź°€ė©ė‹ˆė‹¤. params 키가 `Mcp-Name` ķ—¤ė”ģ— 실려야 ķ•  ė•Œ(tasks ź°™ģ€ ķ™•ģž„ ģ‚¬ģ–‘ģ€ ģžģ‹ ģ˜ ė™ģ‚¬ģ— ģ“ė„¼ ģš”źµ¬ķ•©ė‹ˆė‹¤) ģš”ģ²­ ķƒ€ģž…ģ“ `name_param`ģ„ ģ„ ģ–øķ•©ė‹ˆė‹¤. + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + ģ„øģ…˜ģ€ ėŖØė“  전솔 ź²½ė”œģ—ģ„œ `params["jobId"]`넼 `Mcp-Name`에 ė°˜ģ˜ķ•˜ė©°, ź°’ģ“ ģ—†ģœ¼ė©“ ķ•„ģˆ˜ ķ—¤ė”ė„¼ ģ”°ģš©ķžˆ ė¹ ėœØė¦¬ėŠ” ėŒ€ģ‹  ėŖ…ģ‹œģ ģœ¼ė”œ ģ‹¤ķŒØķ•©ė‹ˆė‹¤. ## ķ™•ģž„ģ“ ķ•  수 ģ—†ėŠ” 것 {#what-an-extension-cannot-do} diff --git a/i18n/ko/pages/advanced/low-level-server.md b/i18n/ko/pages/advanced/low-level-server.md index 811989ad..218397f0 100644 --- a/i18n/ko/pages/advanced/low-level-server.md +++ b/i18n/ko/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # ģ €ģˆ˜ģ¤€ Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 직접 ķ•“ 볓기 {#try-it} -ģ“ė²ˆģ—ėŠ” Inspector넼 쓸 수 ģ—†ģŠµė‹ˆė‹¤. `mcp dev`와 `mcp run`ģ€ `MCPServer`만 ė°›ģŠµė‹ˆė‹¤. ģøė©”ėŖØė¦¬ `Client`ėŠ” ģƒź“€ķ•˜ģ§€ ģ•Šģœ¼ė©°, `MCPServer`넼 ė°›ėŠ” 것과 ė˜‘ź°™ģ“ ģ €ģˆ˜ģ¤€ `Server`ė„ ė°›ģŠµė‹ˆė‹¤. +`mcp dev`와 `mcp run`ģ€ `MCPServer`만 ė°›ģœ¼ėÆ€ė”œ ģ“ ģ„œė²„ėŠ” 직접 ė„ģ›Œģ•¼ ķ•©ė‹ˆė‹¤. `server.py`ģ˜ ė§ˆģ§€ė§‰ ģ¤„ģ“ ģ“ ģ„œė²„ė”œ ķ‰ė²”ķ•œ ASGI ģ•±ģ„ ė§Œė“¤ź³ , uvicornģ“ ź·ø ģ•±ģ„ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspectorė“  다넸 ģ–“ė–¤ ķ“ė¼ģ“ģ–øķŠøė“  `http://localhost:8000/mcp`딜 ķ–„ķ•˜ź²Œ ķ•˜ģ„øģš”. + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content`ź°€ `None`ģž…ė‹ˆė‹¤. ź³ ģˆ˜ģ¤€ ģ„œė²„ėŠ” `-> str` ė°˜ķ™˜ź°’ģ„ `{"result": ...}`딜 감싸 ģ£¼ģ§€ė§Œ, ģ—¬źø°ģ„œėŠ” 직접 ė§Œė“¤ģ§€ ģ•Šģ€ ź²ƒģ„ ėŒ€ģ‹  ė§Œė“¤ģ–“ ģ£¼ėŠ” ź³³ģ“ ģ—†ģŠµė‹ˆė‹¤. * `list_tools`ėŠ” **직접** ģž…ė „ķ•œ ģŠ¤ķ‚¤ė§ˆė„¼ źø€ģž ķ•˜ė‚˜ź¹Œģ§€ ź·øėŒ€ė”œ ė°˜ķ™˜ķ•©ė‹ˆė‹¤. ź³ ģˆ˜ģ¤€ ė²„ģ „ģ—ėŠ” ėŖØė“  ģ†ģ„±ģ— `"title": "Query"`ź°€, ė£ØķŠøģ— `"title": "search_booksArguments"`ź°€ ģžˆģ—ˆģŠµė‹ˆė‹¤. Pydanticģ“ 남긓 ķ”ģ ģž…ė‹ˆė‹¤. ģ—¬źø°ģ„œėŠ” ģ™€ģ“ģ–“ģ— 실린 ź²ƒģ“ė¼ė©“ ģ „ė¶€ 직접 ė„£ģ€ ź²ƒģž…ė‹ˆė‹¤. +ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” uvicornź³¼ ķ¬ķŠøė„¼ ź±“ė„ˆėœė‹ˆė‹¤. `Client(server)`ėŠ” `MCPServer`넼 ė°›ėŠ” 것과 ė˜‘ź°™ģ“ ģ €ģˆ˜ģ¤€ `Server`ė„ ķ”„ė”œģ„øģŠ¤ ģ•ˆģ—ģ„œ ė°›ģœ¼ė©°, **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)**ź°€ ė°”ė”œ ź·ø ķŒØķ„“ģž…ė‹ˆė‹¤. + ## ģžė™ ź²€ģ¦ ģ—†ģŒ {#nothing-is-checked-for-you} `MCPServer`ėŠ” ķ•Øģˆ˜ź°€ ģ‹¤ķ–‰ė˜źø°ė„ 전에, ģƒģ„±ķ•œ ģŠ¤ķ‚¤ė§ˆģ— ķ˜øģ¶œģ„ ėŒ€ģ”°ķ•“ ź²€ģ¦ķ•˜ģ—¬ ģž˜ėŖ»ėœ ģøģˆ˜ė„¼ ź±°ė¶€ķ•©ė‹ˆė‹¤(**[ė„źµ¬](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)`ėŠ” ģ–“ė–¤ ė©”ģ„œė“œė“  ģ œź³µķ•©ė‹ˆė‹¤. `initialize`ėŠ” ģ˜ˆģ•½ė˜ģ–“ ģžˆģŠµė‹ˆė‹¤. * `Server`ź°€ ģ•Œė¦¬ėŠ” źø°ėŠ„ģ€ ė“±ė”ķ•œ ķ•øė“¤ėŸ¬ģ—ģ„œ ė„ģ¶œė©ė‹ˆė‹¤. -`Client(server)`ź°€ 두 ģ„œė²„ė„¼ ė˜‘ź°™ģ“ 다룬 ź²ƒģ€ ė‘˜ģ“ **ź°™ģ€** ķ”„ė”œķ† ģ½œģ“źø° ė•Œė¬øģ“ė©°, ė°”ė”œ ź·ø ģ ģ“ ķ•µģ‹¬ģž…ė‹ˆė‹¤. ź·øė‹¤ģŒ ģ•„ėž˜ ź³„ģøµģ€ ķ“ėž˜ģŠ¤ź°€ ģ•„ė‹™ė‹ˆė‹¤. ė°”ė”œ **[미들웨얓](middleware.md)**ģž…ė‹ˆė‹¤. +ķ“ė¼ģ“ģ–øķŠøź°€ 두 ģ„œė²„ė„¼ ė˜‘ź°™ģ“ 다룬 ź²ƒģ€ ė‘˜ģ“ **ź°™ģ€** ķ”„ė”œķ† ģ½œģ“źø° ė•Œė¬øģ“ė©°, ė°”ė”œ ź·ø ģ ģ“ ķ•µģ‹¬ģž…ė‹ˆė‹¤. ź·øė‹¤ģŒ ģ•„ėž˜ ź³„ģøµģ€ ķ“ėž˜ģŠ¤ź°€ ģ•„ė‹™ė‹ˆė‹¤. ė°”ė”œ **[미들웨얓](middleware.md)**ģž…ė‹ˆė‹¤. diff --git a/i18n/ko/pages/advanced/pagination.md b/i18n/ko/pages/advanced/pagination.md index def248d8..9d5ff400 100644 --- a/i18n/ko/pages/advanced/pagination.md +++ b/i18n/ko/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # ķŽ˜ģ“ģ§€ė„¤ģ“ģ…˜ {#pagination} @@ -31,9 +31,13 @@ translation: ### 직접 ķ•“ 볓기 {#try-it} -`Client(server)`ėŠ” `MCPServer`에 ģ—°ź²°ķ•  ė•Œģ™€ ė˜‘ź°™ģ“ ģ €ģˆ˜ģ¤€ `Server`에 ģøė©”ėŖØė¦¬ė”œ ģ—°ź²°ķ•©ė‹ˆė‹¤. +`mcp run`ģ€ `MCPServer`만 ė°›ģœ¼ėÆ€ė”œ ģ“ ģ„œė²„ėŠ” 직접 ė„ģ›Œģ•¼ ķ•©ė‹ˆė‹¤. `server.py`ģ˜ ė§ˆģ§€ė§‰ ģ¤„ģ“ `Server`딜 ķ‰ė²”ķ•œ ASGI ģ•±ģ„ ė§Œė“¤ź³ , uvicornģ“ ź·ø ģ•±ģ„ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. -ģøģž ģ—†ģ“ `list_resources()`넼 ķ˜øģ¶œķ•˜ģ„øģš”. `book-1`부터 `book-10`ź¹Œģ§€ ė¦¬ģ†ŒģŠ¤ 10ź°œź°€ ėŒģ•„ģ˜¤ź³ , `next_cursor`ėŠ” ė¬øģžģ—“ `"10"`ģž…ė‹ˆė‹¤. +```console +uvicorn server:app --port 8000 +``` + +아묓 ķ“ė¼ģ“ģ–øķŠø(**[ķ“ė¼ģ“ģ–øķŠø](../client/index.md)** ė˜ėŠ” Inspector)ė‚˜ `http://localhost:8000/mcp`에 ģ—°ź²°ķ•˜ź³  ģøģž ģ—†ģ“ `list_resources()`넼 ķ˜øģ¶œķ•˜ģ„øģš”. `book-1`부터 `book-10`ź¹Œģ§€ ė¦¬ģ†ŒģŠ¤ 10ź°œź°€ ėŒģ•„ģ˜¤ź³ , `next_cursor`ėŠ” ė¬øģžģ—“ `"10"`ģž…ė‹ˆė‹¤. ģ“ė„¼ `list_resources(cursor="10")`으딜 ė‹¤ģ‹œ ė„˜źø°ė©“ 첫 번째 ė¦¬ģ†ŒģŠ¤ėŠ” `book-11`ģ“ź³ , 새 `next_cursor`ėŠ” `"20"`ģž…ė‹ˆė‹¤. @@ -43,7 +47,7 @@ translation: `Client`ģ˜ ėŖØė“  `list_*` ė©”ģ„œė“œ(`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`)ėŠ” `cursor=` ķ‚¤ģ›Œė“œė„¼ ė°›ģŠµė‹ˆė‹¤. ķŽ˜ģ“ģ§€ė”œ ė‚˜ė‰œ ėŖ©ė”ģ„ ģ „ė¶€ ź°€ģ øģ˜¤ėŠ” ź²ƒģ€ `while True` ķ•˜ė‚˜ė©“ ė©ė‹ˆė‹¤. -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ translation: * `next_cursor`넼 ķ™•ģøķ•˜źø° **전에** 결과넼 ė§ė¶™ģ“ģ„øģš”. ė§ˆģ§€ė§‰ ķŽ˜ģ“ģ§€ģ—ė„ ė¦¬ģ†ŒģŠ¤ź°€ ģžˆģŠµė‹ˆė‹¤. * `next_cursor is None`ģ“ ģ¢…ė£Œ ģ”°ź±“ģž…ė‹ˆė‹¤. ź·ø ģ™øģ˜ ź°’ģ€ ģ†ėŒ€ģ§€ ģ•Šź³  ź·øėŒ€ė”œ `cursor=`에 ė‹¤ģ‹œ ė„£ģŠµė‹ˆė‹¤. -ģ“ ķŒŒģ¼ģ˜ `main()`ģ„ ģ‹¤ķ–‰ķ•˜ė©“ `100 resources`ź°€ ģ¶œė „ė©ė‹ˆė‹¤. ģ—“ ź°œģ”© ģ—“ ķŽ˜ģ“ģ§€ź°€, ķŽ˜ģ“ģ§€ź°€ ģ—“ ź°œė¼ėŠ” 사실씰차 ėŖØė„“ėŠ” 루프에 ģ˜ķ•“ ķ•˜ė‚˜ė”œ ģ“ģ–“ 붙여진 ź²°ź³¼ģž…ė‹ˆė‹¤. +uvicornģ“ `server.py`넼 ź³„ģ† ė„ģ›Œ ė‘” ģƒķƒœģ—ģ„œ 두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`넼 ģ‹¤ķ–‰ķ•˜ģ„øģš”. `100 resources`ź°€ ģ¶œė „ė©ė‹ˆė‹¤. ģ—“ ź°œģ”© ģ—“ ķŽ˜ģ“ģ§€ź°€, ķŽ˜ģ“ģ§€ź°€ ģ—“ ź°œė¼ėŠ” 사실씰차 ėŖØė„“ėŠ” 루프넼 통핓 ķ•˜ė‚˜ė”œ ģ“ģ–“ 붙여진 ź²°ź³¼ģž…ė‹ˆė‹¤. ģ“ ė£Øķ”„ėŠ” **[ķ“ė¼ģ“ģ–øķŠø](../client/index.md)**ģ—ģ„œ ėŖØė“  `list_*` ė™ģ‚¬ģ— ėŒ€ķ•“ 볓여 ģ£¼ėŠ” 것과 ź°™ģ€ ė£Øķ”„ģ“ė©°, ķŽ˜ģ“ģ§€ė„¼ ė‚˜ėˆ„ģ§€ ģ•ŠėŠ” ģ„œė²„ģ— ėŒ€ķ•“ģ„œė„ ė¹„ģš©ģ“ 들지 ģ•ŠģŠµė‹ˆė‹¤. 첫 ģ‘ė‹µģ—ģ„œ `next_cursor`ź°€ `None`ģ“ėÆ€ė”œ ė£Øķ”„ėŠ” ķ•œ 번만 ė•ė‹ˆė‹¤. diff --git a/i18n/ko/pages/client/caching.md b/i18n/ko/pages/client/caching.md index 026272c2..50625fa1 100644 --- a/i18n/ko/pages/client/caching.md +++ b/i18n/ko/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # 캐싱 ķžŒķŠø {#caching-hints} @@ -30,7 +30,7 @@ translation: ģ €ģˆ˜ģ¤€ `Server`ģ—ģ„œėŠ” ķ•øė“¤ėŸ¬ź°€ 결과넼 직접 ģ”°ė¦½ķ•˜ė©°, `ttl_ms` / `cache_scope`ėŠ” ź²°ź³¼ ėŖØėøģ˜ ķ•„ė“œģ¼ ėæģž…ė‹ˆė‹¤. ģ“ ķ•„ė“œė„¼ ėŖ…ģ‹œģ ģœ¼ė”œ ģ„¤ģ •ķ•œ ķ•øė“¤ėŸ¬ėŠ” ķ•„ė“œ ė‹Øģœ„ė”œ ģ–øģ œė‚˜ ģƒģ„±ģž 맵볓다 ģš°ģ„ ķ•©ė‹ˆė‹¤. -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: 2026-07-28 ģ„øģ…˜ģ—ģ„œėŠ” `Client`ź°€ ķžŒķŠøė„¼ ėŒ€ģ‹  ė”°ė¦…ė‹ˆė‹¤. 기본으딜 켜져 ģžˆėŠ” ė‚“ģž„ ģ‘ė‹µ ģŗģ‹œź°€ ģžˆźø° ė•Œė¬øģž…ė‹ˆė‹¤. `ttlMs`넼 싣고 ė„ģ°©ķ•œ ź²°ź³¼ėŠ” ģ €ģž„ė˜ź³ , ź·ø TTL ģ•ˆģ— ė™ģ¼ķ•œ ķ˜øģ¶œģ“ 오멓 왕복 ģ—†ģ“ ģŗģ‹œģ—ģ„œ ģ œź³µė©ė‹ˆė‹¤. ķžŒķŠøź°€ **ģ—†ėŠ”** ź²°ź³¼ėŠ” ģŗģ‹œė˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ķžŒķŠø ģ—†ėŠ” ź²°ź³¼ģ—ėŠ” `CacheConfig.default_ttl_ms`ź°€ ģ ģš©ė˜ėŠ”ė° źø°ė³øź°’ģ“ `0`(ģ¦‰ģ‹œ 만료)ģ“ėÆ€ė”œ, ģ•„ė¬“ź²ƒė„ ģ„ ģ–øķ•˜ģ§€ ģ•ŠėŠ” ģ„œė²„ėŠ” 늘 ź·øėž¬ė“Æ ķ˜øģ¶œė§ˆė‹¤ ģš”ģ²­ģ“ ģ˜¤ėŠ” ķŠøėž˜ķ”½ģ„ ź·øėŒ€ė”œ 볓게 ė©ė‹ˆė‹¤. -```python title="client.py" hl_lines="33 35 38" +ģ“ ė™ģž‘ģ„ 직접 ķ™•ģøķ•˜ė ¤ė©“ ģ•ž ģ ˆģ˜ `server.py`넼 uvicorn으딜 ė„ģš°ģ„øģš”(ė§ˆģ§€ė§‰ ģ¤„ģ“ ASGI ģ•±ģ„ ė§Œė“­ė‹ˆė‹¤). ķ•øė“¤ėŸ¬ėŠ” ģ‹¤ģ œė”œ 실행될 ė•Œė§ˆė‹¤ ķ•œ ģ¤„ģ„ ģ¶œė „ķ•©ė‹ˆė‹¤. + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`넼 ģ‹¤ķ–‰ķ•˜ģ„øģš”. 첫 번째 결과가 싣고 온 ķžŒķŠø, 즉 ķ•øė“¤ėŸ¬ģ˜ `ttlMs`와 ė§µģ˜ `cacheScope`넼 ė‚˜ėž€ķžˆ ģ¶œė „ķ•©ė‹ˆė‹¤. + +```text +1000 public +``` + +ė‚˜ėØøģ§€ėŠ” ģ„œė²„ ģŖ½ ķ„°ėÆøė„ģ“ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. uvicornģ˜ ģš”ģ²­ 딜그 ģ‚¬ģ“ė”œ `tools/list served`ź°€ 세 번 ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. + ķ˜øģ¶œģ€ 네 번, ģ„œė²„ģ—ģ„œ ź°€ģ øģ˜Ø ź²ƒģ€ 세 ė²ˆģž…ė‹ˆė‹¤. 두 번째 ķ˜øģ¶œģ€ ģ‹ ģ„ ķ•œ ķ•­ėŖ©ģ„ ģ°¾ģ•˜ź³  ģ„œė²„ģ— ė„ė‹¬ķ•˜ģ§€ ģ•Šģ•˜ģŠµė‹ˆė‹¤. (ģ£¼ģž…ėœ) ģ‹œź³„ė„¼ TTL ė„ˆėØøė”œ ģ§„ķ–‰ģ‹œķ‚¤ģž 세 번째 ķ˜øģ¶œģ€ ė‹¤ģ‹œ 가져왔고, 네 번째 ķ˜øģ¶œģ€ `cache_mode="refresh"`넼 ģ§€ģ •ķ–ˆģŠµė‹ˆė‹¤. ģ“ ķ‚¤ģ›Œė“œ ģøģžėŠ” 캐싱 ė™ģ‚¬ 다섯 개(`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`)에 ģžˆģŠµė‹ˆė‹¤. * `"use"`(źø°ė³øź°’)ėŠ” ģ‹ ģ„ ķ•œ ķ•­ėŖ©ģ“ ģžˆģœ¼ė©“ 핓당 ķ•­ėŖ©ģ„ ģ œź³µķ•˜ź³ , ģ—†ģœ¼ė©“ ź°€ģ øģ™€ģ„œ ģ €ģž„ķ•©ė‹ˆė‹¤. @@ -56,7 +70,7 @@ translation: `"use"` ģœ„ģ— ź·œģ¹™ģ“ ķ•˜ė‚˜ ė” ģžˆģŠµė‹ˆė‹¤. **`meta`넼 ė‹“ģ€ ķ˜øģ¶œģ€ ģ–øģ œė‚˜ ģ„œė²„ģ— ė„ė‹¬ķ•©ė‹ˆė‹¤.** `meta`ź°€ ģ„¤ģ •ėœ ģš”ģ²­(진행넠 토큰, 추적 ķ•„ė“œ)ģ€ ģ‹¤ģ œė”œ ģ „ģ†”ė˜ėŠ” ģš”ģ²­ģ„ źø°ėŒ€ķ•˜ėÆ€ė”œ, `cache_mode="use"`ģ—ģ„œėŠ” `"refresh"`딜 ģ·Øźø‰ė©ė‹ˆė‹¤. ģŗģ‹œ ģ½źø°ėŠ” ź±“ė„ˆė›°ź³ , ź°€ģ øģ˜Ø ź²°ź³¼ėŠ” ģ—¬ģ „ķžˆ ģŗģ‹œėœ ķ•­ėŖ©ģ„ ėŒ€ģ²“ķ•©ė‹ˆė‹¤. `"bypass"`와 ėŖ…ģ‹œģ  `"refresh"`ėŠ” ķ‰ģ†ŒėŒ€ė”œ ė™ģž‘ķ•©ė‹ˆė‹¤. -ģŗģ‹±ģ„ ģ™„ģ „ķžˆ 끄려멓 `Client(server, cache=None)`으딜 ģƒģ„±ķ•˜ģ„øģš”. ėŖØė“  ķ˜øģ¶œģ“ ė‹¤ģ‹œ ģ™•ė³µģ“ 되며, `cache_mode`ėŠ” ģ—¬ģ „ķžˆ ė°›ģ•„ė“¤ģ—¬ģ§€ģ§€ė§Œ 아묓 ģ¼ė„ ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. +ģŗģ‹±ģ„ ģ™„ģ „ķžˆ 끄려멓 `Client`넼 ģƒģ„±ķ•  ė•Œ `cache=None`ģ„ ģ „ė‹¬ķ•˜ģ„øģš”. ėŖØė“  ķ˜øģ¶œģ“ ė‹¤ģ‹œ ģ™•ė³µģ“ 되며, `cache_mode`ėŠ” ģ—¬ģ „ķžˆ ė°›ģ•„ė“¤ģ—¬ģ§€ģ§€ė§Œ 아묓 ģ¼ė„ ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ė²”ģœ„ė„ ģžė™ģœ¼ė”œ ģ”“ģ¤‘ė©ė‹ˆė‹¤. `"private"` ķ•­ėŖ©ģ€ ģŗģ‹œģ˜ **ķŒŒķ‹°ģ…˜**(ģ•„ėž˜ ģ°øź³ )ģ„ ķ‚¤ė”œ ķ•˜ź³ , `"public"` ķ•­ėŖ©ģ€ ė” ė„“ģ€ 공유넼 ģ„ ķƒķ•  수 ģžˆģŠµė‹ˆė‹¤. 그리고 ģ•Œė¦¼ģ“ ģ§€ėŖ©ķ•˜ėŠ” ė°”ė”œ ź·ø 항목에 ėŒ€ķ•“ģ„œėŠ” **ģ•Œė¦¼ģ“ TTL볓다 ģš°ģ„ ķ•©ė‹ˆė‹¤**. `list_changed` ģ•Œė¦¼ģ€ ģ¼ģ¹˜ķ•˜ėŠ” ģŗģ‹œėœ ėŖ©ė”ģ„ ģ¶•ģ¶œķ•˜ź³ , `resources/updated`ėŠ” ģ •ķ™•ķžˆ ź·ø URI딜 ģ €ģž„ėœ ģŗģ‹œėœ ģ½źø° 결과넼 ģ¶•ģ¶œķ•©ė‹ˆė‹¤. ģ–¼ė§ˆė‚˜ ģ‹ ģ„ ķ–ˆė“  ģƒź“€ģ—†ģŠµė‹ˆė‹¤. 2026-07-28 ģ—°ź²°ģ—ģ„œ ģ“ ģ•Œė¦¼ģ€ `client.listen(...)`으딜 ģ—¬ėŠ” `subscriptions/listen` 스트림으딜 ė„ģ°©ķ•˜ė©°, ģ¶•ģ¶œģ€ ź°ģ‹œģžź°€ ģ“ė²¤ķŠøė„¼ 볓기 전에 ģ™„ė£Œė©ė‹ˆė‹¤. ģžģ„øķ•œ ė‚“ģš©ģ€ **[źµ¬ė…](subscriptions.md)**ģ—ģ„œ ķ™•ģøķ•˜ģ„øģš”. diff --git a/i18n/ko/pages/client/callbacks.md b/i18n/ko/pages/client/callbacks.md index 49bbd347..f9b023a7 100644 --- a/i18n/ko/pages/client/callbacks.md +++ b/i18n/ko/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # ķ“ė¼ģ“ģ–øķŠø 콜백 {#client-callbacks} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `Client(...)` ķ˜øģ¶œģ˜ `mode="legacy"`ėŠ” ģ‹¤ģ œė”œ ģ¤‘ģš”ķ•œ ģ—­ķ• ģ„ ķ•©ė‹ˆė‹¤. 기본적으딜 `Client(...)`ėŠ” ģµœģ‹  ķ”„ė”œķ† ģ½œ 경딜넼 ķ˜‘ģƒķ•˜ėŠ”ė°, ź·ø ź²½ė”œģ—ėŠ” ģ„œė²„ģ—ģ„œ ķ“ė¼ģ“ģ–øķŠøė”œ ź°€ėŠ” ģš”ģ²­ģ„ ģœ„ķ•œ ģ—­ė°©ķ–„ ģ±„ė„ģ“ ģ—†ģ–“ģ„œ ģ½œė°±ģ“ ģ‹¤ķ–‰ė˜źø°ė„ 전에 `ctx.elicit` ķ˜øģ¶œģ“ ģ‹¤ķŒØķ•©ė‹ˆė‹¤. ģ“ė„¼ ź²°ģ •ķ•˜ėŠ” ź²ƒģ€ ķŠøėžœģŠ¤ķ¬ķŠøź°€ ģ•„ė‹ˆė¼ ķ˜‘ģƒėœ - ķ”„ė”œķ† ģ½œģ“ė©°, ģøė©”ėŖØė¦¬ė“  URLģ„ ķ†µķ•˜ė“  ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ ģ“ėŸ° ģš”ģ²­ģ— ģ‘ė‹µķ•“ģ•¼ ķ•  ė•Œė§ˆė‹¤ + ķ”„ė”œķ† ģ½œģž…ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ ģ“ėŸ° ģš”ģ²­ģ— ģ‘ė‹µķ•“ģ•¼ ķ•  ė•Œė§ˆė‹¤ `mode="legacy"`딜 ź³ ģ •ķ•˜ģ„øģš”. ģ“ ķŽ˜ģ“ģ§€ė„¼ ė’·ė°›ģ¹Øķ•˜ėŠ” ėŖØė“  ķ…ŒģŠ¤ķŠøź°€ ź·øė ‡ź²Œ ķ•©ė‹ˆė‹¤. ģžģ„øķ•œ ė‚“ģš©ģ€ **[ķ”„ė”œķ† ģ½œ 버전](../protocol-versions.md)**ģ—ģ„œ ķ™•ģøķ•˜ģ„øģš”. 2026-07-28 ģ„øģ…˜ģ—ģ„œė„ ģ½œė°±ģ“ ģ“øėŖØģ—†ģ–“ģ§€ėŠ” ź²ƒģ€ ģ•„ė‹ˆė©°, ģž…ė „ģ„ ė°›ėŠ” ė°©ģ‹ģ“ 다넼 ėæģž…ė‹ˆė‹¤. ė„źµ¬ź°€ @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback`ź³¼ `list_roots_callback`ė„ ź°™ģ€ ė°©ģ‹ģœ¼ė”œ ė™ģž‘ķ•˜ģ§€ė§Œ 지원 중단 ģ˜ˆģ • źø°ėŠ„ģ„ ģ²˜ė¦¬ķ•©ė‹ˆė‹¤. ģµœģ‹  ģ„œė²„ėŠ” ėŒ€ģ‹  다중 왕복 ģš”ģ²­ģ„ ģ‚¬ģš©ķ•©ė‹ˆė‹¤. * `logging_callback`ź³¼ `message_handler`ėŠ” ģ•Œė¦¼ģ„ ė°›ģŠµė‹ˆė‹¤. ģ•„ė¬“ź²ƒė„ ģ„ ģ–øķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. -`Client(...)`ģ˜ 첫 번째 ģøģžėŠ” ķŠøėžœģŠ¤ķ¬ķŠø ź°ģ²“ģž…ė‹ˆė‹¤. ėŖØė“  ģ¢…ė„˜ėŠ” **[ķ“ė¼ģ“ģ–øķŠø ķŠøėžœģŠ¤ķ¬ķŠø](transports.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. +`Client(...)`ģ˜ 첫 번째 ģøģžź°€ ķŠøėžœģŠ¤ķ¬ķŠøė„¼ ź²°ģ •ķ•©ė‹ˆė‹¤. ėŖØė“  ģ¢…ė„˜ėŠ” **[ķ“ė¼ģ“ģ–øķŠø ķŠøėžœģŠ¤ķ¬ķŠø](transports.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. diff --git a/i18n/ko/pages/client/index.md b/i18n/ko/pages/client/index.md index 636b1609..51c1e151 100644 --- a/i18n/ko/pages/client/index.md +++ b/i18n/ko/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # ķ“ė¼ģ“ģ–øķŠø {#the-client} @@ -11,13 +11,23 @@ translation: ## 첫 번째 ķ“ė¼ģ“ģ–øķŠø {#your-first-client} -```python title="client.py" hl_lines="14-18" +ķ“ė¼ģ“ģ–øķŠøģ—ź²ŒėŠ” 통신할 ģ„œė²„ź°€ ķ•„ģš”ķ•©ė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ģ˜ ėŖØė“  ģ˜ˆģ œź°€ ģ—°ź²°ķ•˜ėŠ” ėŒ€ģƒģ“ ė°”ė”œ ģ“ Bookshop ģ„œė²„ģž…ė‹ˆė‹¤. `server.py`딜 ģ €ģž„ķ•˜ź³  HTTP딜 실행핓 ė‘ģ„øģš”. + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -ė§Ø ģœ„ģ˜ ģ„œė²„ėŠ” ģ—°ź²°ķ•  ėŒ€ģƒģ„ ė§ˆė Øķ•˜źø° ģœ„ķ•“ ģžˆģ„ ėæģž…ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøėŠ” ź°•ģ”° ķ‘œģ‹œėœ 다섯 ģ¤„ģž…ė‹ˆė‹¤. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)`ģ—ėŠ” **ģ„œė²„ ź°ģ²“ ģžģ²“**넼 ė„˜ź¹ė‹ˆė‹¤. ģ“ź²ƒģ“ ģøė©”ėŖØė¦¬ ķŠøėžœģŠ¤ķ¬ķŠøģž…ė‹ˆė‹¤. ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„, ķ¬ķŠøė„, HTTPė„ ģ—†ģŠµė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ģ˜ ėŖØė“  ģ˜ˆģ œģ™€ ģ•žģœ¼ė”œ ģž‘ģ„±ķ•  ėŖØė“  ķ…ŒģŠ¤ķŠøź°€ ģ“ ė°©ģ‹ģœ¼ė”œ ģ—°ź²°ķ•©ė‹ˆė‹¤. +ģ“ė ‡ź²Œ ķ•˜ė©“ `http://localhost:8000/mcp`ģ—ģ„œ ģ„œė²„ź°€ ģ œź³µė©ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøėŠ” ė³„ė„ģ˜ ķ”„ė”œź·øėžØģž…ė‹ˆė‹¤. `client.py`딜 ģ €ģž„ķ•˜ź³  두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`넼 ģ‹¤ķ–‰ķ•˜ģ„øģš”. + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")`ģ—ėŠ” **URL**ģ„ ė„˜źø°ėÆ€ė”œ, 방금 ģ‹œģž‘ķ•œ ģ„œė²„ģ— Streamable HTTP딜 ģ—°ź²°ķ•©ė‹ˆė‹¤. * `async with`ź°€ **ģƒėŖ… 주기**ģž…ė‹ˆė‹¤. 들얓가멓 ģ—°ź²°ķ•˜ź³  ķ˜‘ģƒķ•˜ė©°, ė‚˜ģ˜¤ė©“ ģ—°ź²°ģ„ ėŠģŠµė‹ˆė‹¤. `connect()` / `close()` ģŒģ€ ģ—†ģœ¼ė©°, ėø”ė”ģ“ ėė‚œ ė’¤ģ—ėŠ” `Client`넼 ģž¬ģ‚¬ģš©ķ•  수 ģ—†ģŠµė‹ˆė‹¤. * ėø”ė” ģ•ˆģ—ģ„œėŠ” ģ—°ź²° 정볓가 ģ“ėÆø ķ‰ė²”ķ•œ ķ”„ė”œķ¼ķ‹°ė”œ ģ¤€ė¹„ė˜ģ–“ ģžˆģŠµė‹ˆė‹¤. @@ -25,10 +35,10 @@ translation: `Client`ėŠ” ģœ„ģ¹˜ ģøģž ķ•˜ė‚˜ė„¼ 받고, ź·ø ķƒ€ģž…ģœ¼ė”œ ķŠøėžœģŠ¤ķ¬ķŠøė„¼ ź²°ģ •ķ•©ė‹ˆė‹¤. -* `MCPServer`(ė˜ėŠ” ģ €ģˆ˜ģ¤€ `Server`) ģøģŠ¤ķ„“ģŠ¤: **ķ”„ė”œģ„øģŠ¤ ė‚“ė¶€**ģ—ģ„œ ģ—°ź²°ķ•©ė‹ˆė‹¤. -* URL ė¬øģžģ—“(`Client("http://localhost:8000/mcp")`): ķ”„ė”œė•ģ…˜ ź²½ė”œģø Streamable HTTPģž…ė‹ˆė‹¤. -* `StdioServerParameters`: **ģ„œėøŒķ”„ė”œģ„øģŠ¤**딜 실행할 ėŖ…ė ¹ģ“ė©°, ź·ø stdinź³¼ stdoutģ„ 통핓 ķ†µģ‹ ķ•©ė‹ˆė‹¤. +* URL ė¬øģžģ—“(`Client("http://localhost:8000/mcp")`): ģ‹¤ģ œ ė°°ķ¬ģ— ģ“°ėŠ” ķŠøėžœģŠ¤ķ¬ķŠøģø Streamable HTTPģž…ė‹ˆė‹¤. +* `StdioServerParameters`: 딜컬 **ģ„œėøŒķ”„ė”œģ„øģŠ¤**딜 실행할 ėŖ…ė ¹ģ“ė©°, ź·ø stdinź³¼ stdoutģ„ 통핓 ķ†µģ‹ ķ•©ė‹ˆė‹¤. * **ķŠøėžœģŠ¤ķ¬ķŠø**: `async with ... as (read, write)`딜 ģ‚¬ģš©ķ•  수 ģžˆėŠ” ėŖØė“  것, 예넼 들얓 직접 ė§Œė“  HTTP ķ“ė¼ģ“ģ–øķŠøė„¼ ź°ģ‹øėŠ” `streamable_http_client(url, http_client=...)`ģž…ė‹ˆė‹¤. +* `MCPServer`(ė˜ėŠ” ģ €ģˆ˜ģ¤€ `Server`) ģøģŠ¤ķ„“ģŠ¤: ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„ ķ¬ķŠøė„ ģ—†ģ“ **ķ”„ė”œģ„øģŠ¤ ė‚“ė¶€**ģ—ģ„œ ģ—°ź²°ķ•©ė‹ˆė‹¤. ģ“ ė°©ģ‹ģ€ ķ…ŒģŠ¤ķŠøė„¼ ģœ„ķ•œ ź²ƒģ“ė©°, **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)**ź°€ ģ“ė„¼ 기반으딜 ķ•©ė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ģ˜ ė‚˜ėØøģ§€ ė‚“ģš©ģ€ 네 가지 ėŖØė‘ģ—ģ„œ ė™ģ¼ķ•©ė‹ˆė‹¤. ķ—¤ė”, ģ„œėøŒķ”„ė”œģ„øģŠ¤, ķƒ€ģž„ģ•„ģ›ƒ, `Transport` ķ”„ė”œķ† ģ½œģ€ ė³„ė„ģ˜ ķŽ˜ģ“ģ§€ģø **[ķ“ė¼ģ“ģ–øķŠø ķŠøėžœģŠ¤ķ¬ķŠø](transports.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. @@ -49,11 +59,11 @@ translation: ## ė„źµ¬ ėŖ©ė” 씰회 {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()`ėŠ” `ListToolsResult`넼 ė°˜ķ™˜ķ•˜ė©°, ė„źµ¬ėŠ” `.tools`에 들얓 ģžˆģŠµė‹ˆė‹¤. 각 ė„źµ¬ėŠ” ķ˜øģŠ¤ķŠøź°€ ėŖØėøģ— ź±“ė„¤ėŠ” ģ™„ģ „ķ•œ ģ •ģ˜ģž…ė‹ˆė‹¤. +`list_tools()`ėŠ” `ListToolsResult`넼 ė°˜ķ™˜ķ•˜ė©°, ė„źµ¬ėŠ” `.tools`에 들얓 ģžˆģŠµė‹ˆė‹¤. 각 ė„źµ¬ėŠ” ķ˜øģŠ¤ķŠøź°€ ėŖØėøģ— ź±“ė„¤ėŠ” ģ™„ģ „ķ•œ ģ •ģ˜ģž…ė‹ˆė‹¤. 첫 번째 ė„źµ¬ėŠ” ė‹¤ģŒź³¼ ź°™ģŠµė‹ˆė‹¤. ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' ģ“ ģŠ¤ķ‚¤ė§ˆėŠ” UIź°€ ģøģž ģž…ė „ ķ¼ģ„ ė Œė”ė§ķ•˜ėŠ” ė° ķ•„ģš”ķ•œ ģ „ė¶€ģ“ģž, ėŖØėøģ“ ģœ ķšØķ•œ ģøģžė„¼ ė§Œė“¤ģ–“ ė‚“ėŠ” ė° ķ•„ģš”ķ•œ ģ „ė¶€ģž…ė‹ˆė‹¤. +두 번째 ė„źµ¬ģø `lookup_book`ģ€ `title=` ģ—†ģ“ ė“±ė”ė˜ģ—ˆģœ¼ėÆ€ė”œ `tool.title`ģ“ `None`ģž…ė‹ˆė‹¤. + !!! tip `title`ģ€ ģ„ ķƒ ģ‚¬ķ•­ģ“ėÆ€ė”œ, ģ‚¬ėžŒģ—ź²Œ ė„źµ¬ė„¼ 볓여 ģ£¼ėŠ” UIėŠ” ė¬“ģ—‡ģ„ ķ‘œģ‹œķ• ģ§€ ź³Øė¼ģ•¼ ķ•©ė‹ˆė‹¤. `title`ģ“ ģžˆģœ¼ė©“ ģ“°ź³ , ģ—†ģœ¼ė©“ `name`ģ„ ģ”ė‹ˆė‹¤. `from mcp.shared.metadata_utils import get_display_name`ģ“ ģ •ķ™•ķžˆ ź·ø ģ¼ģ„ ķ•˜ė©°, @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)`ėŠ” ė„źµ¬ė„¼ ģ‹¤ķ–‰ķ•˜ź³  `CallToolResult`넼 ėŒė ¤ģ¤ė‹ˆė‹¤. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ result.is_error # False ė¦¬ģ†ŒģŠ¤ ė™ģž‘ģ€ ģ§ģ„ ģ“ė£¹ė‹ˆė‹¤. ėŖ©ė”ģ„ ģ”°ķšŒķ•˜ėŠ” ė°©ė²•ģ“ ė‘˜, ģ½ėŠ” ė°©ė²•ģ“ ķ•˜ė‚˜ģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ result.is_error # False ## ķ”„ė”¬ķ”„ķŠø {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ģžė™ 완성 ķ•øė“¤ėŸ¬ź°€ ģžˆėŠ” ģ„œė²„ėŠ” ģ‚¬ģš©ģžź°€ ģž…ė „ķ•˜ėŠ” ė™ģ•ˆ ķ”„ė”¬ķ”„ķŠøģ™€ ė¦¬ģ†ŒģŠ¤ ķ…œķ”Œė¦æ ģøģžė„¼ ģžė™ 완성할 수 ģžˆģŠµė‹ˆė‹¤. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ėŖØė“  `list_*` ė©”ģ„œė“œėŠ” `cursor=` ķ‚¤ģ›Œė“œė„¼ 받고, ėŖØė“  ź²°ź³¼ģ—ėŠ” `next_cursor`ź°€ ģžˆģŠµė‹ˆė‹¤. `next_cursor`ź°€ `None`ģ“ė©“ ģ „ė¶€ ė°›ģ€ ź²ƒģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -ģ“ ė£Øķ”„ėŠ” ģ–“ė–¤ ģ„œė²„ģ— ėŒ€ķ•“ģ„œė„ ģ˜¬ė°”ė¦…ė‹ˆė‹¤. `MCPServer`ėŠ” ėŖØė“  ź²ƒģ„ ķ•œ ķŽ˜ģ“ģ§€ģ— ė°˜ķ™˜ķ•˜ėÆ€ė”œ `next_cursor`ėŠ” `None`ģ“ź³  ė£Øķ”„ėŠ” ķ•œ 번만 ėŒė©°, ź·øėž˜ģ„œ ėŒ€ė¶€ė¶„ģ˜ ģ½”ė“œėŠ” ģ“ 루프넼 ģž‘ģ„±ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ‹¤ģ œė”œ ķŽ˜ģ“ģ§€ė„¼ ė‚˜ėˆ„ėŠ” ģ„œė²„ģ™€ ģ»¤ģ„œź°€ ė”°ė„“ėŠ” ź·œģ¹™ģ€ **[ķŽ˜ģ“ģ§€ė„¤ģ“ģ…˜](../advanced/pagination.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. +`list_all_tools`ėŠ” ģ–“ė–¤ ģ„œė²„ģ— ėŒ€ķ•“ģ„œė„ ģ˜¬ė°”ė„“ź²Œ ė™ģž‘ķ•©ė‹ˆė‹¤. `MCPServer`ėŠ” ėŖØė“  ź²ƒģ„ ķ•œ ķŽ˜ģ“ģ§€ģ— ė°˜ķ™˜ķ•˜ėÆ€ė”œ `next_cursor`ėŠ” `None`ģ“ź³  ė£Øķ”„ėŠ” ķ•œ 번만 ėŒė©°, ź·øėž˜ģ„œ ėŒ€ė¶€ė¶„ģ˜ ģ½”ė“œėŠ” ģ“ 루프넼 ģž‘ģ„±ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ‹¤ģ œė”œ ķŽ˜ģ“ģ§€ė„¼ ė‚˜ėˆ„ėŠ” ģ„œė²„ģ™€ ģ»¤ģ„œź°€ ė”°ė„“ėŠ” ź·œģ¹™ģ€ **[ķŽ˜ģ“ģ§€ė„¤ģ“ģ…˜](../advanced/pagination.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. ## ķ…ŒģŠ¤ķŠøģ—ģ„œ {#in-tests} -ķ”„ė”œģ„øģŠ¤ė„ ķ¬ķŠøė„ ģ—†ėŠ” `Client(mcp)`ėŠ” ź·ø ģžģ²“ė”œ ģ“ėÆø ģ„œė²„ģ˜ ķ…ŒģŠ¤ķŠø ķ•˜ė„¤ģŠ¤ģž…ė‹ˆė‹¤. +ģ“ ķŽ˜ģ“ģ§€ģ˜ ėŖØė“  `client.py`ėŠ” HTTP딜 `server.py`에 ģ—°ź²°ķ–ˆģŠµė‹ˆė‹¤. ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” ė„¤ķŠøģ›Œķ¬ė„¼ ź±“ė„ˆė›°ź³  `Client`에 ģ„œė²„ ź°ģ²“ ģžģ²“ė„¼ ė„˜ź¹ė‹ˆė‹¤. `from server import mcp`넼 ķ•œ ė‹¤ģŒ `Client(mcp)`넼 ė§Œė“¤ė©“ ė©ė‹ˆė‹¤. ķ”„ė”œģ„øģŠ¤ė„ ķ¬ķŠøė„ ģ—†ģœ¼ė©°, ģœ„ģ˜ ėŖØė“  ė©”ģ„œė“œź°€ ė˜‘ź°™ģ“ ė™ģž‘ķ•©ė‹ˆė‹¤. -ģ“ė„¼ ģœ„ķ•“ ė§Œė“¤ģ–“ģ§„ ģƒģ„±ģž ķ”Œėž˜ź·øź°€ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. `Client(mcp, raise_exceptions=True)`ģž…ė‹ˆė‹¤. ģøė©”ėŖØė¦¬ ģ—°ź²°ģ—ģ„œė§Œ ķšØź³¼ź°€ ģžˆģœ¼ė©°, ģ“ė„¼ ģ„¤ėŖ…ķ•˜ź³  전첓 ķŒØķ„“ģ„ źµ¬ģ¶•ķ•˜ėŠ” ķŽ˜ģ“ģ§€ėŠ” **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)**ģž…ė‹ˆė‹¤. +ģ“ė„¼ ģœ„ķ•“ ė§Œė“¤ģ–“ģ§„ ģƒģ„±ģž ķ”Œėž˜ź·øź°€ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. `Client(mcp, raise_exceptions=True)`ģž…ė‹ˆė‹¤. ķ”„ė”œģ„øģŠ¤ ė‚“ė¶€ ģ—°ź²°ģ—ģ„œė§Œ ķšØź³¼ź°€ ģžˆģœ¼ė©°, ģ“ė„¼ ģ„¤ėŖ…ķ•˜ź³  전첓 ķŒØķ„“ģ„ źµ¬ģ¶•ķ•˜ėŠ” ķŽ˜ģ“ģ§€ėŠ” **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)**ģž…ė‹ˆė‹¤. ## ģš”ģ•½ {#recap} -* `Client(x)`ėŠ” ģ„œė²„ ź°ģ²“ģ—ėŠ” ģøė©”ėŖØė¦¬ė”œ, URL ė¬øģžģ—“ģ—ėŠ” Streamable HTTP딜, ź·ø ė°–ģ˜ ź²ƒģ—ėŠ” ķŠøėžœģŠ¤ķ¬ķŠøė„¼ 통핓 ģ—°ź²°ķ•©ė‹ˆė‹¤. +* `Client(x)`ėŠ” URL ė¬øģžģ—“ģ—ėŠ” Streamable HTTP딜 ģ—°ź²°ķ•˜ź³ , `StdioServerParameters`ģ—ėŠ” ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„¼ ģ‹¤ķ–‰ķ•˜ė©°, ķŠøėžœģŠ¤ķ¬ķŠøģ—ėŠ” 직접 들얓가고, ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” ģ„œė²„ ź°ģ²“ ģžģ²“ė„¼ ė°›ģŠµė‹ˆė‹¤. * `async with`ź°€ ģƒėŖ… ģ£¼źø°ģ˜ ģ „ė¶€ģž…ė‹ˆė‹¤. ź·ø ģ•ˆģ—ģ„œėŠ” `server_capabilities`와 `protocol_version`ģ“ ģ“ėÆø ģ±„ģ›Œģ ø ģžˆģœ¼ė©°, ģ„œė²„ź°€ ģ œź³µķ•˜ėŠ” 경우 `server_info`와 `instructions`ė„ ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤. * `list_tools()`ėŠ” 각 ė„źµ¬ģ˜ `name`, `title`, `description`, `input_schema`넼 ģ œź³µķ•©ė‹ˆė‹¤. * `call_tool()`ģ€ ėŖØėøģ„ ģœ„ķ•œ `content`, ģ½”ė“œė„¼ ģœ„ķ•œ `structured_content`, 그리고 `is_error`넼 ė°˜ķ™˜ķ•©ė‹ˆė‹¤. ģ˜ˆģ™øė„¼ ė°œģƒģ‹œķ‚¤ėŠ” ė„źµ¬ėŠ” ģ˜ˆģ™øź°€ ģ•„ė‹ˆė¼ ź²°ź³¼ģž…ė‹ˆė‹¤. diff --git a/i18n/ko/pages/client/oauth-clients.md b/i18n/ko/pages/client/oauth-clients.md index 534311a9..91d99477 100644 --- a/i18n/ko/pages/client/oauth-clients.md +++ b/i18n/ko/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth ķ“ė¼ģ“ģ–øķŠø {#oauth-clients} @@ -81,18 +81,20 @@ translation: `Client`ź°€ ģ²˜ģŒ ģš”ģ²­ģ„ 볓낓멓 ģ„œė²„ėŠ” `401`딜 ģ‘ė‹µķ•©ė‹ˆė‹¤. 그러멓 ķ”„ė”œė°”ģ“ė”ź°€ ģ“ģ–“ė°›ģŠµė‹ˆė‹¤. -1. **ė””ģŠ¤ģ»¤ė²„ė¦¬.** `WWW-Authenticate` ķ—¤ė”ė„¼ ģ½ź³ , `/.well-known/oauth-protected-resource`ģ—ģ„œ ģ„œė²„ģ˜ Protected Resource Metadata넼 ź°€ģ øģ˜¤ź³ , ģ–“ėŠ ģøź°€ ģ„œė²„ź°€ ģ“ ė¦¬ģ†ŒģŠ¤ė„¼ ė³“ķ˜øķ•˜ėŠ”ģ§€ ģ•Œģ•„ė‚ø ė’¤, **ź·ø** ģ„œė²„ģ˜ ė©”ķƒ€ė°ģ“ķ„°ė„¼ ź°€ģ øģ˜µė‹ˆė‹¤. +1. **ė””ģŠ¤ģ»¤ė²„ė¦¬.** `WWW-Authenticate` ķ—¤ė”ė„¼ ģ½ź³ , `/.well-known/oauth-protected-resource`ģ—ģ„œ ģ„œė²„ģ˜ Protected Resource Metadata넼 ź°€ģ øģ˜¤ź³ , ģ–“ėŠ ģøź°€ ģ„œė²„ź°€ ģ“ ė¦¬ģ†ŒģŠ¤ė„¼ ė³“ķ˜øķ•˜ėŠ”ģ§€ ģ•Œģ•„ė‚ø ė’¤, **ź·ø** ģ„œė²„ģ˜ ė©”ķƒ€ė°ģ“ķ„°ė„¼ ź°€ģ øģ˜µė‹ˆė‹¤. (ė¦¬ģ†ŒģŠ¤ ė©”ķƒ€ė°ģ“ķ„°ė„¼ ź²Œģ‹œķ•˜ģ§€ ģ•ŠėŠ” ģ˜¤ėž˜ėœ ģ„œė²„ģ—ėŠ” ėŒ€ģ‹  ź·ø ģ„œė²„ ģžģ‹ ģ˜ ģ˜¤ė¦¬ģ§„ģ—ģ„œ ģøź°€ ģ„œė²„ ė©”ķƒ€ė°ģ“ķ„°ė„¼ ģš”ģ²­ķ•©ė‹ˆė‹¤.) ģ–“ėŠ ź²½ģš°ė“  ė©”ķƒ€ė°ģ“ķ„°ģ˜ `issuer` ź°’ģ€ ź·ø ė©”ķƒ€ė°ģ“ķ„°ė„¼ ź°€ģ øģ˜Ø ėŒ€ģƒ ģ„œė²„ė„¼ ź°€ė¦¬ģ¼œģ•¼ ķ•˜ė©°, 다넸 ź°’ģ“ė©“ ź±°ė¶€ķ•©ė‹ˆė‹¤. 2. **ė“±ė”.** ģ €ģž„ģ†Œģ— ģ•„ė¬“ź²ƒė„ ģ—†ģœ¼ė©“ `OAuthClientMetadata`딜 ė™ģ ģœ¼ė”œ ė“±ė”ķ•˜ź³  결과넼 ģ €ģž„ķ•©ė‹ˆė‹¤. 3. **ģøź°€.** PKCE ģŒź³¼ `state`넼 ģƒģ„±ķ•˜ź³ , ģøź°€ URLģ„ ģ”°ė¦½ķ•˜ź³ , `redirect_handler`넼 awaitķ•œ ė‹¤ģŒ, ģ½”ė“œė„¼ 받기 ģœ„ķ•“ `callback_handler`넼 awaitķ•©ė‹ˆė‹¤. 4. **źµķ™˜.** ģ½”ė“œė„¼ `OAuthToken`으딜 źµķ™˜ķ•“ ģ €ģž„ķ•˜ź³ , ģ›ėž˜ ģš”ģ²­ģ— `Authorization: Bearer ...`넼 붙여 ė‹¤ģ‹œ ė³“ėƒ…ė‹ˆė‹¤. ź·øė‹¤ģŒė¶€ķ„°ėŠ” ģ”°ģš©ķ•©ė‹ˆė‹¤. ķ† ķ°ģ€ ģ €ģž„ģ†Œģ—ģ„œ 꺼낓 ģ“°ź³ , 만료된 ģ•”ģ„øģŠ¤ ķ† ķ°ģ€ ė¦¬ķ”„ė ˆģ‹œ ķ† ķ°ģœ¼ė”œ ź°±ģ‹ ķ•˜ė©°, ź·ø ģ–“ėŠ ź²ƒė„ ķ†µķ•˜ģ§€ ģ•Šģ„ ė•Œģ—ė§Œ ķė¦„ģ„ ė‹¤ģ‹œ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. +ģ“ ėŖØė“  ģš”ģ²­ģ— ģ ģš©ė˜ėŠ” ķŠøėžœģŠ¤ķ¬ķŠø ź·œģ¹™ģ“ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. ė°”ź¹„ģ˜ MCP ģš”ģ²­ź³¼ ė§ˆģ°¬ź°€ģ§€ė”œ, ź°™ģ€ ģ˜¤ė¦¬ģ§„ģ— ėØøė¬¼ė©“ģ„œ ė©”ģ„œė“œė„¼ ģœ ģ§€ķ•˜ėŠ” ė¦¬ė‹¤ģ“ė ‰ķŠø(ģ“ė„¼ķ…Œė©“ ė ģŠ¬ėž˜ģ‹œė„¼ ė¶™ģ“ėŠ” 307/308)만 ė”°ė„“ź³ , ź·ø ė°–ģ˜ ė¦¬ė‹¤ģ“ė ‰ķŠøėŠ” 핓당 URLģ“ ģ‘ė‹µķ•˜ģ§€ ģ•ŠėŠ” 것으딜 ģ·Øźø‰ķ•©ė‹ˆė‹¤. + ģ“ ź°€ģš“ė° 직접 ģž‘ģ„±ķ•œ ģ½”ė“œėŠ” ķ•˜ė‚˜ė„ ģ—†ģŠµė‹ˆė‹¤. ķ‚¤ģ›Œė“œ ģøģžź°€ 두 개 ė” 남아 ģžˆėŠ”ė°(`client_metadata_url`ź³¼ `validate_resource_url`), ģ“ ķŒŒģ¼ģ—ėŠ” ė‘˜ 다 ķ•„ģš” ģ—†ģŠµė‹ˆė‹¤. ģ•Œģ•„ ė‘˜ ė§Œķ•œ ź²ƒģ€ `client_metadata_url`ģ“ė©°, ģ•„ėž˜ģ— ė³„ė„ ģ„¹ģ…˜ģ“ ģžˆģŠµė‹ˆė‹¤. ### 직접 ķ•“ 볓기 {#try-it} -ģ“ ė¬øģ„œģ˜ 예제 ėŒ€ė¶€ė¶„ģ€ ģøė©”ėŖØė¦¬ `Client(server)`딜 ķ™•ģøķ•  수 ģžˆģŠµė‹ˆė‹¤. ģ“ ģ˜ˆģ œėŠ” ģ•„ė‹™ė‹ˆė‹¤. ģ“ ķė¦„ģ˜ ķ•µģ‹¬ģ“ HTTP `401`ģøė°, ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠøģ™€ ģ„œė²„ ģ‚¬ģ“ģ—ėŠ” HTTPź°€ 없기 ė•Œė¬øģž…ė‹ˆė‹¤. +ķ…ŒģŠ¤ķŠøģ—ģ„œ ģ“°ėŠ” ģøė©”ėŖØė¦¬ `Client(server)`ėŠ” ģ—¬źø°ģ„œėŠ” ė„ģ›€ģ“ ė˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ“ ķė¦„ģ˜ ķ•µģ‹¬ģ“ HTTP `401`ģøė°, ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠøģ™€ ģ„œė²„ ģ‚¬ģ“ģ—ėŠ” HTTPź°€ 없기 ė•Œė¬øģž…ė‹ˆė‹¤. ė¦¬ķ¬ģ§€ķ† ė¦¬ģ—ėŠ” ģ‹¤ģ œė”œ ė™ģž‘ķ•˜ėŠ” ė²„ģ „ģ“ 들얓 ģžˆģŠµė‹ˆė‹¤. `examples/servers/simple-auth/`ėŠ” ė…ė¦½ ģ‹¤ķ–‰ķ˜• ģøź°€ ģ„œė²„ģ™€ 볓호된 MCP ģ„œė²„ė„¼ ģ‹¤ķ–‰ķ•˜ź³ , `examples/clients/simple-auth-client/`ėŠ” ģ“ ķŽ˜ģ“ģ§€ģ˜ ķ“ė¼ģ“ģ–øķŠøė„¼ ģž‘ģ€ CLI딜 ķ‚¤ģš“ ź²ƒģž…ė‹ˆė‹¤. ź·ø README에 두 ėŖ…ė ¹ģ“ ģžˆģŠµė‹ˆė‹¤. ģ„œė²„ė„¼ ģ‹œģž‘ķ•˜ź³ , ź·ø ģ„œė²„ė„¼ ėŒ€ģƒģœ¼ė”œ ķ“ė¼ģ“ģ–øķŠøė„¼ ģ‹¤ķ–‰ķ•˜ė©“ 네 단계가 ģ§€ė‚˜ź°€ėŠ” ėŖØģŠµģ„ ė³¼ 수 ģžˆģŠµė‹ˆė‹¤. @@ -110,13 +112,14 @@ URLģ€ ė£ØķŠøź°€ ģ•„ė‹Œ 경딜넼 가진 HTTPS여야 ķ•©ė‹ˆė‹¤. ź·ø ģ™øģ—ėŠ” `ClientCredentialsOAuthProvider`ėŠ” ģ‚¬ėžŒė§Œ ė¹ ģ§„ ė˜‘ź°™ģ€ `httpx2.Auth`ģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` ė‹¬ė¼ģ§„ ģ ģ€ ė‹¤ģŒź³¼ ź°™ģŠµė‹ˆė‹¤. * `OAuthClientMetadata`ė„ ķ•øė“¤ėŸ¬ė„ ģ—†ģŠµė‹ˆė‹¤. `client_id`와 `client_secret`ģ„ ģ „ė‹¬ķ•˜ė©“ ķ”„ė”œė°”ģ“ė”ź°€ ģ“ė„¼ ź°ģ‹øėŠ” ģµœģ†Œķ•œģ˜ `client_credentials` ė“±ė”ģ„ ė§Œė“¤ź³  ė™ģ  ė“±ė”ģ€ ģ™„ģ „ķžˆ ź±“ė„ˆėœė‹ˆė‹¤. +* `issuer`ėŠ” ź·ø ģžź²© ģ¦ėŖ…ģ„ ė°œźø‰ķ•œ ģøź°€ ģ„œė²„ė„¼ ģ§€ėŖ©ķ•©ė‹ˆė‹¤. ź·ø ģ„œė²„ģ˜ `/.well-known/oauth-authorization-server` ė¬øģ„œź°€ ė°˜ķ™˜ķ•˜ėŠ” `issuer` ź°’ģ„ ģ“°ģ„øģš”. ė””ģŠ¤ģ»¤ė²„ė¦¬ėŠ” ģœ„ģ™€ ė˜‘ź°™ģ“ ģ‹¤ķ–‰ė˜ģ§€ė§Œ, 토큰 ģš”ģ²­ģ€ 오직 **ź·ø** ė°œźø‰ģžģ˜ ė©”ķƒ€ė°ģ“ķ„°ė”œė§Œ ģ”°ė¦½ė©ė‹ˆė‹¤. MCP ģ„œė²„ź°€ 다넸 ź³³ģ„ 가리키멓 ķė¦„ģ€ ėŒ€ģ‹  `OAuthFlowError`와 ķ•Øź»˜ ė©ˆģ¶„ė‹ˆė‹¤. ģ“ ź°’ģ„ ģƒėžµķ•˜ėŠ” ź²ƒģ€ 지원 중단 ģ˜ˆģ •ģ“ė©° 3.0ģ—ģ„œėŠ” ķ•„ģˆ˜ź°€ ė©ė‹ˆė‹¤(**[지원 중단 ģ˜ˆģ • 기늄](../deprecated.md#deprecated-sdk-helpers)** ģ°øź³ ). ź·øė•Œź¹Œģ§€ėŠ” ķ”„ė”œė°”ģ“ė”ź°€ 경고넼 ė‚“ź³  ė””ģŠ¤ģ»¤ė²„ė¦¬ź°€ 찾아낸 ģøź°€ ģ„œė²„ė„¼ ź·øėŒ€ė”œ ģ”ė‹ˆė‹¤. * `scope`ėŠ” 공백으딜 źµ¬ė¶„ķ•œ ė¬øģžģ—“ė”œ, OAuthģ˜ 전솔 ķ˜•ģ‹ģž…ė‹ˆė‹¤. * ź·ø ģ•„ėž˜ėŠ” 모두 ė™ģ¼ķ•©ė‹ˆė‹¤. ź°™ģ€ `TokenStorage`, ź°™ģ€ `httpx2.AsyncClient(auth=...)`, ź°™ģ€ `streamable_http_client`넼 ģ”ė‹ˆė‹¤. @@ -129,8 +132,8 @@ URLģ€ ė£ØķŠøź°€ ģ•„ė‹Œ 경딜넼 가진 HTTPS여야 ķ•©ė‹ˆė‹¤. ź·ø ģ™øģ—ėŠ” `mcp.client.auth.extensions.client_credentials`ģ—ėŠ” ķ”„ė”œė°”ģ“ė”ź°€ ķ•˜ė‚˜ ė” ģžˆģŠµė‹ˆė‹¤. 공유 ģ‹œķ¬ė¦æ ėŒ€ģ‹  JWT딜 ģøģ¦ķ•˜ėŠ” ķ“ė¼ģ“ģ–øķŠøė„¼ ģœ„ķ•œ **`PrivateKeyJWTOAuthProvider`**ģž…ė‹ˆė‹¤ (`private_key_jwt`, 즉 키 ģŒź³¼ ģ›Œķ¬ė”œė“œ ģ•„ģ“ė“ķ‹°ķ‹° ė°©ģ‹). ź°™ģ€ ķŒØķ„“ģ„ ė”°ė¦…ė‹ˆė‹¤. - ķ•˜ė‚˜ė„¼ ģƒģ„±ķ•“ `auth=`에 ė„£ģœ¼ė©“ ė©ė‹ˆė‹¤. ź°™ģ€ ėŖØė“ˆģ—ėŠ” ź·ø ģ–“ģ„¤ģ…˜ģ„ ė§Œė“œėŠ” 두 ķ—¬ķ¼ģø - `SignedJWTParameters`와 `static_assertion_provider`ė„ 들얓 ģžˆģŠµė‹ˆė‹¤. + ķ•˜ė‚˜ė„¼ ģƒģ„±ķ•“(ģ„ ķƒ ģøģž `issuer`넼 ė˜‘ź°™ģ“ ė°›ģŠµė‹ˆė‹¤) `auth=`에 ė„£ģœ¼ė©“ ė©ė‹ˆė‹¤. ź°™ģ€ ėŖØė“ˆģ—ėŠ” + ź·ø ģ–“ģ„¤ģ…˜ģ„ ė§Œė“œėŠ” 두 ķ—¬ķ¼ģø `SignedJWTParameters`와 `static_assertion_provider`ė„ 들얓 ģžˆģŠµė‹ˆė‹¤. ģ‚¬ėžŒģ“ ģ—†ėŠ” ģƒķ™©ģ“ ķ•˜ė‚˜ ė” ģžˆģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ 기업에 ģ†ķ•“ ģžˆź³ , ģ–“ėŠ MCP ģ„œė²„ģ— 접근할 수 ģžˆėŠ”ģ§€ė„¼ ģ‚¬ģš©ģžź°€ ģ•„ė‹ˆė¼ ź·ø źø°ģ—…ģ˜ ģ•„ģ“ė“ķ‹°ķ‹° ź³µźø‰ģžź°€ ź²°ģ •ķ•˜ėŠ” ź²½ģš°ģž…ė‹ˆė‹¤. ģ“ėŠ” ź³ ģœ ķ•œ 신뢰 ėŖØėøģ„ 가진 다넸 ź·øėžœķŠøģ“ė©°, ė³„ė„ ķŽ˜ģ“ģ§€ģø **[ģ•„ģ“ė“ķ‹°ķ‹° ģ–“ģ„¤ģ…˜](identity-assertion.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. diff --git a/i18n/ko/pages/client/session-groups.md b/i18n/ko/pages/client/session-groups.md index be15ae20..fc78a899 100644 --- a/i18n/ko/pages/client/session-groups.md +++ b/i18n/ko/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # ģ„øģ…˜ 그룹 {#session-groups} @@ -78,7 +78,7 @@ translation: ## ģš”ģ•½ {#recap} * `ClientSessionGroup`ģ€ ģ—¬ėŸ¬ ģ„œė²„ ģ—°ź²°ģ„ ė‹“ź³  ė„źµ¬, ė¦¬ģ†ŒģŠ¤, ķ”„ė”¬ķ”„ķŠøė„¼ 각각 ķ•˜ė‚˜ģ˜ `dict`딜 ķ•©ģ¹©ė‹ˆė‹¤. -* ģ„œė²„ė§ˆė‹¤ `connect_to_server(params)`넼 ķ˜øģ¶œķ•©ė‹ˆė‹¤. `Client`ź°€ ė°›ėŠ” ģ„œė²„ ź°ģ²“ė‚˜ URLģ“ ģ•„ė‹ˆė¼ ķŠøėžœģŠ¤ķ¬ķŠø ė§¤ź°œė³€ģˆ˜ė„¼ ė°›ģŠµė‹ˆė‹¤. +* ģ„œė²„ė§ˆė‹¤ `connect_to_server(params)`넼 ķ˜øģ¶œķ•©ė‹ˆė‹¤. `Client`ź°€ ė°›ėŠ” URLģ“ė‚˜ `Transport`ź°€ ģ•„ė‹ˆė¼ ķŠøėžœģŠ¤ķ¬ķŠø ė§¤ź°œė³€ģˆ˜ė„¼ ė°›ģŠµė‹ˆė‹¤. * `group.call_tool(name, arguments)`ėŠ” ģ†Œģœ ķ•œ ģ„œė²„ė”œ ģ•Œģ•„ģ„œ ė¼ģš°ķŒ…ķ•©ė‹ˆė‹¤. * ģ“ė¦„ģ€ 그룹 ģ „ģ²“ģ—ģ„œ ź³ ģœ ķ•“ģ•¼ ķ•©ė‹ˆė‹¤. `search` ė„źµ¬ė„¼ 가진 두 ģ„œė²„ėŠ” ź·øėŒ€ė”œėŠ” 공씓할 수 ģ—†ģŠµė‹ˆė‹¤. * `component_name_hook=`ģ€ ė“±ė”ė˜ėŠ” ėŖØė“  ģ“ė¦„ģ„ ė‹¤ģ‹œ ģ”ė‹ˆė‹¤. ė”•ģ…”ė„ˆė¦¬ ķ‚¤ėŠ” ė°”ė€Œģ§€ė§Œ ģ „ģ†”ė˜ėŠ” ģ“ė¦„ģ€ ė°”ė€Œģ§€ ģ•ŠģŠµė‹ˆė‹¤. diff --git a/i18n/ko/pages/client/transports.md b/i18n/ko/pages/client/transports.md index 801f67be..19b50e2e 100644 --- a/i18n/ko/pages/client/transports.md +++ b/i18n/ko/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # ķ“ė¼ģ“ģ–øķŠø ķŠøėžœģŠ¤ķ¬ķŠø {#client-transports} @@ -11,30 +11,15 @@ translation: 각 ķŠøėžœģŠ¤ķ¬ķŠøģ˜ **ģ„œė²„** ģŖ½(`mcp.run()`ģ“ ķ•˜ėŠ” ģ¼ź³¼ ė°°ķ¬ ėŒ€ģƒ)ģ€ **[ģ„œė²„ ģ‹¤ķ–‰ķ•˜źø°](../run/index.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. -## ģøė©”ėŖØė¦¬ {#in-memory} - -ģ„œė²„ ź°ģ²“ ģžģ²“ė„¼ ģ „ė‹¬ķ•˜ģ„øģš”. - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„, ķ¬ķŠøė„, ė„¤ķŠøģ›Œķ¬ė„¼ ģ˜¤ź°€ėŠ” ė°”ģ“ķŠøė„ ģ—†ģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøģ™€ ģ„œė²„ėŠ” ź°™ģ€ ķ”„ė”œģ„øģŠ¤ ģ•ˆģ˜ 두 ź°ģ²“ģ¼ ėæģ“ģ§€ė§Œ, ķ˜øģ¶œģ€ ģ—¬ģ „ķžˆ ģ‹¤ģ œ ķ”„ė”œķ† ģ½œ ź³„ģøµģ„ ź±°ģ¹©ė‹ˆė‹¤. `search_books`ėŠ” HTTP넼 통할 ė•Œģ™€ ė˜‘ź°™ģ“ ė‚˜ģ—“ė˜ź³ , ź²€ģ¦ė˜ź³ , ķ˜øģ¶œė©ė‹ˆė‹¤. - -ė•ė¶„ģ— ģ“ ė°©ģ‹ģ€ ė™ģ‹œģ— 두 가지 ģ—­ķ• ģ„ ķ•©ė‹ˆė‹¤. - -* **ķ…ŒģŠ¤ķŠø ė„źµ¬.** ģ“ ė¬øģ„œģ˜ ėŖØė“  ģ˜ˆģ œėŠ” ģ“ ė°©ģ‹ģœ¼ė”œ ģ‹¤ķ–‰ė˜ė©°, **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)** ķŽ˜ģ“ģ§€ėŠ” 전첓 ķŒØķ„“ģ„ ģ“ ė°©ģ‹ ģœ„ģ— źµ¬ģ¶•ķ•©ė‹ˆė‹¤. -* **ģž„ė² ė”© API.** ģ„œė²„ė„¼ 직접 ģƒģ„±ķ•˜ėŠ” ģ• ķ”Œė¦¬ģ¼€ģ“ģ…˜ģ€ ė„źµ¬ė„¼ ķ˜øģ¶œķ•˜źø° ģœ„ķ•“ ė„¤ķŠøģ›Œķ¬ė„¼ ź±°ģ¹  ķ•„ģš”ź°€ ģ—†ģŠµė‹ˆė‹¤. - ## Streamable HTTP {#streamable-http} -URL ė¬øģžģ—“ģ„ ģ „ė‹¬ķ•˜ė©“ **Streamable HTTP**넼 ģ–»ģŠµė‹ˆė‹¤. ė°°ķ¬ ģ‹œ ģ‚¬ģš©ķ•˜ėŠ” ķŠøėžœģŠ¤ķ¬ķŠøģž…ė‹ˆė‹¤. +URL ė¬øģžģ—“ģ„ ģ „ė‹¬ķ•˜ė©“ **Streamable HTTP**넼 ģ‚¬ģš©ķ•©ė‹ˆė‹¤. ė°°ķ¬ķ•  ė•Œ ź·ø 뒤에 ģ„œė²„ė„¼ ė‘ėŠ” ķŠøėžœģŠ¤ķ¬ķŠøģ“ģž, ź°€ģž„ 먼저 ģ„ ķƒķ•“ģ•¼ ķ•  ķŠøėžœģŠ¤ķ¬ķŠøģž…ė‹ˆė‹¤. ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -ģ“ź²ƒģ“ ķ”„ė”œė•ģ…˜ ķ“ė¼ģ“ģ–øķŠøģ˜ ģ „ė¶€ģž…ė‹ˆė‹¤. `Client`ź°€ URLģ„ `streamable_http_client(...)`딜 감싸 주며, ź·ø ģ•„ėž˜ģ—ėŠ” MCP에 ė§žź²Œ ģ„¤ģ •ėœ `httpx2.AsyncClient`ź°€ ģžˆģŠµė‹ˆė‹¤. `follow_redirects=True`, connect/write/pool에 30쓈 ķƒ€ģž„ģ•„ģ›ƒ, 그리고 ģ„œė²„ź°€ ģ‘ė‹µ ģŠ¤ķŠøė¦¼ģ„ ģ—“ģ–“ ė‘˜ 수 ģžˆģœ¼ėÆ€ė”œ readģ—ėŠ” 300쓈 ķƒ€ģž„ģ•„ģ›ƒģ“ ģ ģš©ė©ė‹ˆė‹¤. +ģ“ź²ƒģ“ ķ”„ė”œė•ģ…˜ ķ“ė¼ģ“ģ–øķŠøģ˜ ģ „ė¶€ģž…ė‹ˆė‹¤. `Client`ź°€ URLģ„ `streamable_http_client(...)`딜 감싸 주며, ź·ø ģ•„ėž˜ģ—ėŠ” MCP에 ė§žź²Œ ģ„¤ģ •ėœ `httpx2.AsyncClient`ź°€ ģžˆģŠµė‹ˆė‹¤. connect/write/poolģ—ėŠ” 30쓈 ķƒ€ģž„ģ•„ģ›ƒ, 그리고 ģ„œė²„ź°€ ģ‘ė‹µ ģŠ¤ķŠøė¦¼ģ„ ģ—“ģ–“ ė‘˜ 수 ģžˆģœ¼ėÆ€ė”œ readģ—ėŠ” 300쓈 ķƒ€ģž„ģ•„ģ›ƒģ“ ģ ģš©ė©ė‹ˆė‹¤. !!! check ģƒģ„±ė§Œ ķ•œ `Client`ėŠ” **ģ—°ź²°ė˜ģ§€ ģ•Šģ€** ģƒķƒœģž…ė‹ˆė‹¤. ģƒģ„±ģ€ ķŠøėžœģŠ¤ķ¬ķŠøė„¼ 고넼 ėæģ“ź³ , @@ -50,7 +35,7 @@ URL ė¬øģžģ—“ģ„ ģ „ė‹¬ķ•˜ė©“ **Streamable HTTP**넼 ģ–»ģŠµė‹ˆė‹¤. ė°°ķ¬ ģ‹œ `Authorization` ķ—¤ė”, 쿠키, ķ”„ė”ģ‹œ, mTLS, 다넸 ķƒ€ģž„ģ•„ģ›ƒģ“ ķ•„ģš”ķ•“ģ§€ėŠ” ģˆœź°„, `httpx2.AsyncClient`넼 직접 ė§Œė“¤ģ–“ `streamable_http_client`에 ė„˜źø°ģ„øģš”. -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ TLS ꓀련 ģ°øź³  ģ‚¬ķ•­ģ“ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. `httpx2`ėŠ” ė²ˆė“¤ėœ CA ėŖ©ė” !!! info `httpx2`ėŠ” ģµģˆ™ķ•œ `httpx` API넼 ź·øėŒ€ė”œ ģœ ģ§€ķ•˜ėÆ€ė”œ, `httpx`넼 ģ•ˆė‹¤ė©“ ģ—¬źø°ģ„œ ģøģ¦, ķ”„ė”ģ‹œ, ģ“ė²¤ķŠø ķ›…, ģž¬ģ‹œė„, ģ—°ź²° ģ œķ•œģ„ ė‹¤ė£ØėŠ” ė°©ė²•ė„ ģ“ėÆø ģ•„ėŠ” ģ…ˆģž…ė‹ˆė‹¤. SDKėŠ” ź·ø ģœ„ģ— ģ•„ė¬“ź²ƒė„ - ė”ķ•˜ģ§€ ģ•Šź³  ģ•„ė¬“ź²ƒė„ 빼지 ģ•ŠģŠµė‹ˆė‹¤. OAuthź°€ ģ—°ź²°ė˜ėŠ” ģ§€ģ ė„ ģ—¬źø°ģž…ė‹ˆė‹¤. + ė”ķ•˜ģ§€ ģ•Šź³  ģ•„ė¬“ź²ƒė„ 빼지 ģ•ŠģŠµė‹ˆė‹¤. 단, [ė¦¬ė””ė ‰ģ…˜ 처리](#redirects)ė§Œģ€ ģ˜ˆģ™øģž…ė‹ˆė‹¤. OAuthź°€ ģ—°ź²°ė˜ėŠ” ģ§€ģ ė„ ģ—¬źø°ģž…ė‹ˆė‹¤. `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. 전첓 ķė¦„ģ€ **[OAuth ķ“ė¼ģ“ģ–øķŠø](oauth-clients.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. +### ė¦¬ė””ė ‰ģ…˜ {#redirects} + +ķŠøėžœģŠ¤ķ¬ķŠøėŠ” ģ „ė‹¬ė°›ģ€ URL에 ģ—°ź²°ķ•˜ė©°, ź·ø ģ˜¤ė¦¬ģ§„ģ—ė§Œ ģ—°ź²°ķ•©ė‹ˆė‹¤. + +* ź°™ģ€ ģŠ¤ķ‚“, 호스트, ķ¬ķŠø ģ•ˆģ— ėØøė¬“ėŠ” `307`/`308` ė¦¬ė””ė ‰ģ…˜ģ€ ė”°ė¼ź°€ė©°, ź°™ģ€ ķ˜øģŠ¤ķŠøģ—ģ„œ `http://` → `https://`딜 ź°€ėŠ” ė¦¬ė””ė ‰ģ…˜ė„ ė”°ė¼ź°‘ė‹ˆė‹¤. ķ”ķ•œ `/mcp` → `/mcp/` 후행 ģŠ¬ėž˜ģ‹œ ė¦¬ė””ė ‰ģ…˜ģ“ 여기에 ķ¬ķ•Øė©ė‹ˆė‹¤. +* ź·ø ė°–ģ˜ 곳으딜 ź°€ėŠ” ė¦¬ė””ė ‰ģ…˜ģ€ ė”°ė¼ź°€ģ§€ **ģ•ŠģŠµė‹ˆė‹¤**. ķ˜øģ¶œģ€ ė‹¤ģŒ 오넘딜 ģ‹¤ķŒØķ•©ė‹ˆė‹¤. + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + ź·ø URLģ“ ģ˜ė„ķ•œ ģ„œė²„ė¼ė©“ 설정에 ź·ø URLģ„ ė„£ģœ¼ģ„øģš”. ģ•„ė‹ˆė¼ė©“ ģ„œė²„ė‚˜ ź·ø ģ•žģ˜ ķ”„ė”ģ‹œ ģ„¤ģ •ģ“ ģž˜ėŖ»ėœ ź²ƒģž…ė‹ˆė‹¤. + +ģ“ ź·œģ¹™ģ€ ģ „ė‹¬ķ•˜ėŠ” ģ–“ė–¤ `httpx2.AsyncClient`ģ—ė„ ģ ģš©ė©ė‹ˆė‹¤. MCP ģš”ģ²­ģ—ėŠ” `follow_redirects` ģ„¤ģ •ģ“ ģ–“ėŠ ė°©ķ–„ģœ¼ė”œė“  ģ°øģ”°ė˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. SDKģ˜ OAuth ķ”„ė”œė°”ģ“ė”ė„ ģžģ²“ ģš”ģ²­ģ— ź°™ģ€ ź·œģ¹™ģ„ ģ ģš©ķ•©ė‹ˆė‹¤. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP`ėŠ” + ģ„œė²„ź°€ ģžģ‹ ģ“ ėŖØė„“ėŠ” TLS ģ¢…ė£Œ ķ”„ė”ģ‹œ 뒤에 ģžˆģ–“ `http://` ė¦¬ė””ė ‰ģ…˜ģ„ 낓볓낓고 ģžˆė‹¤ėŠ” ėœ»ģž…ė‹ˆė‹¤. + ģ“ėŠ” ģ„œė²„ ģŖ½ģ—ģ„œ ź³ ģ¹˜ź±°ė‚˜(**[ė°°ķ¬ģ™€ ķ™•ģž„](../run/deploy.md#behind-a-tls-terminating-proxy)**), + ė©”ģ‹œģ§€ź°€ ģ œģ•ˆķ•˜ėŠ” ģ •ķ™•ķ•œ `https://…/` URLģ„ ģ‚¬ģš©ķ•“ ķ•“ź²°ķ•©ė‹ˆė‹¤. + ## stdio {#stdio} **stdio** ģ„œė²„ėŠ” ģ„œėøŒķ”„ė”œģ„øģŠ¤ģž…ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ ģ“ė„¼ ģ‹¤ķ–‰ķ•˜ź³ , stdin에 JSON-RPC넼 ģ“°ź³ , stdoutģ—ģ„œ JSON-RPC넼 ģ½ģŠµė‹ˆė‹¤. ė°ģŠ¤ķ¬ķ†± ķ˜øģŠ¤ķŠøź°€ ģ‚¬ģš©ģž ģ»“ķ“Øķ„°ģ—ģ„œ ģ„œė²„ė„¼ ģ‹¤ķ–‰ķ•˜ėŠ” ė°©ģ‹ģ“ ė°”ė”œ ģ“ź²ƒģž…ė‹ˆė‹¤. ķ˜øģŠ¤ķŠøėŠ” **ź³§** ģ“ ģ½”ė“œģ— UI넼 ė”ķ•œ ź²ƒģ“ė©°, **[ģ‹¤ģ œ ķ˜øģŠ¤ķŠøģ— ģ—°ź²°ķ•˜źø°](../get-started/real-host.md)**ėŠ” ź°™ģ€ ꓀계넼 호스트 ģŖ½ģ—ģ„œ 설정 ķŒŒģ¼ė”œ ė°”ė¼ė³ø ź²ƒģž…ė‹ˆė‹¤. @@ -105,6 +111,18 @@ TLS ꓀련 ģ°øź³  ģ‚¬ķ•­ģ“ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. `httpx2`ėŠ” ė²ˆė“¤ėœ CA ėŖ©ė” API 키가 ķ•„ģš”ķ•œ ģ„œė²„ėŠ” ź±°źø°ģ„œ 키넼 찾지 ėŖ»ķ•©ė‹ˆė‹¤. `env=`딜 ėŖ…ģ‹œģ ģœ¼ė”œ ģ „ė‹¬ķ•˜ģ„øģš”. 핓당 ė³€ģˆ˜ėŠ” ķ—ˆģš© ėŖ©ė” ģœ„ģ— ė³‘ķ•©ė©ė‹ˆė‹¤. ģœ„ ģ˜ˆģ œģ—ģ„œ `BOOKSHOP_API_KEY`ź°€ ķ•˜ėŠ” ģ¼ģ“ ė°”ė”œ ģ“ź²ƒģž…ė‹ˆė‹¤. +## ģøė©”ėŖØė¦¬ {#in-memory} + +ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” ė°°ķ¬ķ•  ź²ƒė„, 실행할 ź²ƒė„ ģ—†ģŠµė‹ˆė‹¤. ģ„œė²„ ź°ģ²“ ģžģ²“ė„¼ ģ „ė‹¬ķ•˜ģ„øģš”. + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„, ķ¬ķŠøė„, ė„¤ķŠøģ›Œķ¬ė„¼ ģ˜¤ź°€ėŠ” ė°”ģ“ķŠøė„ ģ—†ģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøģ™€ ģ„œė²„ėŠ” ź°™ģ€ ķ”„ė”œģ„øģŠ¤ ģ•ˆģ˜ 두 ź°ģ²“ģ“ė©°, ķ˜øģ¶œģ€ ģ—¬ģ „ķžˆ ģ‹¤ģ œ ķ”„ė”œķ† ģ½œ ź³„ģøµģ„ ź±°ģ¹©ė‹ˆė‹¤. `search_books`ėŠ” HTTP넼 통할 ė•Œģ™€ ė˜‘ź°™ģ“ ė‚˜ģ—“ė˜ź³ , ź²€ģ¦ė˜ź³ , ķ˜øģ¶œė©ė‹ˆė‹¤. **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)** ķŽ˜ģ“ģ§€ėŠ” 전첓 ķŒØķ„“ģ„ ģ“ ė°©ģ‹ ģœ„ģ— źµ¬ģ¶•ķ•©ė‹ˆė‹¤. + +ź°™ģ€ ķ˜•ķƒœź°€ ģž„ė² ė”© API ģ—­ķ• ė„ ź²øķ•©ė‹ˆė‹¤. ģ„œė²„ė„¼ 직접 ģƒģ„±ķ•˜ėŠ” ģ• ķ”Œė¦¬ģ¼€ģ“ģ…˜ģ€ ė„¤ķŠøģ›Œķ¬ė„¼ ź±°ģ¹˜ģ§€ ģ•Šź³  ģ„œė²„ģ˜ ė„źµ¬ė„¼ ķ˜øģ¶œķ•  수 ģžˆģŠµė‹ˆė‹¤. + ## SSE {#sse} `mcp.client.sse`ģ˜ `sse_client(url)`ģ€ Streamable HTTP딜 ėŒ€ģ²“ėœ ģ“ģ „ HTTP ķŠøėžœģŠ¤ķ¬ķŠøģž…ė‹ˆė‹¤. 아직 ģ“ ė°©ģ‹ģ„ ģ“°ėŠ” ģ„œė²„ģ™€ ķ†µģ‹ ķ•˜ė ¤ė©“ `Client(sse_client("http://localhost:8000/sse"))`처럼 ź°™ģ€ ė°©ģ‹ģœ¼ė”œ ź°ģ‹øģ„œ ģ‚¬ģš©ķ•˜ė˜, 새딜욓 ź²ƒģ„ ģ“ ģœ„ģ— ė§Œė“¤ģ§€ėŠ” ė§ˆģ„øģš”. @@ -113,15 +131,16 @@ TLS ꓀련 ģ°øź³  ģ‚¬ķ•­ģ“ ķ•˜ė‚˜ ģžˆģŠµė‹ˆė‹¤. `httpx2`ėŠ” ė²ˆė“¤ėœ CA ėŖ©ė” `Client`ģ—ź²Œ ģœ„ģ˜ ėŖØė“  ź²ƒģ€ ź°™ģ€ ź²ƒģž…ė‹ˆė‹¤. -**ķŠøėžœģŠ¤ķ¬ķŠø**ėž€ `(read, write)` ė©”ģ‹œģ§€ 스트림 ģŒģ„ ė‚“ģ–“ģ£¼ėŠ” ė¹„ė™źø° ģ»Øķ…ģŠ¤ķŠø ė§¤ė‹ˆģ €ė¼ė©“ ė¬“ģ—‡ģ“ė“  ķ•“ė‹¹ķ•©ė‹ˆė‹¤. ģ •ģ‹ģœ¼ė”œėŠ” `mcp.client`ģ˜ `Transport` ķ”„ė”œķ† ģ½œģž…ė‹ˆė‹¤. `Client`ėŠ” ģøģžė„¼ ķƒ€ģž…ģœ¼ė”œ źµ¬ė¶„ķ•©ė‹ˆė‹¤. ģ„œė²„ ź°ģ²“ėŠ” ķ”„ė”œģ„øģŠ¤ ė‚“ģ—ģ„œ ģ—°ź²°ķ•˜ź³ , `str`ģ€ `streamable_http_client(url)`ģ“ 되며, `StdioServerParameters`ėŠ” `stdio_client(params)`ź°€ 되고, ź·ø ė°–ģ˜ ź²ƒģ€ ķŠøėžœģŠ¤ķ¬ķŠøė”œ 직접 ģ§„ģž…ķ•©ė‹ˆė‹¤. ė§ˆģ§€ė§‰ ź·œģ¹™ ė•ė¶„ģ— `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`ź°€ 모두 ź°™ģ€ ģžė¦¬ģ— 들얓가고, 직접 ė§Œė“  ķŠøėžœģŠ¤ķ¬ķŠøė„ 쓸 수 ģžˆģŠµė‹ˆė‹¤. +**ķŠøėžœģŠ¤ķ¬ķŠø**ėž€ `(read, write)` ė©”ģ‹œģ§€ 스트림 ģŒģ„ ė‚“ģ–“ģ£¼ėŠ” ė¹„ė™źø° ģ»Øķ…ģŠ¤ķŠø ė§¤ė‹ˆģ €ė¼ė©“ ė¬“ģ—‡ģ“ė“  ķ•“ė‹¹ķ•©ė‹ˆė‹¤. ģ •ģ‹ģœ¼ė”œėŠ” `mcp.client`ģ˜ `Transport` ķ”„ė”œķ† ģ½œģž…ė‹ˆė‹¤. `Client`ėŠ” ģøģžė„¼ ķƒ€ģž…ģœ¼ė”œ źµ¬ė¶„ķ•©ė‹ˆė‹¤. `str`ģ€ `streamable_http_client(url)`ģ“ 되고, `StdioServerParameters`ėŠ” `stdio_client(params)`ź°€ 되며, ģ„œė²„ ź°ģ²“ėŠ” ķ”„ė”œģ„øģŠ¤ ė‚“ģ—ģ„œ ģ—°ź²°ķ•˜ź³ , ź·ø ė°–ģ˜ ź²ƒģ€ ķŠøėžœģŠ¤ķ¬ķŠøė”œ 직접 ģ§„ģž…ķ•©ė‹ˆė‹¤. ė§ˆģ§€ė§‰ ź·œģ¹™ ė•ė¶„ģ— `stdio_client(...)`, `streamable_http_client(...)`, `sse_client(...)`ź°€ 모두 ź°™ģ€ ģžė¦¬ģ— 들얓가고, 직접 ė§Œė“  ķŠøėžœģŠ¤ķ¬ķŠøė„ 쓸 수 ģžˆģŠµė‹ˆė‹¤. ## ģš”ģ•½ {#recap} -* `Client(mcp)`(ģ„œė²„ ź°ģ²“)ėŠ” ģøė©”ėŖØė¦¬ė”œ ģ—°ź²°ķ•©ė‹ˆė‹¤. ķ…ŒģŠ¤ķŠøģ™€ ģž„ė² ė”©ģ— ģ‚¬ģš©ķ•˜ģ„øģš”. * `Client("http://.../mcp")`(URL)ėŠ” ķ”„ė”œė•ģ…˜ ķŠøėžœģŠ¤ķ¬ķŠøģø Streamable HTTP딜 ģ—°ź²°ķ•©ė‹ˆė‹¤. * ķ—¤ė”, ģøģ¦, ķ”„ė”ģ‹œ, ķƒ€ģž„ģ•„ģ›ƒģ€ `streamable_http_client(url, http_client=...)`에 ģ „ė‹¬ķ•˜ėŠ” `httpx2.AsyncClient`에 ģ„¤ģ •ķ•©ė‹ˆė‹¤. `headers=` ķ‚¤ģ›Œė“œėŠ” ģ—†ģŠµė‹ˆė‹¤. +* ė¦¬ė””ė ‰ģ…˜ģ€ URL ģžģ²“ģ˜ ģ˜¤ė¦¬ģ§„ ģ•ˆģ—ģ„œė§Œ(후행 ģŠ¬ėž˜ģ‹œ `307`/`308`) ė”°ė¼ź°€ė©°, ź°™ģ€ ķ˜øģŠ¤ķŠøģ˜ `http`→`https`ė„ ė”°ė¼ź°‘ė‹ˆė‹¤. ź·ø ė°–ģ˜ ź²½ģš°ėŠ” `Redirect to … not followed`딜 ģ‹¤ķŒØķ•˜ėÆ€ė”œ ģµœģ¢… URLģ„ ģ„¤ģ •ķ•˜ģ„øģš”. * stdioėŠ” `Client(StdioServerParameters(...))`ģž…ė‹ˆė‹¤. ģžģ‹ ķ”„ė”œģ„øģŠ¤ģ˜ stderr넼 다넸 곳으딜 ėŒė¦“ ė•Œė§Œ 직접 `stdio_client(...)`딜 ź°ģ‹øģ„øģš”. * ģ„œėøŒķ”„ė”œģ„øģŠ¤ėŠ” ķ˜„ģž¬ ķ™˜ź²½ģ“ ģ•„ė‹ˆė¼ ķ—ˆģš© ėŖ©ė”ģ— ģžˆėŠ” ķ™˜ź²½ ė³€ģˆ˜ė§Œ ė°›ģŠµė‹ˆė‹¤. `env=`딜 여기에 ģ¶”ź°€ķ•©ė‹ˆė‹¤. +* `Client(mcp)`(ģ„œė²„ ź°ģ²“)ėŠ” ģøė©”ėŖØė¦¬ė”œ ģ—°ź²°ķ•©ė‹ˆė‹¤. ķ…ŒģŠ¤ķŠøģ—ģ„œ ģ‚¬ģš©ķ•˜ź±°ė‚˜, ģ„œė²„ė„¼ ė§Œė“  ģ• ķ”Œė¦¬ģ¼€ģ“ģ…˜ ģ•ˆģ— ģ„œė²„ė„¼ ģž„ė² ė”©ķ•  ė•Œ ģ‚¬ģš©ķ•˜ģ„øģš”. * ķŠøėžœģŠ¤ķ¬ķŠøėŠ” `async with x as (read, write)`딜 쓸 수 ģžˆėŠ” ź²ƒģ“ė©“ ė¬“ģ—‡ģ“ė“  ė©ė‹ˆė‹¤. `Client`ėŠ” ģ„œė²„ ź°ģ²“, URL, `StdioServerParameters`ź°€ ģ•„ė‹Œ ź²ƒģ€ 모두 ź·ø ķ”„ė”œķ† ģ½œģ— ź·øėŒ€ė”œ ė„˜ź¹ė‹ˆė‹¤. * `Client`넼 ģƒģ„±ķ•˜ė©“ ķŠøėžœģŠ¤ķ¬ķŠøź°€ ģ •ķ•“ģ§‘ė‹ˆė‹¤. `async with`ź°€ ģ“ė„¼ ģ—½ė‹ˆė‹¤. diff --git a/i18n/ko/pages/deprecated.md b/i18n/ko/pages/deprecated.md index 63d1d27a..cc1413a6 100644 --- a/i18n/ko/pages/deprecated.md +++ b/i18n/ko/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # 지원 중단 ģ˜ˆģ • 기늄 {#deprecated-features} -2026-07-28 ģ‚¬ģ–‘ģ€ 다섯 가지넼 ķ‡“ģ—­ģ‹œķ‚µė‹ˆė‹¤. SDKėŠ” ģ—¬ģ „ķžˆ ģ“ 다섯 가지넼 모두 źµ¬ķ˜„ķ•˜ė©°, ģ“ģ œ 모두에 **지원 중단 ģ˜ˆģ •(deprecated) 경고**ź°€ ė¶™ģŠµė‹ˆė‹¤. SDK ķ—¬ķ¼ ķ•˜ė‚˜ėŠ” ė³„ė„ģ˜ ģ“ģœ ė”œ 지원 중단 ģ˜ˆģ •ģ“ė©° [ķŽ˜ģ“ģ§€ ė](#deprecated-sdk-helpers)에 ģ •ė¦¬ė˜ģ–“ ģžˆģŠµė‹ˆė‹¤. +2026-07-28 ģ‚¬ģ–‘ģ€ 다섯 가지넼 ķ‡“ģ—­ģ‹œķ‚µė‹ˆė‹¤. SDKėŠ” ģ—¬ģ „ķžˆ ģ“ 다섯 가지넼 모두 źµ¬ķ˜„ķ•˜ė©°, ģ“ģ œ 모두에 **지원 중단 ģ˜ˆģ •(deprecated) 경고**ź°€ ė¶™ģŠµė‹ˆė‹¤. ėŖ‡ 가지 SDK ģˆ˜ģ¤€ģ˜ 지원 중단 ģ˜ˆģ • ķ•­ėŖ©ģ€ 사양과 ė³„ź°œģ˜ ģ“ģœ ė”œ ģ§€ģ •ėœ 것으딜, [ķŽ˜ģ“ģ§€ ė](#deprecated-sdk-helpers)에 ģ •ė¦¬ė˜ģ–“ ģžˆģŠµė‹ˆė‹¤. ģ•„ėž˜ ķ‘œėŠ” 지원 중단 ģ˜ˆģ •ģø 각 źø°ėŠ„ģ˜ ģ“ė¦„, ģ‚¬ė¼ģ§€ėŠ” ģ“ģœ , 그리고 ėŒ€ģ‹  ģ‚¬ģš©ķ•  ėŒ€ģ²“ ģˆ˜ė‹Øģ„ ģ •ė¦¬ķ•œ ź²ƒģž…ė‹ˆė‹¤. @@ -136,11 +136,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## 지원 중단 ģ˜ˆģ • SDK ķ—¬ķ¼ {#deprecated-sdk-helpers} -ģ“ź²ƒģ€ 사양 ė³€ź²½ģ“ ģ•„ė‹ˆė¼ ė” ė‚˜ģ€ ėŒ€ģ²“ ģˆ˜ė‹Øģ“ ģžˆėŠ” SDK ė‚“ė¶€ źµ¬ķ˜„ģ¼ ėæģž…ė‹ˆė‹¤. ź°™ģ€ `MCPDeprecationWarning`으딜 ź²½ź³ ķ•˜ė©° 3.0ģ—ģ„œ ģ œź±°ė©ė‹ˆė‹¤. +ģ“ź²ƒģ€ 사양 ė³€ź²½ģ“ ģ•„ė‹ˆė¼ ė” ė‚˜ģ€ ėŒ€ģ²“ ģˆ˜ė‹Øģ“ ģžˆėŠ” SDK ģ‚¬ģš© ė°©ģ‹ģ¼ ėæģž…ė‹ˆė‹¤. ź°™ģ€ `MCPDeprecationWarning`으딜 ź²½ź³ ķ•˜ė©°, 3.0ģ—ģ„œ 기씓 ķ˜•ķƒœź°€ ģ œź±°ė©ė‹ˆė‹¤. | 지원 중단 ģ˜ˆģ • | ėŒ€ģ‹  ķ•  ģ¼ | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`넼 ķ˜øģ¶œķ•œ ė’¤ `FuncMetadata.call_fn()`ģ„ ķ˜øģ¶œķ•˜ģ„øģš”. `FuncMetadata`넼 직접 ė‹¤ė£ØėŠ” ģ½”ė“œ(예넼 들얓 ģ‚¬ģš©ģž ģ •ģ˜ `Tool` ķ•˜ģœ„ ķ“ėž˜ģŠ¤)만 ģ“ ė©”ģ„œė“œė„¼ ķ˜øģ¶œķ–ˆģŠµė‹ˆė‹¤. | +| `validate_token_resource=` ģ—†ģ“ ģ“°ėŠ” `AuthSettings(resource_server_url=...)` | ź°’ģ„ ģ§€ģ •ķ•˜ģ„øģš”. `True`딜 ģ„¤ģ •ķ•˜ė©“ ź²€ģ¦źø°ź°€ `resource_server_url`용으딜 ė°œźø‰ė˜ģ—ˆė‹¤ź³  ė³“ź³ ķ•˜ģ§€ ģ•ŠėŠ” ė² ģ–“ėŸ¬ ķ† ķ°ģ„ ģ„œė²„ź°€ ź±°ė¶€ķ•˜ź³ , `False`ėŠ” ź²€ģ¦źø°ź°€ ķ† ķ°ģ˜ ėŒ€ģƒ(audience)ģ„ 직접 ķ™•ģøķ•œė‹¤ėŠ” ėœ»ģž…ė‹ˆė‹¤(**[ģøź°€](run/authorization.md#a-token-verifier)** ģ°øź³ ). ģ§€ģ •ķ•˜ģ§€ ģ•Šģœ¼ė©“ `False`처럼 ė™ģž‘ķ•˜ė©°, 3.0ė¶€ķ„°ėŠ” `resource_server_url`ģ“ ģ„¤ģ •ė˜ģ–“ ģžˆģœ¼ė©“ `True`ź°€ źø°ė³øź°’ģ“ ė©ė‹ˆė‹¤. | +| `issuer=` ģ—†ģ“ ģ“°ėŠ” `ClientCredentialsOAuthProvider(...)` ė˜ėŠ” `PrivateKeyJWTOAuthProvider(...)` | ģžź²© ģ¦ėŖ…ģ„ ė°œźø‰ķ•œ ģøź°€ ģ„œė²„ė„¼ ģ§€ģ •ķ•˜ėŠ” `issuer=`넼 ģ „ė‹¬ķ•˜ģ„øģš”(**[OAuth ķ“ė¼ģ“ģ–øķŠø ģž‘ģ„±ķ•˜źø°](client/oauth-clients.md#machine-to-machine)** ģ°øź³ ). ģ“ ź°’ģ“ ģ—†ģœ¼ė©“ ģ–“ėŠ ģøź°€ ģ„œė²„ź°€ ģžź²© ģ¦ėŖ…ģ„ ė°›ģ„ģ§€ė„¼ MCP ģ„œė²„ź°€ ź²°ģ •ķ•˜ź²Œ ė©ė‹ˆė‹¤. 3.0ė¶€ķ„°ėŠ” ģ“ ķ‚¤ģ›Œė“œź°€ ķ•„ģˆ˜ģž…ė‹ˆė‹¤. | ## ģš”ģ•½ {#recap} @@ -149,7 +151,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * 지원 중단 ģ˜ˆģ •ģ€ 권고 ģ‚¬ķ•­ģž…ė‹ˆė‹¤. ģ™€ģ“ģ–“ ė³€ź²½ģ€ 없고, 2026ė…„ ģ“ģ „ ģ„øģ…˜ģ—ģ„œėŠ” ėŖØė“  ź²ƒģ“ ź³„ģ† ė™ģž‘ķ•˜ė©°, ėˆˆģ— ė„ėŠ” `MCPDeprecationWarning`ģ“ ė‚˜ģ˜µė‹ˆė‹¤(`UserWarning`ģ“ėÆ€ė”œ 기본적으딜 켜져 ģžˆģŠµė‹ˆė‹¤). * ģƒ˜ķ”Œė§ź³¼ ė£ØķŠøėŠ” ģ¶”ź°€ė”œ 2026-07-28 ģ„øģ…˜ģ—ėŠ” ģ—†ėŠ” ė°±ģ±„ė„ģ“ ķ•„ģš”ķ•©ė‹ˆė‹¤. ģµœģ‹  ģ—°ź²°ģ—ģ„œėŠ” 경고넼 낸 ė’¤ ģ˜ˆģ™øė„¼ ė°œģƒģ‹œķ‚µė‹ˆė‹¤. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)`ģ€ ģ¹“ķ…Œź³ ė¦¬ 전첓넼 끄고, pytestģ˜ `"error::mcp.MCPDeprecationWarning"`ģ€ ģ“ė„¼ ķ…ŒģŠ¤ķŠø ģ‹¤ķŒØė”œ ė°”źæ‰ė‹ˆė‹¤. -* SDK ķ—¬ķ¼ ķ•˜ė‚˜ģø `FuncMetadata.call_fn_with_arg_validation()`ģ€ ė³„ė„ė”œ 지원 중단 ģ˜ˆģ •ģ“ė©° 3.0ģ—ģ„œ ģ œź±°ė©ė‹ˆė‹¤. +* [SDK ģˆ˜ģ¤€ģ˜ 지원 중단 ģ˜ˆģ • ķ•­ėŖ©](#deprecated-sdk-helpers)ė„ ź°™ģ€ ź·œģ¹™ģ„ ė”°ė¦…ė‹ˆė‹¤. ģ§€źøˆģ€ 경고넼 ė‚“ź³ , 3.0ģ—ģ„œ 기씓 ķ˜•ķƒœź°€ ģ œź±°ė©ė‹ˆė‹¤. * 새 ģ½”ė“œėŠ” ģ“ 기늄 중 ģ–“ėŠ ź²ƒģ—ė„ źø°ė°˜ķ•“ģ„œėŠ” ģ•ˆ ė©ė‹ˆė‹¤. ģ“ ė¬øģ„œģ˜ 다넸 ėŖØė“  ķŽ˜ģ“ģ§€ėŠ” ķ˜„ģž¬ API넼 ģ„¤ėŖ…ķ•©ė‹ˆė‹¤. diff --git a/i18n/ko/pages/get-started/first-steps.md b/i18n/ko/pages/get-started/first-steps.md index dab57cc9..19e9791b 100644 --- a/i18n/ko/pages/get-started/first-steps.md +++ b/i18n/ko/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # ģ²«ź±øģŒ {#first-steps} @@ -17,7 +17,7 @@ translation: * **ķ“ė¼ģ“ģ–øķŠø**ėŠ” 호스트 ģ•ˆģ— ģžˆģœ¼ė©° MCP딜 ķ†µģ‹ ķ•©ė‹ˆė‹¤. ķ˜øģŠ¤ķŠøėŠ” ģ—°ź²°ėœ ģ„œė²„ė§ˆė‹¤ ķ“ė¼ģ“ģ–øķŠøė„¼ ķ•˜ė‚˜ģ”© ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. * **ģ„œė²„**ėŠ” ģ“ SDK딜 ė§Œė“œėŠ” ź²ƒģž…ė‹ˆė‹¤. ģ„œė²„ėŠ” ķ“ė¼ģ“ģ–øķŠøģ— ģ—¬ėŸ¬ 가지넼 ė…øģ¶œķ•˜ė©°, ėŖØėøź³¼ 직접 ėŒ€ķ™”ķ•˜ėŠ” ģ¼ģ€ ģ—†ģŠµė‹ˆė‹¤. -직접 ģž‘ģ„±ķ•˜ėŠ” ź²ƒģ€ ģ„œė²„ģž…ė‹ˆė‹¤. ķ˜øģŠ¤ķŠøėŠ” 다넸 ėˆ„źµ°ź°€ź°€ ė§Œė“  ģ œķ’ˆģž…ė‹ˆė‹¤. SDKėŠ” `Client`ė„ ģ œź³µķ•©ė‹ˆė‹¤. ģ„œė²„ė„¼ ķ…ŒģŠ¤ķŠøķ•  ė•Œ ģ“°ź²Œ 되며, ģ“ ķŽ˜ģ“ģ§€ ė’·ė¶€ė¶„ģ—ģ„œ ė‹¤ģ‹œ ė“±ģž„ķ•©ė‹ˆė‹¤. +직접 ģž‘ģ„±ķ•˜ėŠ” ź²ƒģ€ ģ„œė²„ģž…ė‹ˆė‹¤. ķ˜øģŠ¤ķŠøėŠ” 다넸 ėˆ„źµ°ź°€ź°€ ė§Œė“  ģ œķ’ˆģž…ė‹ˆė‹¤. SDKėŠ” `Client`ė„ ģ œź³µķ•©ė‹ˆė‹¤. ķ˜øģŠ¤ķŠøź°€ URL딜 ģ„œė²„ģ— ģ ‘ģ†ķ•˜ź±°ė‚˜ ģ„œė²„ė„¼ ģ„œėøŒķ”„ė”œģ„øģŠ¤ė”œ 실행할 ė•Œ ģ“°ėŠ” ė°”ė”œ ź·ø ķ“ėž˜ģŠ¤ģž…ė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ ė’·ė¶€ė¶„ģ—ģ„œ ė‹¤ģ‹œ ė“±ģž„ķ•˜ė©°, ģž‘ģ„±ķ•œ ģ„œė²„ė„¼ ķ…ŒģŠ¤ķŠøķ•˜ėŠ” ģˆ˜ė‹Øģ“źø°ė„ ķ•©ė‹ˆė‹¤. ## 세 가지 ķ”„ė¦¬ėÆøķ‹°ėøŒ {#the-three-primitives} @@ -82,22 +82,20 @@ Inspectorģ—ģ„œ 탭 세 개넼 ė³“ģ•˜ģŠµė‹ˆė‹¤. Inspectorź°€ 세 ź°œė¼ėŠ” 것 ķ“ė¼ģ“ģ–øķŠøź°€ ģ—°ź²°ķ•˜ė©“ ģ„œė²„ėŠ” **기늄**, 즉 ģ–“ė–¤ ė¶€ė„˜ģ˜ ģš”ģ²­ģ— ģ‘ė‹µķ• ģ§€ė„¼ ģ„ ģ–øķ•©ė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøėŠ” ģ“ ģ„ ģ–øģ„ 볓고 ģ• ģ“ˆģ— ė¬“ģ—‡ģ„ ģš”ģ²­ķ• ģ§€ ź²°ģ •ķ•©ė‹ˆė‹¤. ģ“ ģ„ ģ–øģ„ ģž‘ģ„±ķ•œ ģ ģ€ ģ—†ģŠµė‹ˆė‹¤. `MCPServer`ź°€ ėŒ€ģ‹  ģ„ ģ–øķ•©ė‹ˆė‹¤. -직접 ķ™•ģøķ•“ ė³“ģ„øģš”. SDKģ˜ `Client`ėŠ” ģ„œė²„ ź°ģ²“ė„¼ ź·øėŒ€ė”œ 받아 **ģøė©”ėŖØė¦¬**딜 ģ—°ź²°ķ•©ė‹ˆė‹¤(ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„, ķ¬ķŠøė„ ģ—†ģŠµė‹ˆė‹¤). +직접 ķ™•ģøķ•“ ė³“ģ„øģš”. ķ•œģŖ½ ķ„°ėÆøė„ģ—ģ„œ `server.py`넼 HTTP딜 실행핓 ė‘ģ„øģš”. -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +그리고 다넸 ķ„°ėÆøė„ģ—ģ„œ ķ“ė¼ģ“ģ–øķŠøė„¼ ģ“ ģ„œė²„ģ— ģ—°ź²°ķ•˜ģ„øģš”. -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -117,8 +115,9 @@ asyncio.run(main()) ģ—†ėŠ” ź²ƒģ—ė„ ģ£¼ėŖ©ķ•˜ģ„øģš”. `completions`(ė¦¬ģ†ŒģŠ¤ ķ…œķ”Œė¦æź³¼ ķ”„ė”¬ķ”„ķŠøģ˜ ģøģž ģžė™ 완성)ģ—ėŠ” 직접 ģž‘ģ„±ķ•˜ėŠ” ķ•øė“¤ėŸ¬ź°€ ķ•„ģš”ķ•œė°, ģ“ ģ„œė²„ģ—ėŠ” ķ•øė“¤ėŸ¬ź°€ ģ—†ģœ¼ėÆ€ė”œ 핓당 źø°ėŠ„ģ“ ė¹ ģ ø ģžˆź³ , ģ˜¬ė°”ė„“ź²Œ ė™ģž‘ķ•˜ėŠ” ķ“ė¼ģ“ģ–øķŠøė¼ė©“ ģš”ģ²­ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ„ ķƒ ģ‚¬ķ•­ģ€ 모두 ģ“ ź·œģ¹™ģ„ ė”°ė¦…ė‹ˆė‹¤. ė“±ė”ķ•˜ė©“ źø°ėŠ„ģ“ ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. **[ģžė™ 완성](../servers/completions.md)** ķŽ˜ģ“ģ§€ź°€ ģ“ė„¼ 볓여 ģ¤ė‹ˆė‹¤. !!! info - `Client(mcp)`ėŠ” ģ“ ė¬øģ„œģ˜ ėŖØė“  예제넼 ķ…ŒģŠ¤ķŠøķ•˜ėŠ” ė° ģ“°ģ“ėŠ” ė°”ė”œ ź·ø ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠøģ“ė©°, - ģž‘ģ„±ķ•œ ģ„œė²„ė„ ź°™ģ€ ė°©ģ‹ģœ¼ė”œ ķ…ŒģŠ¤ķŠøķ•˜ź²Œ ė©ė‹ˆė‹¤. ģ“ė„¼ ė‹¤ė£ØėŠ” ķŽ˜ģ“ģ§€ź°€ ė”°ė”œ ģžˆģŠµė‹ˆė‹¤. **[ķ…ŒģŠ¤ķŠø](testing.md)**ģž…ė‹ˆė‹¤. + ģ“ `client.py`ėŠ” ģ™„ģ „ķ•œ MCP ķ“ė¼ģ“ģ–øķŠøģ“ė©°, ģ“ė„¼ ė‹¤ė£ØėŠ” ķŽ˜ģ“ģ§€ėŠ” **[ķ“ė¼ģ“ģ–øķŠø](../client/index.md)**ģž…ė‹ˆė‹¤. + ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” ķ„°ėÆøė„ė„ ķ¬ķŠøė„ ź±“ė„ˆė›°ź³  `Client(mcp)`처럼 `Client`에 ģ„œė²„ ź°ģ²“ ģžģ²“ė„¼ ė„˜ź¹ė‹ˆė‹¤. + ģ“ ģ—­ģ‹œ ģ „ģš© ķŽ˜ģ“ģ§€ź°€ ģžˆģŠµė‹ˆė‹¤. **[ķ…ŒģŠ¤ķŠø](testing.md)**ģž…ė‹ˆė‹¤. ## ģž‘ģ„±ķ•˜ģ§€ ģ•Šģ€ 것 {#what-you-did-not-write} @@ -127,7 +126,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int`ź°€ **ź³§** `add`ģ˜ ģŠ¤ķ‚¤ė§ˆģž…ė‹ˆė‹¤. * ģš”ģ²­ ķ•øė“¤ėŸ¬. `tools/list`, `resources/read`, `prompts/get`ģ€ 모두 ėŒ€ģ‹  ģ²˜ė¦¬ė©ė‹ˆė‹¤. * 기늄 ģ„ ģ–ø. `MCPServer`ź°€ ėŒ€ģ‹  ė§Œė“¤ģ—ˆģŠµė‹ˆė‹¤. -* ķ”„ė”œķ† ģ½œ ģ½”ė“œ 단 ķ•œ 줄. 버전 ķ˜‘ģƒ, JSON-RPC ķ”„ė ˆģ“ė°, 기늄 źµķ™˜ģ€ 모두 `mcp dev`와 `Client(mcp)` ģ•ˆģ—ģ„œ ģ¼ģ–“ė‚¬ź³ , ėˆˆģ— ė³“ģ“ģ§€ė„ ģ•Šģ•˜ģŠµė‹ˆė‹¤. +* ķ”„ė”œķ† ģ½œ ģ½”ė“œ 단 ķ•œ 줄. 버전 ķ˜‘ģƒ, JSON-RPC ķ”„ė ˆģ“ė°, 기늄 źµķ™˜ģ€ 모두 `mcp dev`와 `client.py` ģ•ˆģ—ģ„œ ģ¼ģ–“ė‚¬ź³ , ėˆˆģ— ė³“ģ“ģ§€ė„ ģ•Šģ•˜ģŠµė‹ˆė‹¤. ģ“ ė¹„ģœØģ“ģ•¼ė§ė”œ SDKź°€ ģ”“ģž¬ķ•˜ėŠ” ģ“ģœ ģž…ė‹ˆė‹¤. @@ -138,6 +137,6 @@ asyncio.run(main()) * ķ”„ė¦¬ėÆøķ‹°ėøŒė§ˆė‹¤ ė°ģ½”ė ˆģ“ķ„° ķ•˜ė‚˜ė©“ ė©ė‹ˆė‹¤. `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`ģž…ė‹ˆė‹¤. ģ“ė¦„, 설명, ģŠ¤ķ‚¤ė§ˆėŠ” ķ•Øģˆ˜ģ—ģ„œ ź°€ģ øģ˜µė‹ˆė‹¤. * `{param}`ģ“ 들얓간 URIėŠ” ė¦¬ģ†ŒģŠ¤ **ķ…œķ”Œė¦æ**ģ„ ė§Œė“¤ė©°, źµ¬ģ²“ģ ģø ė¦¬ģ†ŒģŠ¤ģ™€ėŠ” ė”°ė”œ ė‚˜ģ—“ė©ė‹ˆė‹¤. * ģ„œė²„ģ˜ **기늄**ģ€ ģžė™ģœ¼ė”œ ģ„ ģ–øė˜ė©°, ķ“ė¼ģ“ģ–øķŠøėŠ” ģ„œė²„ź°€ ģ„ ģ–øķ•œ 것만 ģš”ģ²­ķ•©ė‹ˆė‹¤. -* `Client(mcp)`ėŠ” ģ„œė²„ ź°ģ²“ģ— ģøė©”ėŖØė¦¬ė”œ ģ—°ź²°ķ•©ė‹ˆė‹¤. 첫날부터 ź°–ģ¶”ėŠ” ķ…ŒģŠ¤ķŠø ķ•˜ė„¤ģŠ¤ģž…ė‹ˆė‹¤. +* `Client("http://localhost:8000/mcp")`ėŠ” 실행 ģ¤‘ģø ģ„œė²„ģ™€ ķ†µģ‹ ķ•©ė‹ˆė‹¤. ėŒ€ģ‹  ģ„œė²„ ź°ģ²“ė„¼ ė„˜ź²Ø `Client(mcp)`딜 ė§Œė“¤ė©“ 첫날부터 ģ“°ėŠ” ķ…ŒģŠ¤ķŠø ķ•˜ė„¤ģŠ¤ź°€ ė©ė‹ˆė‹¤. ė‹¤ģŒģ€ **[ģ‹¤ģ œ ķ˜øģŠ¤ķŠøģ— ģ—°ź²°ķ•˜źø°](real-host.md)**ģž…ė‹ˆė‹¤. ģ“ ģ„œė²„ė„¼ Claude Desktopģ“ė‚˜ IDE ģ•ˆģ—ģ„œ ģ‹¤ģ œė”œ ėŒė ¤ ė“…ė‹ˆė‹¤. ź·øė‹¤ģŒģ€ **[ķ…ŒģŠ¤ķŠø](testing.md)**ģž…ė‹ˆė‹¤. ķŽ˜ģ“ģ§€ ķ•˜ė‚˜, ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠø ķ•˜ė‚˜ė©“ ė™ģž‘ķ•˜ėŠ”ģ§€ 추츔할 ģ¼ģ“ ģ—†ģ–“ģ§‘ė‹ˆė‹¤. ź·ø ė’¤ė”œėŠ” ķ”„ė¦¬ėÆøķ‹°ėøŒė§ˆė‹¤ ģ „ģš© ķŽ˜ģ“ģ§€ź°€ ģ“ģ–“ģ§€ė©°, ėŖØėøģ“ ģ£¼ė„ķ•˜ėŠ” ķ”„ė¦¬ėÆøķ‹°ėøŒģø **[ė„źµ¬](../servers/tools.md)**부터 ģ‹œģž‘ķ•©ė‹ˆė‹¤. diff --git a/i18n/ko/pages/get-started/testing.md b/i18n/ko/pages/get-started/testing.md index 2537f1b0..db1a83dc 100644 --- a/i18n/ko/pages/get-started/testing.md +++ b/i18n/ko/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # ķ…ŒģŠ¤ķŠø {#testing} -Python SDKėŠ” **ģøė©”ėŖØė¦¬ ķŠøėžœģŠ¤ķ¬ķŠø**넼 ź°–ģ¶˜ `Client` ķ“ėž˜ģŠ¤ė„¼ ģ œź³µķ•©ė‹ˆė‹¤. ģ„œė²„ ź°ģ²“ė„¼ ė„˜źø°ė©“ ź·ø ģ„œė²„ģ— 직접 ģ—°ź²°ķ•©ė‹ˆė‹¤. +SDKģ˜ `Client` ķ“ėž˜ģŠ¤ėŠ” URL에 ģ—°ź²°ķ•˜ź±°ė‚˜ ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„¼ ė„ģšø ė•Œ ģ“°ėŠ” ė°”ė”œ ź·ø ķ“ėž˜ģŠ¤ģøė°, **ģøė©”ėŖØė¦¬**ė”œė„ ģ—°ź²°ķ•©ė‹ˆė‹¤. ģ„œė²„ ź°ģ²“ė„¼ ė„˜źø°ė©“ ź·ø ģ„œė²„ģ™€ 직접 ķ†µģ‹ ķ•©ė‹ˆė‹¤. -ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„ ģ—†ģŠµė‹ˆė‹¤. ķ¬ķŠøė„ ģ—†ģŠµė‹ˆė‹¤. ķŠøėžœģŠ¤ķ¬ķŠøė„ ģ „ķ˜€ ģ—†ģŠµė‹ˆė‹¤. FastAPIģ˜ `TestClient`와 ź°™ģ€ ė°œģƒģž…ė‹ˆė‹¤. +ģ„œėøŒķ”„ė”œģ„øģŠ¤ė„ ģ—†ģŠµė‹ˆė‹¤. ķ¬ķŠøė„ ģ—†ģŠµė‹ˆė‹¤. ė„¤ķŠøģ›Œķ¬ė„¼ ģ˜¤ź°€ėŠ” ź²ƒė„ ģ „ķ˜€ ģ—†ģŠµė‹ˆė‹¤. FastAPIģ˜ `TestClient`와 ź°™ģ€ ė°œģƒģž…ė‹ˆė‹¤. ## źø°ė³ø ģ‚¬ģš©ė²• {#basic-usage} @@ -96,14 +96,14 @@ async def test_call_add_tool(client: Client): ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” 켜 ė‘ģ„øģš”. ķ”„ė”œė•ģ…˜ ģ½”ė“œģ—ģ„œėŠ” 아묓 ģ˜ėÆøź°€ ģ—†ģŠµė‹ˆė‹¤. -## źø°ė³øź°’ģ€ ģøķ”„ė”œģ„øģŠ¤ ģ—°ź²° {#in-process-by-default} +## źø°ė³øź°’ģ€ ģ„øėŒ€ 중립 {#era-neutral-by-default} !!! note `Client(mcp)`ėŠ” ģøķ”„ė”œģ„øģŠ¤ė”œ ģ—°ź²°ķ•˜ė©° 기본적으딜 **ģ„øėŒ€ 중립적**(era-neutral)ģž…ė‹ˆė‹¤. ģ„œė²„ė„¼ - 씰사핓 ģ•Œė§žģ€ ķ”„ė”œķ† ģ½œ 경딜넼 ź³ ė¦…ė‹ˆė‹¤. ķ…ŒģŠ¤ķŠøź°€ ė ˆź±°ģ‹œ ģ „ģš© ė™ģž‘, 즉 ģƒ˜ķ”Œė§ģ“ė‚˜ - ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜(elicitation) ķ‘øģ‹œ, `message_handler` ź°™ģ€ ź²ƒģ„ ź²€ģ¦ķ•œė‹¤ė©“ `mode="legacy"`딜 - ź³ ģ •ķ•˜ź³ , ź·ø ź²½ģš°ģ—ėŠ” `raise_exceptions=True`넼 ė¹¼ģ„øģš”. ė ˆź±°ģ‹œ ģ—°ź²°ģ€ ģ• ģ“ˆģ— ģ •ģ œė„¼ ķ•˜ģ§€ - ģ•Šģœ¼ė©°, ģ“ ķ”Œėž˜ź·øėŠ” ģ‹¤ķŒØė„¼ ķ…ŒģŠ¤ķŠøź°€ ģ•„ė‹ˆė¼ ģ„œė²„ 태스크 ģ•ˆģ—ģ„œ ė‹¤ģ‹œ ė°œģƒģ‹œķ‚¤źø° ė•Œė¬øģž…ė‹ˆė‹¤. + 씰사핓 ģ•Œė§žģ€ ķ”„ė”œķ† ģ½œ 경딜넼 ź³ ė¦…ė‹ˆė‹¤. ķ…ŒģŠ¤ķŠøź°€ ė ˆź±°ģ‹œ ģ „ģš© ė™ģž‘(ģƒ˜ķ”Œė§ģ“ė‚˜ + ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜(elicitation) ķ‘øģ‹œ, `message_handler`)ģ„ ź²€ģ¦ķ•œė‹¤ė©“ `mode="legacy"`딜 ź³ ģ •ķ•˜ź³ , + ź·ø ź²½ģš°ģ—ėŠ” `raise_exceptions=True`넼 ė¹¼ģ„øģš”. ė ˆź±°ģ‹œ ģ—°ź²°ģ€ ģ• ģ“ˆģ— ģ •ģ œė„¼ ķ•˜ģ§€ ģ•Šģœ¼ė©°, ģ“ + ķ”Œėž˜ź·øėŠ” ģ‹¤ķŒØė„¼ ķ…ŒģŠ¤ķŠøź°€ ģ•„ė‹ˆė¼ ģ„œė²„ 태스크 ģ•ˆģ—ģ„œ ė‹¤ģ‹œ ė°œģƒģ‹œķ‚¤źø° ė•Œė¬øģž…ė‹ˆė‹¤. ģ“ ė¬øģ„œģ˜ ģ˜ˆģ œź°€ ģ‹¤ģ œė”œ ė™ģž‘ķ•œė‹¤ź³  ģ•½ģ†ķ•  수 ģžˆėŠ” ź²ƒė„ ė°”ė”œ ź·ø ķ•œ 줄 ė•ė¶„ģž…ė‹ˆė‹¤. ėŖØė“  예제 ķŒŒģ¼ģ€ SDK ģžģ²“ģ˜ ķ…ŒģŠ¤ķŠø ģŠ¤ģœ„ķŠøģ—ģ„œ ģ‹¤ķ–‰ė˜ė©°, ź±°ģ˜ 전부가 ģ •ķ™•ķžˆ ģ“ ķ“ė¼ģ“ģ–øķŠøė„¼ ź±°ģ¹©ė‹ˆė‹¤. diff --git a/i18n/ko/pages/handlers/multi-round-trip.md b/i18n/ko/pages/handlers/multi-round-trip.md index 3955da96..f2c57798 100644 --- a/i18n/ko/pages/handlers/multi-round-trip.md +++ b/i18n/ko/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # 다중 왕복 ģš”ģ²­ {#multi-round-trip-requests} @@ -164,7 +164,7 @@ TTL, 주첓 ė°”ģøė”©, ģš”ģ²­ ė°”ģøė”©ģ€ ģ½”ė±ģ˜ ģ¼ģ“ **ģ•„ė‹™ė‹ˆė‹¤**. SD ## 2026-07-28 ģ „ģš© ź²°ź³¼ {#a-2026-07-28-result} -`InputRequiredResult`ėŠ” ķ”„ė”œķ† ģ½œ 버전 **2026-07-28**ģ—ė§Œ ģ”“ģž¬ķ•©ė‹ˆė‹¤. ģøė©”ėŖØė¦¬ `Client(server)`ėŠ” ģ“ė„¼ ėŒ€ģ‹  ķ˜‘ģƒķ•˜ź³ , ė„¤ķŠøģ›Œķ¬ė„¼ 통할 ė•ŒėŠ” `mode="auto"`ź°€ ģ“ė„¼ ė°œź²¬ķ•©ė‹ˆė‹¤. ģ—°ź²°ķ•œ ė’¤ģ—ėŠ” `client.protocol_version`ģ“ ė¬“ģ—‡ģ„ ģ–»ģ—ˆėŠ”ģ§€ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. +`InputRequiredResult`ėŠ” ķ”„ė”œķ† ģ½œ 버전 **2026-07-28**ģ—ė§Œ ģ”“ģž¬ķ•©ė‹ˆė‹¤. `Client`ģ˜ źø°ė³øź°’ģø `mode="auto"`ėŠ” ģ–“ė–¤ ģ—°ź²°ģ—ģ„œė“  ģ“ė„¼ ė°œź²¬ķ•©ė‹ˆė‹¤. ģ—°ź²°ķ•œ ė’¤ģ—ėŠ” `client.protocol_version`ģ“ ė¬“ģ—‡ģ„ ģ–»ģ—ˆėŠ”ģ§€ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. !!! warning 2026 ģ“ģ „ ģ„øģ…˜ģ—ėŠ” `InputRequiredResult`넼 ė„£ģ„ ź³³ģ“ ģ—†ģŠµė‹ˆė‹¤. `mode="legacy"` ģ—°ź²°ģ—ģ„œ diff --git a/i18n/ko/pages/handlers/progress.md b/i18n/ko/pages/handlers/progress.md index 0b0bbd98..d576a917 100644 --- a/i18n/ko/pages/handlers/progress.md +++ b/i18n/ko/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # ģ§„ķ–‰ ģƒķ™© {#progress} @@ -29,19 +29,17 @@ translation: ķ“ė¼ģ“ģ–øķŠøėŠ” **호출 ė‹Øģœ„ė”œ** ģˆ˜ģ‹ ģ„ ģ„ ķƒķ•©ė‹ˆė‹¤. `call_tool`에 `progress_callback=` ģøģžė„¼ ģ „ė‹¬ķ•˜ė©“ ė©ė‹ˆė‹¤. -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,27 +54,31 @@ anyio.run(main) ģ½œė°±ģ€ ģ„œė²„ź°€ ė³“ź³ ķ•œ ź°’ ź·øėŒ€ė”œ, 즉 `progress`, `total`, `message`넼 ė°›ėŠ” `async` ķ•Øģˆ˜ģž…ė‹ˆė‹¤. !!! info - `Client(mcp)`ėŠ” ģ„œė²„ ź°ģ²“ģ— 메모리 ģ•ˆģ—ģ„œ 직접 ģ—°ź²°ķ•˜ė©°, **[ķ…ŒģŠ¤ķŠø](../get-started/testing.md)** - ķŽ˜ģ“ģ§€ģ˜ źø°ė°˜ģ“ ė˜ėŠ” 것과 ź°™ģ€ ķ“ė¼ģ“ģ–øķŠøģž…ė‹ˆė‹¤. `progress_callback`ģ€ `Client`ź°€ ģ–“ė–¤ ķŠøėžœģŠ¤ķ¬ķŠøė„¼ - ģ“°ė“  ź°™ģ€ ė§¤ź°œė³€ģˆ˜ģž…ė‹ˆė‹¤. ė‹¤ė§Œ ź³§ 볓게 될 **ķƒ€ģ“ė°**ģ€ ģøė©”ėŖØė¦¬ ģ—°ź²°ģ˜ ķƒ€ģ“ė°ģž…ė‹ˆė‹¤. ģøė©”ėŖØė¦¬ ģ—°ź²°ģ€ - ģ½œė°±ģ„ ģøė¼ģøģœ¼ė”œ ģ‹¤ķ–‰ķ•˜ėÆ€ė”œ ėŖØė“  볓고가 `call_tool`ģ“ ė°˜ķ™˜ė˜źø° 전에 ė„ģ°©ķ•©ė‹ˆė‹¤. ģ‹¤ģ œ ķŠøėžœģŠ¤ķ¬ķŠøģ—ģ„œėŠ” - ģ•Œė¦¼ź³¼ 결과가 ź²½ģŸķ•˜ėÆ€ė”œ, 느린 ģ½œė°±ģ€ `call_tool`ģ“ ė°˜ķ™˜ėœ ė’¤ģ—ė„ ģ—¬ģ „ķžˆ 실행 ģ¤‘ģ¼ 수 ģžˆģŠµė‹ˆė‹¤. + `progress_callback`ģ€ `Client`에 ė¬“ģ—‡ģ„ ė„˜ź²¼ė“  ź°™ģ€ ė§¤ź°œė³€ģˆ˜ģž…ė‹ˆė‹¤. ģ—¬źø°ģ„œģ²˜ėŸ¼ URLģ“ė“ , + `StdioServerParameters`ė“ , ķ…ŒģŠ¤ķŠøģ—ģ„œ ģ“°ėŠ” ģ„œė²„ ź°ģ²“ė“  ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤. ė‹¤ė§Œ ģ‹¤ģ œ ķŠøėžœģŠ¤ķ¬ķŠøģ—ģ„œėŠ” + ķƒ€ģ“ė°ģ— ģ£¼ģ˜ķ•˜ģ„øģš”. 각 ģ•Œė¦¼ģ€ ģ‘ė‹µź³¼ ė³„ė„ė”œ ė”°ė”œ ģ „ė‹¬ė˜ėÆ€ė”œ, 느린 ģ½œė°±ģ€ `call_tool`ģ“ ė°˜ķ™˜ėœ + ė’¤ģ—ė„ ģ—¬ģ „ķžˆ 실행 ģ¤‘ģ¼ 수 ģžˆģŠµė‹ˆė‹¤. ķ”„ė”œģ„øģŠ¤ ė‚“ ķ…ŒģŠ¤ķŠø ģ—°ź²°ė§Œģ“ ģ½œė°±ģ„ ģøė¼ģøģœ¼ė”œ ģ‹¤ķ–‰ķ•˜ģ—¬ ėŖØė“  + 볓고가 먼저 ė„ģ°©ķ•˜ė„ė” ė³“ģž„ķ•©ė‹ˆė‹¤. ### 직접 ķ•“ 볓기 {#try-it} -`client.py`넼 `server.py` ģ˜†ģ— 두고 ģ‹¤ķ–‰ķ•˜ģ„øģš”. +`server.py`넼 HTTP딜 ģ„œė¹„ģŠ¤ķ•œ ė‹¤ģŒ, 두 번째 ķ„°ėÆøė„ģ—ģ„œ ķ“ė¼ģ“ģ–øķŠøė„¼ ģ‹¤ķ–‰ķ•˜ģ„øģš”. + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -ģ„œė²„ģ˜ `await ctx.report_progress(...)` ķ•˜ė‚˜ķ•˜ė‚˜ź°€ ķ“ė¼ģ“ģ–øķŠøģ—ģ„œ `show` 호출 ķ•˜ė‚˜ź°€ ė˜ģ—ˆź³ , ģˆœģ„œė„ ź·øėŒ€ė”œģ“ė©°, 두 줄 모두 `call_tool`ģ“ ė°˜ķ™˜ė˜źø° **전에** ģ¶œė „ė˜ģ—ˆģŠµė‹ˆė‹¤. ģ§„ķ–‰ ģƒķ™©ģ€ 결과에 묶여 ģ˜¤ģ§€ ģ•Šź³ , ė„źµ¬ź°€ 아직 ģž‘ģ—…ķ•˜ėŠ” ė™ģ•ˆ ģŠ¤ķŠøė¦¬ė°ė©ė‹ˆė‹¤. +ģ„œė²„ģ˜ `await ctx.report_progress(...)` ķ•˜ė‚˜ķ•˜ė‚˜ź°€ ķ“ė¼ģ“ģ–øķŠøģ—ģ„œ `show` 호출 ķ•˜ė‚˜ź°€ ė˜ģ—ˆź³ , ģˆœģ„œė„ ź·øėŒ€ė”œģž…ė‹ˆė‹¤. ģ§„ķ–‰ ģƒķ™©ģ€ 결과에 묶여 ģ˜¤ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ė„źµ¬ź°€ 아직 ģž‘ģ—…ķ•˜ėŠ” ė™ģ•ˆ ģŠ¤ķŠøė¦¬ė°ė©ė‹ˆė‹¤. !!! warning `progress_callback`ģ€ `Client`ź°€ ģ•„ė‹ˆė¼ **호출**에 ģ†ķ•©ė‹ˆė‹¤. ģ“ė„¼ ģœ„ķ•œ ģƒģ„±ģž ģøģžėŠ” ģ—†ģŠµė‹ˆė‹¤. diff --git a/i18n/ko/pages/protocol-versions.md b/i18n/ko/pages/protocol-versions.md index 033e968c..be44b46c 100644 --- a/i18n/ko/pages/protocol-versions.md +++ b/i18n/ko/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # ķ”„ė”œķ† ģ½œ 버전 {#protocol-versions} @@ -11,9 +11,17 @@ MCPģ—ėŠ” 두 ģ‹œėŒ€ź°€ ģžˆģŠµė‹ˆė‹¤. `Client`ź°€ ėŒ€ģ‹  ķ˜‘ģƒķ•˜ėÆ€ė”œ ģ‹ ź²½ 쓸 ģ¼ģ€ ź±°ģ˜ ģ—†ģŠµė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ėŠ” ģ“ė„¼ ģ œģ–“ķ•˜ėŠ” 단 ķ•˜ė‚˜ģ˜ ģƒģ„±ģž ģøģžģø `mode=`와 ģ“ ź°’ģ„ ė°”ź¾øź²Œ ė˜ėŠ” 세 가지 경우넼 ė‹¤ė£¹ė‹ˆė‹¤. +ģ“ ķŽ˜ģ“ģ§€ģ˜ ėŖØė“  ģ½”ė“œ ģ”°ź°ģ€ **[ķ“ė¼ģ“ģ–øķŠø](client/index.md)**에 ė‚˜ģ˜Ø Bookshop `server.py`와 ķ†µģ‹ ķ•˜ėŠ” `client.py`ģž…ė‹ˆė‹¤. ķ•œ ķ„°ėÆøė„ģ—ģ„œ ź·ø ģ„œė²„ė„¼ ģ‹œģž‘ķ•˜ģ„øģš”. + +```console +uv run mcp run server.py --transport streamable-http +``` + +그런 ė‹¤ģŒ 두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`딜 각 ģ½”ė“œ ģ”°ź°ģ„ ģ‹¤ķ–‰ķ•˜ģ„øģš”. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ MCPģ—ėŠ” 두 ģ‹œėŒ€ź°€ ģžˆģŠµė‹ˆė‹¤. ģ“ź²ƒģ“ źø°ėŠ„ģ˜ ģ „ė¶€ģž…ė‹ˆė‹¤. `Client` ķ•˜ė‚˜ė”œ ģ–“ėŠ ģ‹œėŒ€ģ˜ ģ„œė²„ė“  ģƒėŒ€ķ•˜ė©°, ģ½”ė“œģ— ė¶„źø°ź°€ ķ•„ģš” ģ—†ģŠµė‹ˆė‹¤. !!! info - `MCPServer`ėŠ” ģøė©”ėŖØė¦¬, stdio, Streamable HTTP 등 ėŖØė“  ķŠøėžœģŠ¤ķ¬ķŠøģ—ģ„œ `server/discover`에 - ģ‘ė‹µķ•˜ėÆ€ė”œ, 직접 ģž‘ģ„±ķ•œ ģ„œė²„ė„¼ ģƒėŒ€ė”œėŠ” `auto`ź°€ ķ•­ģƒ `2026-07-28`에 ė„ė‹¬ķ•©ė‹ˆė‹¤. ķ“ė°±ģ€ - ģ‹¤ģ œ 2026ė…„ ģ“ģ „ ģ„œė²„ė„¼ ģƒėŒ€ķ•  ė•Œė§Œ ė°œė™ķ•˜ė©°, ė°”ė”œ ź·øė•Œź°€ ķ“ė°±ģ“ ķ•„ģš”ķ•œ ģˆœź°„ģž…ė‹ˆė‹¤. + `MCPServer`ėŠ” Streamable HTTP, stdio, 그리고 ķ…ŒģŠ¤ķŠøģ—ģ„œ ģ“°ėŠ” ķ”„ė”œģ„øģŠ¤ ė‚“ ģ—°ź²°ź¹Œģ§€ ėŖØė“  + ķŠøėžœģŠ¤ķ¬ķŠøģ—ģ„œ `server/discover`에 ģ‘ė‹µķ•˜ėÆ€ė”œ, 직접 ģž‘ģ„±ķ•œ ģ„œė²„ė„¼ ģƒėŒ€ė”œėŠ” `auto`ź°€ ķ•­ģƒ + `2026-07-28`에 ė„ė‹¬ķ•©ė‹ˆė‹¤. ķ“ė°±ģ€ ģ‹¤ģ œ 2026ė…„ ģ“ģ „ ģ„œė²„ė„¼ ģƒėŒ€ķ•  ė•Œė§Œ ė°œė™ķ•˜ė©°, ė°”ė”œ ź·øė•Œź°€ + ķ“ė°±ģ“ ķ•„ģš”ķ•œ ģˆœź°„ģž…ė‹ˆė‹¤. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ MCPģ—ėŠ” 두 ģ‹œėŒ€ź°€ ģžˆģŠµė‹ˆė‹¤. `mode`ģ—ėŠ” ģµœģ‹  ķ”„ė”œķ† ģ½œ 버전 ė¬øģžģ—“ė„ ė„£ģ„ 수 ģžˆģŠµė‹ˆė‹¤. ķ˜„ģž¬ ź·ø ģ§‘ķ•©ģ€ ģ •ķ™•ķžˆ `["2026-07-28"]`ģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 ź·øėŸ¬ė‹ˆ 볓꓀핓 ė‘ģ„øģš”. `auto` ģ—°ź²° 후 `client.session.discover_result`ģ—ėŠ” ģ„œė²„ź°€ 볓낸 `DiscoverResult`ź°€ ź·øėŒ€ė”œ 닓겨 ģžˆģŠµė‹ˆė‹¤. `supported_versions`, `capabilities`, `instructions`, 그리고 ģ„œė²„ź°€ ź²°ź³¼ģ˜ `_meta`에 새겨 ė„£ģ€ 신원 ģ •ė³“ź¹Œģ§€ ķ¬ķ•Øė©ė‹ˆė‹¤. ė‹¤ģŒė²ˆģ—ėŠ” ģ“ė„¼ `prior_discover=`딜 ė‹¤ģ‹œ ė„˜źø°ģ„øģš”. -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ko/pages/run/asgi.md b/i18n/ko/pages/run/asgi.md index 1c1795f0..46fe8f24 100644 --- a/i18n/ko/pages/run/asgi.md +++ b/i18n/ko/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # 기씓 앱에 ģ¶”ź°€ķ•˜źø° {#add-to-an-existing-app} @@ -98,7 +98,7 @@ Starletteģ˜ `Host` ė¼ģš°ķŠøė„ ź°™ģ€ ė°©ģ‹ģœ¼ė”œ ė™ģž‘ķ•©ė‹ˆė‹¤. 경딜 ėŒ€ --8<-- "docs_src/asgi/tutorial004.py" ``` -ģ“ģ œ ķ“ė¼ģ“ģ–øķŠøėŠ” `/notes/mcp`ź°€ ģ•„ė‹ˆė¼ `/notes`에 ģ—°ź²°ķ•©ė‹ˆė‹¤. +ģ“ģ œ ķ“ė¼ģ“ģ–øķŠøėŠ” `/notes/mcp`ź°€ ģ•„ė‹ˆė¼ `/notes/`에 ģ—°ź²°ķ•©ė‹ˆė‹¤. ## ėøŒė¼ģš°ģ € ķ“ė¼ģ“ģ–øķŠøė„¼ ģœ„ķ•œ CORS {#cors-for-browser-clients} @@ -141,4 +141,4 @@ Starletteģ˜ `Host` ė¼ģš°ķŠøė„ ź°™ģ€ ė°©ģ‹ģœ¼ė”œ ė™ģž‘ķ•©ė‹ˆė‹¤. 경딜 ėŒ€ * ėøŒė¼ģš°ģ € ķ“ė¼ģ“ģ–øķŠøģ—ėŠ” CORSź°€ ķ•„ģš”ķ•©ė‹ˆė‹¤. `Mcp-*` ģš”ģ²­ ķ—¤ė”ė„¼ ģœ„ķ•œ `allow_headers`, ģ‘ė‹µģ„ ģœ„ķ•œ `expose_headers=["Mcp-Session-Id"]`ģž…ė‹ˆė‹¤. * `@mcp.custom_route()`ėŠ” `/mcp` ģ˜†ģ— ģøģ¦ė˜ģ§€ ģ•ŠėŠ” ķ‰ė²”ķ•œ HTTP ģ—”ė“œķ¬ģøķŠøė„¼ ģ¶”ź°€ķ•©ė‹ˆė‹¤. -ģ„œė²„ź°€ ģ‹¤ģ œ URL딜 ė„ė‹¬ ź°€ėŠ„ķ•“ģ§€ė©“, **[ķ“ė¼ģ“ģ–øķŠø](../client/index.md)**ėŠ” ģ„œė²„ ź°ģ²“ ėŒ€ģ‹  ź·ø URL딜 ģ—°ź²°ķ•©ė‹ˆė‹¤. +ģ„œė²„ź°€ ģ‹¤ģ œ URL딜 ė„ė‹¬ ź°€ėŠ„ķ•“ģ§€ė©“, **[ķ“ė¼ģ“ģ–øķŠø](../client/index.md)**ėŠ” ź·ø URL딜 ģ„œė²„ģ— ģ—°ź²°ķ•©ė‹ˆė‹¤. diff --git a/i18n/ko/pages/run/authorization.md b/i18n/ko/pages/run/authorization.md index 3e2e68a8..e26d3a9f 100644 --- a/i18n/ko/pages/run/authorization.md +++ b/i18n/ko/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # ģøź°€ {#authorization} @@ -23,12 +23,12 @@ OAuth ģš©ģ–“ė”œ ė§ķ•˜ė©“ ģ„œė²„ėŠ” **ė¦¬ģ†ŒģŠ¤ ģ„œė²„**ģž…ė‹ˆė‹¤. ėˆ„źµ¬ė„ 딜 SDKėŠ” ģœ ķšØķ•œ ķ† ķ°ģ“ ģ–“ė–¤ ėŖØģŠµģøģ§€ģ— ėŒ€ķ•“ ģ•„ė¬“ėŸ° ģ˜ź²¬ģ“ ģ—†ģŠµė‹ˆė‹¤. **`TokenVerifier`**넼 źµ¬ķ˜„ķ•“ģ„œ ģ•Œė ¤ 주멓 ė©ė‹ˆė‹¤. -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier`ėŠ” async ė©”ģ„œė“œ ķ•˜ė‚˜ė„¼ 가진 ķ”„ė”œķ† ģ½œģž…ė‹ˆė‹¤. `verify_token`ģ€ `Authorization` ķ—¤ė”ģ—ģ„œ 꺼낸 ģ›ģ‹œ ķ† ķ°ģ„ 받아, ģœ ķšØķ•˜ė©“ **`AccessToken`**ģ„, ģœ ķšØķ•˜ģ§€ ģ•Šģœ¼ė©“ `None`ģ„ ė°˜ķ™˜ķ•©ė‹ˆė‹¤. ź·ø 외에 źµ¬ķ˜„ķ•  ź²ƒģ€ ģ—†ģŠµė‹ˆė‹¤. -* ģ“ ģ˜ˆģ œėŠ” ķ…Œģ“ėø”ģ—ģ„œ ķ† ķ°ģ„ ģ”°ķšŒķ•©ė‹ˆė‹¤. ģ‹¤ģ œ źµ¬ķ˜„ģ€ JWT ģ„œėŖ…ģ„ ź²€ģ¦ķ•˜ź±°ė‚˜ ģøź°€ ģ„œė²„ģ˜ 토큰 ģøķŠøė”œģŠ¤ķŽ™ģ…˜ ģ—”ė“œķ¬ģøķŠøė„¼ ķ˜øģ¶œķ•©ė‹ˆė‹¤. ź·ø ģ½”ė“œėŠ” 직접 ģž‘ģ„±ķ•˜ėŠ” ź²ƒģ“ź³ , SDKėŠ” 호출만 ķ•©ė‹ˆė‹¤. +* ģ“ ģ˜ˆģ œėŠ” ķ…Œģ“ėø”ģ—ģ„œ ķ† ķ°ģ„ ģ”°ķšŒķ•˜ė©°, 각 ķ•­ėŖ©ģ—ėŠ” ź·ø ķ† ķ°ģ“ ė°œźø‰ėœ ėŒ€ģƒ ė¦¬ģ†ŒģŠ¤ź°€ źø°ė”ė˜ģ–“ ģžˆģŠµė‹ˆė‹¤. ģ‹¤ģ œ źµ¬ķ˜„ģ€ JWT ģ„œėŖ…ģ„ ź²€ģ¦ķ•˜ź±°ė‚˜ ģøź°€ ģ„œė²„ģ˜ 토큰 ģøķŠøė”œģŠ¤ķŽ™ģ…˜ ģ—”ė“œķ¬ģøķŠøė„¼ ķ˜øģ¶œķ•˜ź³ , ķ† ķ°ģ“ ėˆ„źµ¬ė„¼ ģœ„ķ•“ ė°œźø‰ė˜ģ—ˆėŠ”ģ§€(ķ† ķ°ģ˜ `aud`)넼 `AccessToken.resource`에 ė‹“ģ•„ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. ź·ø ģ½”ė“œėŠ” 직접 ģž‘ģ„±ķ•˜ėŠ” ź²ƒģ“ź³ , SDKėŠ” 호출만 ķ•©ė‹ˆė‹¤. * `token_verifier=`와 `auth=`ėŠ” ķ•­ģƒ ķ•Øź»˜ ė‹¤ė‹™ė‹ˆė‹¤. ķ•œģŖ½ė§Œ ģ „ė‹¬ķ•˜ė©“ `MCPServer(...)`ź°€ ģš”ģ²­ģ„ ķ•˜ė‚˜ė„ ģ²˜ė¦¬ķ•˜źø° 전에 `ValueError`넼 ė°œģƒģ‹œķ‚µė‹ˆė‹¤. `AuthSettings`ėŠ” ė¦¬ģ†ŒģŠ¤ ģ„œė²„ģ˜ 공개 ģ •ė³“ģž…ė‹ˆė‹¤. @@ -36,6 +36,10 @@ SDKėŠ” ģœ ķšØķ•œ ķ† ķ°ģ“ ģ–“ė–¤ ėŖØģŠµģøģ§€ģ— ėŒ€ķ•“ ģ•„ė¬“ėŸ° ģ˜ź²¬ģ“ ģ—† * `issuer_url`: ķ† ķ°ģ„ ė°œźø‰ķ•˜ėŠ” ģøź°€ ģ„œė²„ģž…ė‹ˆė‹¤. * `resource_server_url`: ģ“ MCP ģ—”ė“œķ¬ģøķŠøģ˜ 공개 URLģž…ė‹ˆė‹¤. ķ† ķ°ģ“ **ģ–“ė–¤** ė¦¬ģ†ŒģŠ¤ė„¼ ģœ„ķ•œ ź²ƒģøģ§€ ģ§€ģ¹­ķ•˜ė©°, ė””ģŠ¤ģ»¤ė²„ė¦¬ ė¬øģ„œź°€ ģœ„ģ¹˜ķ•˜ėŠ” ź³³ģ“źø°ė„ ķ•©ė‹ˆė‹¤. * `required_scopes`: ėŖØė“  ķ† ķ°ģ“ ģ“ ģŠ¤ģ½”ķ”„ė„¼ ģ „ė¶€ 가지고 ģžˆģ–“ģ•¼ ķ•©ė‹ˆė‹¤. +* `validate_token_resource`: `AccessToken.resource`ź°€ `resource_server_url`ģ“ ģ•„ė‹Œ ķ† ķ°ģ€ 모두 ź±°ė¶€ķ•©ė‹ˆė‹¤. `resource_server_url`ģ€ ģ„¤ģ •ķ•˜ź³  ģ“ ź°’ģ„ ģ„¤ģ •ķ•˜ģ§€ ģ•Šģ€ ģ±„ė”œ 두멓 경고(`MCPDeprecationWarning`)ź°€ ė°œģƒķ•˜ė©° `False`딜 ė™ģž‘ķ•©ė‹ˆė‹¤. 3.0ģ—ģ„œėŠ” ė¦¬ģ†ŒģŠ¤ ģ„œė²„ģ˜ źø°ė³øź°’ģ“ `True`ź°€ ė©ė‹ˆė‹¤. + * ģøź°€ ģ„œė²„ź°€ ķ“ė¼ģ“ģ–øķŠøź°€ ģš”ģ²­ķ•œ `resource`에 ķ† ķ°ģ„ 묶얓 ė°œźø‰ķ•œė‹¤ė©“ ģ“ ģ˜µģ…˜ģ„ ģ¼œģ„øģš”. MCP ķ“ė¼ģ“ģ–øķŠøėŠ” ģ“ ź°’ģ„ ķ•­ģƒ ė³“ėƒ…ė‹ˆė‹¤. `resource_server_url`ģ€ ķ“ė¼ģ“ģ–øķŠøź°€ ģ‹¤ģ œė”œ ģ—°ź²°ķ•˜ėŠ” URLź³¼ ģ •ķ™•ķžˆ ź°™ź²Œ ģœ ģ§€ķ•˜ģ„øģš”. + * ģøź°€ ģ„œė²„ź°€ ģžģ²“ audience ģ‹ė³„ģž(Auth0 API ģ‹ė³„ģž, Entra ģ• ķ”Œė¦¬ģ¼€ģ“ģ…˜ ID)넼 ģ‚¬ģš©ķ•œė‹¤ė©“ ģ“ ģ˜µģ…˜ģ„ 끄고, ėŒ€ģ‹  ź²€ģ¦źø°ģ—ģ„œ `aud`넼 ķ™•ģøķ•“ ģ“ ģ„œė²„ė„¼ ģœ„ķ•œ ķ† ķ°ģ“ ģ•„ė‹ˆė©“ `None`ģ„ ė°˜ķ™˜ķ•˜ģ„øģš”. + * `aud` ź°’ģ“ ė¦¬ģŠ¤ķŠøė¼ė©“ `resource_server_url`ź³¼ ź°™ģ€ ķ•­ėŖ©ģ„ `resource`에 ė„£ģœ¼ģ„øģš”. !!! tip SDK ģ €ģž„ģ†Œģ˜ `examples/servers/simple-auth/`ģ—ėŠ” ģ‹¤ģ œ ģøź°€ ģ„œė²„ģ˜ @@ -91,7 +95,7 @@ SDKėŠ” ģœ ķšØķ•œ ķ† ķ°ģ“ ģ–“ė–¤ ėŖØģŠµģøģ§€ģ— ėŒ€ķ•“ ģ•„ė¬“ėŸ° ģ˜ź²¬ģ“ ģ—† ģ–“ė–¤ ķ•øė“¤ėŸ¬ ģ•ˆģ—ģ„œė“  **`get_access_token()`**ģ€ ķ˜„ģž¬ ģš”ģ²­ģ— ėŒ€ķ•“ ź²€ģ¦źø°ź°€ ė°˜ķ™˜ķ•œ `AccessToken`ģž…ė‹ˆė‹¤. -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ SDKėŠ” ė¦¬ģ†ŒģŠ¤ ģ„œė²„ ģŖ½ ģ ˆė°˜ģ„ ģ œź³µķ•©ė‹ˆė‹¤. ź²€ģ¦ķ•˜ź³ , ģ•Œė¦¬ź³ , * `token_verifier=`와 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])`ėŠ” ķ•­ģƒ ķ•Øź»˜ ė‹¤ė‹™ė‹ˆė‹¤. * SDKėŠ” `/.well-known/oauth-protected-resource/...`에 [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata넼 ź²Œģ‹œķ•˜ź³ , ģøģ¦ė˜ģ§€ ģ•Šģ€ ģš”ģ²­ģ—ėŠ” ģ“ ė¬øģ„œė„¼ ź°€ė¦¬ķ‚¤ėŠ” `WWW-Authenticate` ķ—¤ė”ź°€ ė‹“źø“ 401딜 ģ‘ė‹µķ•©ė‹ˆė‹¤. ė””ģŠ¤ģ»¤ė²„ė¦¬ėŠ” ģ“ź²ƒģ“ ģ „ė¶€ģž…ė‹ˆė‹¤. * ģ–“ė–¤ ķ•øė“¤ėŸ¬ģ—ģ„œė“  `get_access_token()`ģ“ ź³§ ķ˜øģ¶œģžģž…ė‹ˆė‹¤. -* ģøź°€ėŠ” HTTPģ˜ ź“€ģ‹¬ģ‚¬ģž…ė‹ˆė‹¤. `stdio`와 ģøė©”ėŖØė¦¬ ķ“ė¼ģ“ģ–øķŠøģ—ģ„œėŠ” ģøź°€ź°€ ģ „ķ˜€ ė³“ģ“ģ§€ ģ•ŠģŠµė‹ˆė‹¤. +* ģøź°€ėŠ” HTTPģ˜ ź“€ģ‹¬ģ‚¬ģž…ė‹ˆė‹¤. `stdio`와 ģøė©”ėŖØė¦¬ ķ…ŒģŠ¤ķŠø ķ“ė¼ģ“ģ–øķŠøģ—ģ„œėŠ” ģøź°€ź°€ ģ „ķ˜€ ė³“ģ“ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠø ģŖ½ 절반(ģøź°€ ģ„œė²„ė„¼ 찾아낓고 ķ† ķ°ģ„ ėŒ€ģ‹  ź°€ģ øģ˜¤ėŠ” ģ¼)ģ€ **[OAuth ķ“ė¼ģ“ģ–øķŠø](../client/oauth-clients.md)**ģ—ģ„œ ķ™•ģøķ•˜ģ„øģš”. 그리고 ģ‚¬ģš©ģžģ—ź²Œ ģ‹ ģ›ģ„ ė¬»ėŠ” ėŒ€ģ‹  ģ‹ ģ›ģ„ **ģ–“ģ„¤ģ…˜**ķ•˜ėŠ” ķ“ė¼ģ“ģ–øķŠøėŠ” **[ID ģ–“ģ„¤ģ…˜](../client/identity-assertion.md)**ģ—ģ„œ ķ™•ģøķ•˜ģ„øģš”. diff --git a/i18n/ko/pages/run/deploy.md b/i18n/ko/pages/run/deploy.md index 8c3384ba..c994ca1f 100644 --- a/i18n/ko/pages/run/deploy.md +++ b/i18n/ko/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # ė°°ķ¬ģ™€ ķ™•ģž„ {#deploy-scale} @@ -46,6 +46,22 @@ translation: 경고 ķ•œ ģ¤„ė”œė§Œ ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. 새딜 ė°°ķ¬ķ•œ ģ„œė²„ź°€ ėŖØė“  ģ—°ź²°ģ„ ź±°ė¶€ķ•œė‹¤ė©“, 달리 ė°ķ˜€ģ§€źø° ģ „ź¹Œģ§€ėŠ” Host ķ—ˆģš© ėŖ©ė” ė¬øģ œģž…ė‹ˆė‹¤. **[문제 ķ•“ź²°](../troubleshooting.md)**ė„ ģ—¬źø°ģ„œ ģ‹œģž‘ķ•©ė‹ˆė‹¤. +## TLS ģ¢…ė£Œ ķ”„ė”ģ‹œ ė’¤ģ—ģ„œ {#behind-a-tls-terminating-proxy} + +TLSź°€ ķ”„ė”ģ‹œ(ģøź·øė ˆģŠ¤, ė”œė“œ ė°øėŸ°ģ„œ, Caddy, nginx)ģ—ģ„œ ėė‚˜ź³  ź·ø ė’¤ģ—ģ„œ uvicornģ“ ķ‰ė¬ø HTTP넼 ģ„œė¹„ģŠ¤ķ•œė‹¤ė©“, uvicornģ“ ķ”„ė”ģ‹œģ˜ `X-Forwarded-*` ķ—¤ė”ė„¼ ģ‹ ė¢°ķ•˜ė„ė” ģ„¤ģ •ķ•˜ģ„øģš”. + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +ģ“ ģ„¤ģ •ģ“ ģ—†ģœ¼ė©“ ģ•±ģ€ ģžģ‹ ģ“ `http://`딜 ģ„œė¹„ģŠ¤ė˜ź³  ģžˆė‹¤ź³  믿고, ģ•±ģ“ ė‚“ė³“ė‚“ėŠ” ėŖØė“  ė¦¬ė””ė ‰ģ…˜(ķ”ķ•œ ź²ƒģ€ `/mcp` → `/mcp/`)ģ“ `http://…`넼 ź°€ė¦¬ķ‚µė‹ˆė‹¤. Python ķ“ė¼ģ“ģ–øķŠøėŠ” HTTPS ģ—”ė“œķ¬ģøķŠøģ—ģ„œ ķ‰ė¬ø HTTP딜 ė”°ė¼ź°€źø°ė„¼ ź±°ė¶€ķ•˜ė©° ź·ø ģ“ģœ ė„¼ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +ķ“ė¼ģ“ģ–øķŠø ģŖ½ ģž„ģ‹œė°©ķŽøģ€ ģ„œė²„ź°€ ģ„œė¹„ģŠ¤ķ•˜ėŠ” ģ •ķ™•ķ•œ URL(`https://mcp.example.com/mcp/`, ģŠ¬ėž˜ģ‹œ ķ¬ķ•Ø)ģ„ 설정핓 ė¦¬ė””ė ‰ģ…˜ģ“ ģ¼ģ–“ė‚˜ģ§€ ģ•Šź²Œ ķ•˜ėŠ” ź²ƒģž…ė‹ˆė‹¤. ģ œėŒ€ė”œ 된 ķ•“ź²°ģ±…ģ€ ģœ„ģ˜ ķ”Œėž˜ź·øģž…ė‹ˆė‹¤. `FORWARDED_ALLOW_IPS`ėŠ” ź°™ģ€ ģ„¤ģ •ģ˜ ķ™˜ź²½ ė³€ģˆ˜ ģ“ė¦„ģ“ė©°, `*` ź°’ģ€ ėŖØė“  ķ™‰ģ„ ģ‹ ė¢°ķ•˜ėÆ€ė”œ ķ”„ė”ģ‹œ ė§ź³ ėŠ” ģ•„ė¬“ź²ƒė„ uvicorn에 ė‹æģ„ 수 ģ—†ģ„ ė•Œė§Œ ė§žėŠ” ģ„¤ģ •ģž…ė‹ˆė‹¤. + ## ģ›Œģ»¤, 그리고 ģŠ¤ķ‹°ķ‚¤ź°€ ķ•„ģš”ķ•œ ģŖ½ {#workers-and-who-has-to-be-sticky} ķ˜øģŠ¤ķŠøėŖ…ģ“ ģ‘ė‹µķ•˜źø° ģ‹œģž‘ķ–ˆė‹¤ė©“ ź·ø 뒤에 ģ›Œģ»¤ė„¼ ė‘˜ ģ“ģƒ ė‘ģ„øģš”. ģ“ė„¼ ģœ„ķ•œ SDK ģ„¤ģ •ģ€ ģ—†ģŠµė‹ˆė‹¤. Starlette ģ•±ģ€ 다넸 ASGI 앱과 ė˜‘ź°™ģ“, ķ¬ķ¬ķ•  줄 ģ•„ėŠ” ė„źµ¬ģ— ź°ģ²“ė„¼ ė„˜ź²Øģ„œ ķ™•ģž„ķ•©ė‹ˆė‹¤. @@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## ģš”ģ•½ {#recap} * 기본적으딜 ģ“ ģ•±ģ€ localhost딜 ģ˜¤ėŠ” ģš”ģ²­ė§Œ ė°›ģŠµė‹ˆė‹¤. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])`ź°€ ģ„œė¹„ģŠ¤ ź°œģ‹œģ˜ ź“€ė¬øģž…ė‹ˆė‹¤. ģ“ė„¼ ģ „ė‹¬ķ•˜źø° ģ „ź¹Œģ§€ ģ‹¤ģ œ ķ˜øģŠ¤ķŠøėŖ… ė’¤ģ˜ ėŖØė“  ģš”ģ²­ģ€ `421`ģ“ 되며, ź·ø ģ“ģœ ėŠ” ģ„œė²„ ė”œź·øģ—ė§Œ ė‚ØģŠµė‹ˆė‹¤. +* TLS ģ¢…ė£Œ ķ”„ė”ģ‹œ ė’¤ģ—ģ„œėŠ” uvicornģ„ `--proxy-headers --forwarded-allow-ips=...` ģ˜µģ…˜ģœ¼ė”œ ģ‹¤ķ–‰ķ•˜ģ„øģš”. ź·øėŸ¬ģ§€ ģ•Šģœ¼ė©“ ė¦¬ė””ė ‰ģ…˜ģ“ `http://`넼 가리키고 ķ“ė¼ģ“ģ–øķŠøź°€ ģ“ė„¼ ź±°ė¶€ķ•©ė‹ˆė‹¤. * 2026-07-28ģ—ėŠ” ģ„øģ…˜ģ“ 없고, ė”œė“œ ė°øėŸ°ģ„œź°€ ģŠ¤ķ‹°ķ‚¤ė”œ 붙들 ėŒ€ģƒė„ ģ—†ģŠµė‹ˆė‹¤. `stateless_http=True`ėŠ” ė ˆź±°ģ‹œ ģ „ģš© ģ„¤ģ •ģž…ė‹ˆė‹¤. ģµœģ‹  ģš”ģ²­ģ€ ģ“ ķ”Œėž˜ź·øė„¼ ģ½źø°ė„ 전에 ė¼ģš°ķŒ…ė˜ź³  ģ‘ė‹µė˜źø° ė•Œė¬øģž…ė‹ˆė‹¤. * źø°ė³ø `requestState` ķ‚¤ėŠ” ķ”„ė”œģ„øģŠ¤ė§ˆė‹¤ ė§Œė“¤ģ–“ģ§€ėŠ” `os.urandom(32)`ģž…ė‹ˆė‹¤. 다넸 ģ›Œģ»¤ģ— ė„ė‹¬ķ•œ 다중 왕복 ģž¬ģ‹œė„ėŠ” `-32602` *"Invalid or expired requestState"*딜 ģ‹¤ķŒØķ•©ė‹ˆė‹¤. * ķ•“ź²°ģ±…ģ€ `RequestStateSecurity(keys=[...])`넼 ģ“°ėŠ” 것, **그리고** ėŖØė“  ģøģŠ¤ķ„“ģŠ¤ģ— ź°™ģ€ ģ„œė²„ ģ“ė¦„ģ„ ģ“°ėŠ” ź²ƒģž…ė‹ˆė‹¤. ģ“ė¦„ģ€ ķ† ķ°ģ˜ źø°ė³ø audience ķ“ė ˆģž„ģž…ė‹ˆė‹¤. ź°™ģ€ 키, ź°™ģ€ ģ“ė¦„. diff --git a/i18n/ko/pages/run/index.md b/i18n/ko/pages/run/index.md index a2a69a98..32590892 100644 --- a/i18n/ko/pages/run/index.md +++ b/i18n/ko/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # ģ„œė²„ ģ‹¤ķ–‰ķ•˜źø° {#running-your-server} @@ -75,6 +75,11 @@ InspectorėŠ” ģ‹¤ģ œ ķ˜øģŠ¤ķŠøź°€ ķ•˜ėŠ” ģ¼ģ„ ź·øėŒ€ė”œ ķ•©ė‹ˆė‹¤. `server.py` * `max_request_body_size`: ķ—ˆģš©ė˜ėŠ” ģš”ģ²­ ė³øė¬øģ˜ ģµœėŒ€ 크기(ė°”ģ“ķŠø). źø°ė³øź°’ģ€ 4MiBģ“ė©°, ė” 큰 ģš”ģ²­ģ€ ķŒŒģ‹±ģ“ė‚˜ ģ„øģ…˜ ģƒģ„± 전에 HTTP 413ģ„ ė°›ģŠµė‹ˆė‹¤. ģ •ģƒģ ģø MCP ė©”ģ‹œģ§€ź°€ ģ“ 크기넼 ė„˜ģ„ ė•Œė§Œ ģ˜¬ė¦¬ģ„øģš”. +* `session_idle_timeout`: ģ§„ķ–‰ ģ¤‘ģø ź²ƒģ“ ģ•„ė¬“ź²ƒė„ ģ—†ėŠ” 상태딜 ė ˆź±°ģ‹œ ģ„øģ…˜ģ“ 머물 수 ģžˆėŠ” ģ‹œź°„(쓈)ģ“ė©°, ģ“ ģ‹œź°„ģ“ ģ§€ė‚˜ė©“ + ģ„œė²„ź°€ ģ„øģ…˜ģ„ ė‹«ģŠµė‹ˆė‹¤. źø°ė³øź°’ģ€ 1800ģž…ė‹ˆė‹¤. `None`ģ“ė©“ ė¹„ķ™œģ„±ķ™”ė©ė‹ˆė‹¤. + [ģ„øģ…˜ ģˆ˜ėŖ…ź³¼ ģ œķ•œ](legacy-clients.md#session-lifetime-and-limits)ģ„ ģ°øź³ ķ•˜ģ„øģš”. +* `max_sessions`: ķ•œ ķ”„ė”œģ„øģŠ¤ź°€ ė™ģ‹œģ— ģœ ģ§€ķ•˜ėŠ” ė ˆź±°ģ‹œ ģ„øģ…˜ģ˜ 수. źø°ė³øź°’ģ€ 10 000ģž…ė‹ˆė‹¤. `None`ģ“ė©“ + ģ œķ•œģ“ ģ—†ģ–“ģ§‘ė‹ˆė‹¤. ź°™ģ€ ģ„¹ģ…˜ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. * `event_store`, `retry_interval`, `transport_security`: ģž¬ź°œ ź°€ėŠ„ģ„±ź³¼ DNS ė¦¬ė°”ģøė”© 볓호. localhostź°€ ģ•„ė‹Œ 곳에 ė°°ķ¬ķ•˜źø° ģ „ź¹Œģ§€ėŠ” ėÆøė¤„ė„ ė©ė‹ˆė‹¤. `transport_security`ėŠ” **[ė°°ķ¬ģ™€ ķ™•ģž„](deploy.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. !!! warning diff --git a/i18n/ko/pages/run/legacy-clients.md b/i18n/ko/pages/run/legacy-clients.md index b7624924..6ec6aafc 100644 --- a/i18n/ko/pages/run/legacy-clients.md +++ b/i18n/ko/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠø 지원 {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDKėŠ” ėŖØė“  ģš”ģ²­ģ„ `MCP-Protocol-Version` ķ—¤ė”ģ— ė”°ė¼ ė¼ģš°ķŒ…ķ•©ė‹ˆ ## ķ•˜ė‚˜ģ˜ ķ•øė“¤ėŸ¬, 두 ģ‹œėŒ€ {#one-handler-both-eras} -ė‹¤ģŒģ€ ģ‚¬ģš©ģžģ—ź²Œ 묓언가넼 물얓듐야 ķ•˜ėŠ” ė„źµ¬ģ™€, ź·ø ė„źµ¬ė„¼ ķ˜øģ¶œķ•˜ėŠ” 두 ģ‹œėŒ€ģ˜ ķ“ė¼ģ“ģ–øķŠøģž…ė‹ˆė‹¤. +ė‹¤ģŒģ€ ģ‚¬ģš©ģžģ—ź²Œ 묓언가넼 물얓듐야 ķ•˜ėŠ” ė„źµ¬ģž…ė‹ˆė‹¤. -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve`ģ—ėŠ” ėŖØėøģ“ ģ œź³µķ•˜ģ§€ ģ•Šģ€ 정볓가 ķ•˜ė‚˜ ķ•„ģš”ķ•©ė‹ˆė‹¤. ėŖ‡ ź¶Œģøģ§€ģž…ė‹ˆė‹¤. `Annotated[..., Resolve(ask_quantity)]`ėŠ” ė„źµ¬ź°€ ģ“ė„¼ ģ„ ģ–øķ•˜ėŠ” ė°©ė²•ģž…ė‹ˆė‹¤(ģžģ„øķ•œ ė‚“ģš©ģ€ **[ģ˜ģ”“ģ„±](../handlers/dependencies.md)**ģ—ģ„œ ķ™•ģøķ•˜ģ„øģš”). `reserve` ģ•ˆģ—ėŠ” ė²„ģ „ģ„ ėŖ…ģ‹œķ•˜ź±°ė‚˜, źø°ėŠ„ģ„ ķ™•ģøķ•˜ź±°ė‚˜, ė¶„źø°ķ•˜ėŠ” ģ½”ė“œź°€ ģ „ķ˜€ ģ—†ģŠµė‹ˆė‹¤. -두 ķ“ė¼ģ“ģ–øķŠøėŠ” ź°™ģ€ `mcp` ź°ģ²“ģ— **ė™ģ‹œģ—** ģ—“ė ¤ ģžˆģŠµė‹ˆė‹¤. `mode="legacy"`ėŠ” `initialize` ķ•øė“œģ…°ģ“ķ¬ė„¼ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. 2026 ģ“ģ „ ķ“ė¼ģ“ģ–øķŠøź°€ ģ—¬ėŠ” ė°”ė”œ ź·ø ģ—°ź²°ģž…ė‹ˆė‹¤. 다넸 ķ•˜ė‚˜ėŠ” źø°ė³øź°’ģ„ ģ‚¬ģš©ķ•“ `2026-07-28` ė²„ģ „ģœ¼ė”œ ģ—°ź²°ė©ė‹ˆė‹¤. +HTTP딜 ė„ģš°ģ„øģš”. ė‹¤ģŒģ€ ģ“ ė„źµ¬ė„¼ ķ˜øģ¶œķ•˜ėŠ” 두 ģ‹œėŒ€ģ˜ ķ“ė¼ģ“ģ–øķŠøģž…ė‹ˆė‹¤. + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +두 ķ“ė¼ģ“ģ–øķŠøėŠ” 실행 ģ¤‘ģø ź°™ģ€ ģ„œė²„ė„¼ ģƒėŒ€ė”œ **ė™ģ‹œģ—** ģ—“ė ¤ ģžˆģŠµė‹ˆė‹¤. `mode="legacy"`ėŠ” `initialize` ķ•øė“œģ…°ģ“ķ¬ė„¼ ģ‹¤ķ–‰ķ•©ė‹ˆė‹¤. 2026 ģ“ģ „ ķ“ė¼ģ“ģ–øķŠøź°€ ģ—¬ėŠ” ė°”ė”œ ź·ø ģ—°ź²°ģž…ė‹ˆė‹¤. 다넸 ķ•˜ė‚˜ėŠ” źø°ė³øź°’ģ„ ģ‚¬ģš©ķ•“ `2026-07-28` ė²„ģ „ģœ¼ė”œ ģ—°ź²°ė©ė‹ˆė‹¤. 두 번째 ķ„°ėÆøė„ģ—ģ„œ `python client.py`넼 ģ‹¤ķ–‰ķ•˜ģ„øģš”. ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,31 @@ SDKėŠ” ėŖØė“  ģš”ģ²­ģ„ `MCP-Protocol-Version` ķ—¤ė”ģ— ė”°ė¼ ė¼ģš°ķŒ…ķ•©ė‹ˆ ģ„øģ…˜ģ— ė‹¤ģ‹œ ģ—°ź²°ķ•˜ėŠ” ķ“ė¼ģ“ģ–øķŠøģ—ź²Œ ė†“ģ¹œ SSE ģ“ė²¤ķŠøė„¼ ģž¬ģƒķ•˜ėŠ” 것)ģž…ė‹ˆė‹¤. 다넸 ķ”„ė”œģ„øģŠ¤ģ—ģ„œ ģ„øģ…˜ģ— ė„ė‹¬ķ•  수 ģžˆź²Œ ķ•“ ģ£¼ėŠ” ģ¼ģ€ ź²°ģ½” ģ—†ģŠµė‹ˆė‹¤. +## ģ„øģ…˜ ģˆ˜ėŖ…ź³¼ ģ œķ•œ {#session-lifetime-and-limits} + +ė ˆź±°ģ‹œ ģ„øģ…˜ģ€ ģ˜ģ›ķžˆ ģ‚“ģ§€ ģ•Šģœ¼ė©°, ķ•œ ķ”„ė”œģ„øģŠ¤ź°€ ė¬“ķ•œķžˆ ė§Žģ€ ģ„øģ…˜ģ„ ė³“ģœ ķ•˜ģ§€ė„ ģ•ŠģŠµė‹ˆė‹¤. ģ“ė„¼ ģ œģ–“ķ•˜ėŠ” ģ„¤ģ •ģ“ 두 가지 ģžˆģŠµė‹ˆė‹¤. ė‘˜ 다 `run()`, `streamable_http_app()`, `Server.streamable_http_app()`ģ˜ ķ‚¤ģ›Œė“œ ģøģžģž…ė‹ˆė‹¤. ķ˜„ėŒ€(`2026-07-28`) ģ—°ź²°ź³¼ `stateless_http=True`ģ—ėŠ” ģ„øģ…˜ģ“ ģ—†ģœ¼ėÆ€ė”œ 두 설정 모두 ģ ģš©ė˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. + +| 설정 | źø°ė³øź°’ | ķ•˜ėŠ” ģ¼ | ķ“ė¼ģ“ģ–øķŠøź°€ ė³“ėŠ” 것 | ė„ėŠ” 방법 | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30ė¶„) | ź·ø ģ‹œź°„ ė™ģ•ˆ ģ§„ķ–‰ ģ¤‘ģø ź²ƒģ“ ģ•„ė¬“ź²ƒė„ ģ—†ģ—ˆė˜ ģ„øģ…˜ģ„ ė‹«ģŠµė‹ˆė‹¤. | `404 Session not found`. ė‹¤ģ‹œ `initialize`핓야 ķ•©ė‹ˆė‹¤. | `None` | +| `max_sessions` | `10_000` | ź·ø 수넼 ė„˜ėŠ” ģ„øģ…˜ģ€ 엓기넼 ź±°ė¶€ķ•©ė‹ˆė‹¤. 기씓 ģ„øģ…˜ģ€ ź±“ė“œė¦¬ģ§€ ģ•Šģœ¼ė©° ģ•„ė¬“ź²ƒė„ ģ«“ģ•„ė‚“ģ§€ ģ•ŠģŠµė‹ˆė‹¤. | JSON-RPC ģ½”ė“œ `-32603`ź³¼ ķ•Øź»˜ `503 Too many open sessions`. | `None` | + +"ģ§„ķ–‰ 중"으딜 ź°„ģ£¼ė˜ėŠ” ź²ƒģ€ ė‹¤ģŒź³¼ ź°™ģŠµė‹ˆė‹¤. + +* 엓린 `GET` 스트림. SDK ķ“ė¼ģ“ģ–øķŠøėŠ” ķ•˜ė‚˜ė„¼ ģ—“ģ–“ ė‘ėÆ€ė”œ, ģ—°ź²°ėœ ķ“ė¼ģ“ģ–øķŠøģ˜ ģ„øģ…˜ģ€ ė§Œė£Œė˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. +* 아직 ģ‘ė‹µ ģ¤‘ģø ģš”ģ²­. ķƒ€ģž„ģ•„ģ›ƒė³“ė‹¤ ģ˜¤ėž˜ ģ‹¤ķ–‰ė˜ėŠ” ė„źµ¬ ķ˜øģ¶œģ€ ģ¤‘ė‹Øė˜ģ§€ ģ•Šģœ¼ė©°, ģ¹“ģš“ķŠøė‹¤ģš“ģ€ ķ˜øģ¶œģ“ ėė‚œ 뒤에야 ģ‹œģž‘ė©ė‹ˆė‹¤. +* ź·ø ė°–ģ—ėŠ” ģ—†ģŠµė‹ˆė‹¤. ģš”ģ²­ ģ‚¬ģ“ģ—ėŠ” ģ‹œź³„ź°€ ėŒģ•„ź°‘ė‹ˆė‹¤. ģ„øģ…˜ģ— ė“¤ģ–“ģ˜¤ėŠ” ģš”ģ²­ģ€ ė¬“ģ—‡ģ“ė“  ģ‹œź³„ė„¼ ė‹¤ģ‹œ ģ‹œģž‘ķ•˜ė©°, `ping`ė„ ķ¬ķ•Øė©ė‹ˆė‹¤. ģ„øģ…˜ģ“ ķ•œ 번 만료되멓 ė¬“ģ—‡ģœ¼ė”œė„ ė˜ģ‚“ė¦“ 수 ģ—†ģŠµė‹ˆė‹¤. + +`DELETE`딜 ģ„øģ…˜ģ„ ėė‚“ėŠ” ķ“ė¼ģ“ģ–øķŠøėŠ” ģ„øģ…˜ģ„ ģ¦‰ģ‹œ ķ•“ģ œķ•©ė‹ˆė‹¤. ģ—¬ėŠ” ģš”ģ²­ģ“ ź±°ė¶€ėœ ķ“ė¼ģ“ģ–øķŠøė„ ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +두 ģ“ė²¤ķŠø 모두 ģ„œė²„ ė”œź·øģ— ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. ė§Œė£ŒėŠ” `INFO` ģˆ˜ģ¤€ģ˜ `Session idle timeout`ģž…ė‹ˆė‹¤. ģ—“źø° ź±°ė¶€ėŠ” `WARNING` ģˆ˜ģ¤€ģ˜ `Refusing to open a new session: sessions are already open`ģž…ė‹ˆė‹¤. + +ģ œķ•œģ€ ķ”„ė”œģ„øģŠ¤ė³„ģž…ė‹ˆė‹¤. ģ›Œģ»¤ź°€ ė„·ģ“ė©“ ģƒķ•œģ€ `max_sessions`ģ˜ 네 ė°°ģ“ė©°, 각 ģ›Œģ»¤ėŠ” ģžźø° ģ„øģ…˜ģ„ 스스딜 ė§Œė£Œģ‹œķ‚µė‹ˆė‹¤. + ## ģœ ģ¼ķ•œ ģ˜µģ…˜: `stateless_http` {#the-one-knob-stateless_http} ģŠ¤ķ‹°ķ‚¤ ė¼ģš°ķŒ…ģ“ 치넓기 ģ‹«ģ€ ė¹„ģš©ģ“ė¼ė©“, 바꿀 수 ģžˆėŠ” ź²ƒģ€ ģ •ķ™•ķžˆ ķ•˜ė‚˜ģž…ė‹ˆė‹¤. @@ -85,7 +120,7 @@ SDKėŠ” ėŖØė“  ģš”ģ²­ģ„ `MCP-Protocol-Version` ķ—¤ė”ģ— ė”°ė¼ ė¼ģš°ķŒ…ķ•©ė‹ˆ !!! check ģ¼ė¶€ėŸ¬ ģž˜ėŖ»ėœ ģ„¤ģ •ģ„ ķ•“ ė³“ģ„øģš”. `reserve`ėŠ” 방금 두 ķ“ė¼ģ“ģ–øķŠøė„¼ 모두 ģ§€ģ›ķ•œ ė°”ė”œ ź·ø ė„źµ¬ģž…ė‹ˆė‹¤. - `stateless_http=True`딜 ė°°ķ¬ķ•˜ź³ , ź°™ģ€ 두 ķ“ė¼ģ“ģ–øķŠøė„¼ HTTP딜 ģ—°ź²°ķ•œ ė’¤, ź°ź°ģ—ģ„œ ķ˜øģ¶œķ•“ ė³“ģ„øģš”. + `stateless_http=True`딜 ė°°ķ¬ķ•˜ź³ , ź°™ģ€ 두 ķ“ė¼ģ“ģ–øķŠøė„¼ ģ—°ź²°ķ•œ ė’¤, ź°ź°ģ—ģ„œ ķ˜øģ¶œķ•“ ė³“ģ„øģš”. ķ˜„ėŒ€ ķ“ė¼ģ“ģ–øķŠøėŠ” ģ—¬ģ „ķžˆ `Reserved 2 of 'Dune'.`ģ„ ė°›ģŠµė‹ˆė‹¤. ķ˜„ėŒ€ ź²½ė”œėŠ” ė°”ė€Œģ§€ ģ•Šģ•˜ģŠµė‹ˆė‹¤. diff --git a/i18n/ko/pages/troubleshooting.md b/i18n/ko/pages/troubleshooting.md index caaf97c4..7a7bed2c 100644 --- a/i18n/ko/pages/troubleshooting.md +++ b/i18n/ko/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # 문제 ķ•“ź²° {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +핓당 ķ•­ėŖ©ģ€ `http://localhost:8000/mcp`딜 ģ“ ģ„œė²„ģ— ģ ‘ģ†ķ•˜ėÆ€ė”œ, HTTP딜 ģ‹¤ķ–‰ķ•œ ģ±„ė”œ ė‘ģ„øģš”. + +```console +uv run mcp run server.py --transport streamable-http +``` + ģ“ ķŽ˜ģ“ģ§€ģ—ģ„œ ģøģš©ķ•˜ėŠ” ģ˜¤ė„˜ėŠ” 모두 ģ‹¤ģ œ ģ˜¤ė„˜ģž…ė‹ˆė‹¤. SDK ģžģ²“ģ˜ ķ…ŒģŠ¤ķŠø ģŠ¤ģœ„ķŠøź°€ ķ•˜ė‚˜ķ•˜ė‚˜ ģ „ė¶€ ģž¬ķ˜„ķ•©ė‹ˆė‹¤. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -42,14 +48,14 @@ async def main() -> None: +------------------------------------ ``` -ģ“ģ— ėŒ€ķ•“ ķ•  ģ¼ģ€ 두 ź°€ģ§€ģž…ė‹ˆė‹¤. +ģ—¬źø°ģ„œ ķ•  ģ¼ģ€ 두 ź°€ģ§€ģž…ė‹ˆė‹¤. 1. **ė§Ø ģ•„ėž˜ė„¼ ģ½ģœ¼ģ„øģš”.** `MCPError: No forecast for 'Atlantis'.`ź°€ ģ‹¤ķŒØģ˜ ģ›ģøģž…ė‹ˆė‹¤. ģ“ ķŽ˜ģ“ģ§€ģ—ģ„œ 찾아야 ķ•  ź²ƒģ€ ė°”ė”œ **ģ“** ķ…ģŠ¤ķŠøģž…ė‹ˆė‹¤. 2. **ėø”ė” ģ•ˆģ—ģ„œ ģž”ģœ¼ģ„øģš”.** `ExceptionGroup`ģ€ ģ˜ˆģ™øź°€ `async with`넼 **ė²—ģ–“ė‚ ** ė•Œė§Œ ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. ģ•ˆģ—ģ„œ ģž”ģœ¼ė©“ ź°™ģ€ ģ‹¤ķŒØź°€ 그룹 ģ—†ģ“ ķ‰ė²”ķ•œ `MCPError`딜 ė‚˜ķƒ€ė‚©ė‹ˆė‹¤. ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -ģ„œė²„ź°€ ķ“ė¼ģ“ģ–øķŠøź°€ 볓낸 `Mcp-Session-Id`넼 ģ•Œģ•„ė³“ģ§€ ėŖ»ķ•˜ėŠ” ź²½ģš°ģ“ė©°, ź±°ģ˜ ģ–øģ œė‚˜ ģ„œė²„ź°€ **ģž¬ģ‹œģž‘**ė˜ģ—ˆźø°(ė˜ėŠ” 다넸 ģøģŠ¤ķ„“ģŠ¤ė”œ ė¼ģš°ķŒ…ė˜ģ—ˆźø°) ė•Œė¬øģž…ė‹ˆė‹¤. ģ„øģ…˜ģ€ 핓당 ķ”„ė”œģ„øģŠ¤ ķ•˜ė‚˜ģ˜ ė©”ėŖØė¦¬ģ—ė§Œ ģ”“ģž¬ķ•©ė‹ˆė‹¤. +ģ„œė²„ź°€ ķ“ė¼ģ“ģ–øķŠøź°€ 볓낸 `Mcp-Session-Id`넼 ģ•Œģ•„ė³“ģ§€ ėŖ»ķ•˜ėŠ” ź²½ģš°ģž…ė‹ˆė‹¤. ģ„œė²„ź°€ **ģž¬ģ‹œģž‘**ė˜ģ—ˆź±°ė‚˜(ė˜ėŠ” 다넸 ģøģŠ¤ķ„“ģŠ¤ė”œ ė¼ģš°ķŒ…ė˜ģ—ˆź±°ė‚˜), `session_idle_timeout` ė™ģ•ˆ ģ§„ķ–‰ ģ¤‘ģø ģš”ģ²­ģ“ ģ—†ģ–“ģ„œ ģ„øģ…˜ģ“ **만료**된 ź²ƒģž…ė‹ˆė‹¤. ģ“ ź°’ģ€ 기본적으딜 30ė¶„ģž…ė‹ˆė‹¤. [ģ„øģ…˜ ģˆ˜ėŖ…ź³¼ ģ œķ•œ](run/legacy-clients.md#session-lifetime-and-limits)ģ„ ģ°øź³ ķ•˜ģ„øģš”. ģ„øģ…˜ģ€ 핓당 ķ”„ė”œģ„øģŠ¤ ķ•˜ė‚˜ģ˜ ė©”ėŖØė¦¬ģ—ė§Œ ģ”“ģž¬ķ•©ė‹ˆė‹¤. 찾아야 ķ•  ģ„œė²„ ė²„ź·øėŠ” ģ—†ģŠµė‹ˆė‹¤. HTTP ģ‘ė‹µģ€ ė³øė¬øģ“ **ģ‹¤ģ œė”œ** JSON-RPCģø `404`ģ“ėÆ€ė”œ, ģœ„ģ˜ `421`ź³¼ 달리 Python `Client`ź°€ ģ“ė²ˆģ—ėŠ” ź·øėŒ€ė”œ 볓여 ģ¤ė‹ˆė‹¤. @@ -261,9 +267,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ķ•“ź²°ģ±…ģ€ ė‹¤ģ‹œ ģ—°ź²°ķ•˜ėŠ” ź²ƒģž…ė‹ˆė‹¤. `async with Client(...)` ėø”ė”ģ„ ė²—ģ–“ė‚˜ 새 ėø”ė”ģ— ģ§„ģž…ķ•˜ė©“ 새 ģ„øģ…˜ģ„ ķ˜‘ģƒķ•©ė‹ˆė‹¤. ģ˜¤ėž˜ ģ‹¤ķ–‰ė˜ėŠ” ķ“ė¼ģ“ģ–øķŠøė¼ė©“, ķ˜øģ¶œģ„ 감싸 `MCPError`넼 ģž”ģ€ ė’¤ ģ“ ė©”ģ‹œģ§€ź°€ ė‚˜ģ˜¤ė©“ ģ£½ģ€ ģ„øģ…˜ ģ•ˆģ—ģ„œ ģž¬ģ‹œė„ķ•˜ģ§€ 말고 ė‹¤ģ‹œ 연결핓야 ķ•œė‹¤ėŠ” ėœ»ģž…ė‹ˆė‹¤. -ģž¬ģ‹œģž‘ **ģ—†ģ“** ģ“ ė¬øģ œź°€ ė°œģƒķ•œė‹¤ė©“, ģŠ¤ķ‹°ķ‚¤ ģ„øģ…˜ ģ—†ģ“ ģ›Œģ»¤ė„¼ ė‘˜ ģ“ģƒ ģ‹¤ķ–‰ķ•˜ź³  ģžˆėŠ” ź²ƒģž…ė‹ˆė‹¤. 각 ģ›Œģ»¤ź°€ ģžźø°ė§Œģ˜ ģ„øģ…˜ ķ…Œģ“ėø”ģ„ ź°€ģ§€ėÆ€ė”œ, ģ—‰ėš±ķ•œ ģ›Œģ»¤ė”œ ė¼ģš°ķŒ…ėœ ģš”ģ²­ģ“ 여기에 ė„ė‹¬ķ•©ė‹ˆė‹¤. ģ“ ģ“ģ•¼źø°ģ™€ 두 가지 ķ•“ź²°ģ±…(ģŠ¤ķ‹°ķ‚¤ ė¼ģš°ķŒ… ė˜ėŠ” `stateless_http=True`)ģ€ **[ė°°ķ¬ģ™€ ķ™•ģž„](run/deploy.md)**ź³¼ **[ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠø 지원](run/legacy-clients.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. +ģž¬ģ‹œģž‘ė„ **ģ—†ģ—ˆź³ ** ķ“ė¼ģ“ģ–øķŠøź°€ ź·øė ‡ź²Œ ģ˜¤ėž˜ ģ”°ģš©ķžˆ ģžˆģ§€ė„ ģ•Šģ•˜ėŠ”ė° ģ“ ė¬øģ œź°€ ė°œģƒķ•œė‹¤ė©“, ģŠ¤ķ‹°ķ‚¤ ģ„øģ…˜ ģ—†ģ“ ģ›Œģ»¤ė„¼ ė‘˜ ģ“ģƒ ģ‹¤ķ–‰ķ•˜ź³  ģžˆėŠ” ź²ƒģž…ė‹ˆė‹¤. 각 ģ›Œģ»¤ź°€ ģžźø°ė§Œģ˜ ģ„øģ…˜ ķ…Œģ“ėø”ģ„ ź°€ģ§€ėÆ€ė”œ, ģ—‰ėš±ķ•œ ģ›Œģ»¤ė”œ ė¼ģš°ķŒ…ėœ ģš”ģ²­ģ“ 여기에 ė„ė‹¬ķ•©ė‹ˆė‹¤. ģ“ ģ“ģ•¼źø°ģ™€ 두 가지 ķ•“ź²°ģ±…(ģŠ¤ķ‹°ķ‚¤ ė¼ģš°ķŒ… ė˜ėŠ” `stateless_http=True`)ģ€ **[ė°°ķ¬ģ™€ ķ™•ģž„](run/deploy.md)**ź³¼ **[ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠø 지원](run/legacy-clients.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. -ģ„œė²„ ģš“ģ˜ģž ģŖ½ģ—ģ„œ ėŒ€ģ‘ķ•˜ėŠ” 딜그 ģ¤„ģ€ `Rejected request with unknown or expired session ID: `ģž…ė‹ˆė‹¤. `INFO` ģˆ˜ģ¤€ģœ¼ė”œ źø°ė”ė˜ėÆ€ė”œ ģ¼ė°˜ģ ģø `WARNING` ģž„ź³„ź°’ģ—ģ„œėŠ” ė³“ģ“ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ė°°ķ¬ 직후 ģ“ ģ¤„ģ“ ķ•œźŗ¼ė²ˆģ— ģŸģ•„ģ§€ėŠ” ź²ƒģ€ ģ •ģƒģž…ė‹ˆė‹¤. ģ—°ź²°ė˜ģ–“ ģžˆė˜ ėŖØė“  ķ“ė¼ģ“ģ–øķŠøź°€ ė‹¤ģ‹œ ģ—°ź²°ķ•˜ėŠ” ģ¤‘ģ“źø° ė•Œė¬øģž…ė‹ˆė‹¤. +ģ„œė²„ ģš“ģ˜ģž ģŖ½ģ—ģ„œ ėŒ€ģ‘ķ•˜ėŠ” 딜그 ģ¤„ģ€ `Rejected request with unknown or expired session ID: `ģž…ė‹ˆė‹¤. `INFO` ģˆ˜ģ¤€ģœ¼ė”œ źø°ė”ė˜ėÆ€ė”œ ģ¼ė°˜ģ ģø `WARNING` ģž„ź³„ź°’ģ—ģ„œėŠ” ė³“ģ“ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ė°°ķ¬ 직후 ģ“ ģ¤„ģ“ ķ•œźŗ¼ė²ˆģ— ģŸģ•„ģ§€ėŠ” ź²ƒģ€ ģ •ģƒģž…ė‹ˆė‹¤. ģ—°ź²°ė˜ģ–“ ģžˆė˜ ėŖØė“  ķ“ė¼ģ“ģ–øķŠøź°€ ė‹¤ģ‹œ ģ—°ź²°ķ•˜ėŠ” ģ¤‘ģ“źø° ė•Œė¬øģž…ė‹ˆė‹¤. ģ„øģ…˜ģ“ 만료된 ź²½ģš°ė¼ė©“ ź·ø 줄 ģ•žģ— `Session idle timeout`ģ“ 먼저 ģ°ķžˆė©°, ģ—­ģ‹œ `INFO` ģˆ˜ģ¤€ģž…ė‹ˆė‹¤. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ģ„œė²„ź°€ ģ‚¬ģš©ģžģ—ź²Œ 묓언가넼 물얓볓려 ķ•˜ėŠ”ė°, ģ“ ķ“ė¼ģ“ģ–øķŠøź°€ 물얓볼 수 ģžˆė‹¤ź³  ė°ķžŒ ģ ģ“ ģ—†ėŠ” ź²½ģš°ģž…ė‹ˆė‹¤. -ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜(elicitation) ė¦¬ģ”øė²„ėŠ” ģ—°ź²°ėœ ķ“ė¼ģ“ģ–øķŠøź°€ ķ¼ ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜ģ„ ģ„ ģ–øķ•˜ģ§€ ģ•Šģ•˜ģœ¼ė©“ ģ²˜ģŒė¶€ķ„° ź±°ė¶€ķ•˜ė©°, `e.error.data`ź°€ ģ •ķ™•ķžˆ ė¬“ģ—‡ģ“ ė¹ ģ”ŒėŠ”ģ§€ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. +ģ“ Bistro ģ„œė²„ėŠ” ģ˜ˆģ•½ķ•˜źø° 전에 리씸버넼 통핓 먼저 ė¬¼ģ–“ė“…ė‹ˆė‹¤. + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Weather ģ„œė²„ ėŒ€ģ‹  ģ“ ģ„œė²„ė„¼ ė„ģš°ź³ , `elicitation_callback`ģ„ ģ „ė‹¬ķ•˜ģ§€ ģ•Šģ€ ķ“ė¼ģ“ģ–øķŠøģ—ģ„œ `book_table`ģ„ ķ˜øģ¶œķ•˜ģ„øģš”. ģ—°ź²°ėœ ķ“ė¼ģ“ģ–øķŠøź°€ ķ¼ ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜(elicitation)ģ„ ģ„ ģ–øķ•œ ģ ģ“ ģ—†ģœ¼ėÆ€ė”œ ė¦¬ģ”øė²„ėŠ” ģ²˜ģŒė¶€ķ„° ź±°ė¶€ķ•˜ė©°, `e.error.data`ź°€ ģ •ķ™•ķžˆ ė¬“ģ—‡ģ“ ė¹ ģ”ŒėŠ”ģ§€ ģ•Œė ¤ ģ¤ė‹ˆė‹¤. ```json { @@ -289,7 +301,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: ķ•øė“¤ėŸ¬ź°€ ģš”ģ²­ ė„ģ¤‘ģ— ķ“ė¼ģ“ģ–øķŠøģ— ģ†ģ„ ė»—ģœ¼ė ¤ ķ–ˆėŠ”ė°, ź·ø ķ˜øģ¶œģ— ģ„œė²„ģ˜ ģš”ģ²­ģ„ 실얓 ė‚˜ė„¼ ģ±„ė„ģ“ ģ—†ėŠ” ģ—°ź²°ģ“ģ—ˆė˜ ź²½ģš°ģž…ė‹ˆė‹¤. ķ˜øģ¶œģ„ ģ“ėŸ° ģƒķ™©ģ— ė†“ėŠ” ģ„œė²„ źµ¬ģ„±ģ€ 세 ź°€ģ§€ģž…ė‹ˆė‹¤. -**`2026-07-28` ģ—°ź²°. ķŠøėžœģŠ¤ķ¬ķŠøģ™€ ė¬“ź“€ķ•˜ź²Œ ķ•­ģƒ.** ģµœģ‹  ķ”„ė”œķ† ģ½œģ—ėŠ” ģ„œė²„ź°€ ģ‹œģž‘ķ•˜ėŠ” ģš”ģ²­ģ“ ģ•„ģ˜ˆ ģ—†ģœ¼ėÆ€ė”œ, ģ„œė²„ėŠ” ė¬“ģ—‡ģ„ ė³“ė‚“źø°ė„ 전에 ź±°ė¶€ķ•©ė‹ˆė‹¤. ė„źµ¬ ģ•ˆģ—ģ„œ `ctx.elicit()`ģ„ ķ˜øģ¶œķ•˜ėŠ” ź²ƒģ“ ģ“ 오넘넼 ė§Œė‚˜ėŠ” ģ „ķ˜•ģ ģø źøøģ“ė©°(`Client(server)`ėŠ” ė”°ė”œ ģš”ģ²­ķ•˜ģ§€ ģ•Šģ•„ė„ `2026-07-28`ģ„ ķ˜‘ģƒķ•˜ėÆ€ė”œ, 첫 ģøė©”ėŖØė¦¬ ķ…ŒģŠ¤ķŠøģ—ģ„œ ė°”ė”œ ė§Œė‚©ė‹ˆė‹¤), `elicitation_callback=` ģøģžė„¼ ģ „ė‹¬ķ•“ė„ ė‹¬ė¼ģ§€ėŠ” ź²ƒģ€ ģ—†ģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ 답할 ģš”ģ²­ ģžģ²“ź°€ ė„ė‹¬ķ•˜ģ§€ ģ•Šźø° ė•Œė¬øģž…ė‹ˆė‹¤. +**`2026-07-28` ģ—°ź²°. ķŠøėžœģŠ¤ķ¬ķŠøģ™€ ė¬“ź“€ķ•˜ź²Œ ķ•­ģƒ.** ģµœģ‹  ķ”„ė”œķ† ģ½œģ—ėŠ” ģ„œė²„ź°€ ģ‹œģž‘ķ•˜ėŠ” ģš”ģ²­ģ“ ģ•„ģ˜ˆ ģ—†ģœ¼ėÆ€ė”œ, ģ„œė²„ėŠ” ė¬“ģ—‡ģ„ ė³“ė‚“źø°ė„ 전에 ź±°ė¶€ķ•©ė‹ˆė‹¤. ė„źµ¬ ģ•ˆģ—ģ„œ `ctx.elicit()`ģ„ ķ˜øģ¶œķ•˜ėŠ” ź²ƒģ“ ģ“ 오넘넼 ė§Œė‚˜ėŠ” ģ „ķ˜•ģ ģø źøøģ“ė©°, `Client(mcp)`ėŠ” ė”°ė”œ ģš”ģ²­ķ•˜ģ§€ ģ•Šģ•„ė„ `2026-07-28`ģ„ ķ˜‘ģƒķ•˜ėÆ€ė”œ ėŒ€ź°œ ź·ø ė„źµ¬ģ˜ 첫 ģøė©”ėŖØė¦¬ **[ķ…ŒģŠ¤ķŠø](get-started/testing.md)**ģ—ģ„œ ė°”ė”œ ė§Œė‚©ė‹ˆė‹¤. `elicitation_callback=` ģøģžė„¼ ģ „ė‹¬ķ•“ė„ ė‹¬ė¼ģ§€ėŠ” ź²ƒģ€ ģ—†ģŠµė‹ˆė‹¤. ķ“ė¼ģ“ģ–øķŠøź°€ 답할 ģš”ģ²­ ģžģ²“ź°€ ė„ė‹¬ķ•˜ģ§€ ģ•Šźø° ė•Œė¬øģž…ė‹ˆė‹¤. ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * ģ„œė²„ ė”œź·øģ˜ `Tool already exists:`ėŠ” ģ“ė¦„ģ“ ź°™ģ€ 두 ė„źµ¬ź°€ ķ•˜ė‚˜ė”œ ķ•©ģ³ģ”Œė‹¤ėŠ” ģœ ģ¼ķ•œ ģ‹ ķ˜øģž…ė‹ˆė‹¤. * 421 ķ•˜ė‚˜ģ— ķ‘œźø°ėŠ” 세 ź°€ģ§€ģž…ė‹ˆė‹¤. `Server returned an error response`(Python `Client`), `421 Misdirected Request` / `Invalid Host header`(ź·ø ė°–ģ˜ ėŖØė“  ź³³), `Invalid Host header: `(ģ„œė²„ 딜그). ķ•“ź²°ģ±…ģ€ `transport_security=TransportSecuritySettings(allowed_hosts=[...])`ģž…ė‹ˆė‹¤. * `Task group is not initialized` -> 마욓트된 ģ•±ģ—ģ„œ 호스트 lifespanģ“ `mcp.session_manager.run()`에 ģ§„ģž…ķ•˜ģ§€ ģ•Šģ€ ź²½ģš°ģž…ė‹ˆė‹¤. -* `Session not found` -> ģ„œė²„ź°€ ģž¬ģ‹œģž‘ė˜ģ—ˆģŠµė‹ˆė‹¤. ė‹¤ģ‹œ ģ—°ź²°ķ•˜ģ„øģš”. +* `Session not found` -> ģ„œė²„ź°€ ģž¬ģ‹œģž‘ė˜ģ—ˆź±°ė‚˜ ģ„øģ…˜ģ“ ė§Œė£Œė˜ģ—ˆģŠµė‹ˆė‹¤(`session_idle_timeout`). ė‹¤ģ‹œ ģ—°ź²°ķ•˜ģ„øģš”. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()`ģ—ėŠ” ģ„œė²„ģ—ģ„œ ķ“ė¼ģ“ģ–øķŠøė”œ ź°€ėŠ” ģ±„ė„ģ“ ķ•„ģš”ķ•©ė‹ˆė‹¤. `2026-07-28` ģ—°ź²°ģ—ėŠ” 그런 ģ±„ė„ģ“ ģ•„ģ˜ˆ 없고, `stateless_http=True`ėŠ” ė ˆź±°ģ‹œ ģ±„ė„ģ„ 없애며, `json_response=True`ėŠ” ģš”ģ²­ ė²”ģœ„ ģ±„ė„ģ„ ģ—†ģ•±ė‹ˆė‹¤. 리씸버넼 ģ‚¬ģš©ķ•˜ģ„øģš”(ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠøė¼ė©“ ģ±„ė„ģ„ ģœ ģ§€ķ•˜ėŠ” ģ„œė²„ė„ ķ•„ģš”ķ•©ė‹ˆė‹¤). ģ“ģ›ƒģø `Method not found`ėŠ” ģƒėŒ€ķŽø ķ”„ė”œķ† ģ½œ 리비전에 ģ—†ėŠ” ė©”ģ„œė“œė„¼ ģš”ģ²­ķ•œ ź²½ģš°ģž…ė‹ˆė‹¤. * `Client did not declare the form elicitation capability ...` ė° `Elicitation not supported` -> ķ“ė¼ģ“ģ–øķŠøģ— `elicitation_callback=` ģøģžź°€ ė¹ ģ ø ģžˆģŠµė‹ˆė‹¤. * `Invalid or expired requestState`ėŠ” ģ™€ģ“ģ–“ģ—ģ„œ ģ“ģœ ė„¼ ģ ˆėŒ€ ė§ķ•˜ģ§€ ģ•ŠģŠµė‹ˆė‹¤. ģ„œė²„ ė”œź·øź°€ 말핓 주며, `unknown key`ėŠ” ģ›Œģ»¤ 간에 `RequestStateSecurity(keys=[...])` ģ„¤ģ •ģ„ ź³µģœ ķ•˜ė¼ėŠ” ėœ»ģž…ė‹ˆė‹¤. diff --git a/i18n/ko/pages/whats-new.md b/i18n/ko/pages/whats-new.md index 84077c3f..6c50e98d 100644 --- a/i18n/ko/pages/whats-new.md +++ b/i18n/ko/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2ģ—ģ„œ ė‹¬ė¼ģ§„ 점 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1ģ€ 세 겹으딜 ģ¤‘ģ²©ėœ ź³„ģøµģ„ ź±“ė„¤ģ£¼ģ—ˆģŠµė‹ˆė‹¤. ģ›ģ‹œ ģŠ¤ķŠøė¦¼ģ„ ė‚“ė†“ėŠ” ķŠøėžœģŠ¤ķ¬ķŠø ģ»Øķ…ģŠ¤ķŠø ė§¤ė‹ˆģ €, ģ“ė„¼ ź°ģ‹øėŠ” `ClientSession`, 그리고 직접 ķ˜øģ¶œķ•“ģ•¼ ķ•˜ėŠ” `await session.initialize()`ģž…ė‹ˆė‹¤. v2ģ—ėŠ” ź°ģ²“ź°€ ķ•˜ė‚˜ėæģž…ė‹ˆė‹¤. -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client`ėŠ” ģ„œė²„ ź°ģ²“(ķŠøėžœģŠ¤ķ¬ķŠø ģ—†ģ“ ģøė©”ėŖØė¦¬ė”œ ė™ģž‘ķ•˜ė©°, ķ…ŒģŠ¤ķŠøģ— ģ“°ėŠ” ė°©ģ‹ģž…ė‹ˆė‹¤), URL(Streamable HTTP), `StdioServerParameters`(stdio ķ•˜ģœ„ ķ”„ė”œģ„øģŠ¤), ė˜ėŠ” `sse_client(...)` ź°™ģ€ ź·ø ė°–ģ˜ ģž„ģ˜ģ˜ ķŠøėžœģŠ¤ķ¬ķŠø ģ»Øķ…ģŠ¤ķŠø ė§¤ė‹ˆģ €ė„¼ ė°›ģŠµė‹ˆė‹¤. `async with`에 ģ§„ģž…ķ•˜ė©“ ģ„œė²„ź°€ ģ–“ėŠ ģ‹œėŒ€ģ˜ ķ”„ė”œķ† ģ½œģ„ ė§ķ•˜ė“  ģ—°ź²°ģ„ ė§ŗź³  ķ”„ė”œķ† ģ½œ ė²„ģ „ģ„ ķ˜‘ģƒķ•©ė‹ˆė‹¤. ź·ø ė’¤ģ—ėŠ” `client.server_capabilities`와 `client.protocol_version`ģ“ 그냄 ģ¤€ė¹„ė˜ģ–“ ģžˆź³ , ģ„œė²„ź°€ ģžģ‹ ģ„ ģ‹ė³„ķ•˜ėŠ” ź²½ģš°ģ—ėŠ” `client.server_info`ė„ ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤(2026 ģ‹œėŒ€ģ—ėŠ” ģ‹ė³„ 정볓가 ģ„ ķƒ ģ‚¬ķ•­ģ“ėÆ€ė”œ ģ“ģ œ ķƒ€ģž…ģ€ `Implementation | None`ģž…ė‹ˆė‹¤). v1ģ—ģ„œ ė“±ė”ķ•œ ģƒ˜ķ”Œė§ ė° ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜ ģ½œė°±ģ€ ģ—¬ģ „ķžˆ ė™ģž‘ķ•˜ė©°(콜백 ė³øė¬øģ—ėŠ” ģ“ ķŽ˜ģ“ģ§€ģ˜ 다넸 ėŖØė“  것과 ė§ˆģ°¬ź°€ģ§€ė”œ snake_case ģ†ģ„± ģ“ė¦„ ė³€ź²½ģ“ ģ ģš©ė©ė‹ˆė‹¤), ģ“ģ œ 2026 ė°©ģ‹ģ˜ ź²°ź³¼ ģ† ģš”ģ²­(ģ•„ėž˜ ģ°øź³ )ģ—ė„ ģ‘ė‹µķ•˜ź³ , ķ•œ ė²ˆģ— ķ•˜ė‚˜ģ”©ģ“ ģ•„ė‹ˆė¼ ė™ģ‹œģ— ģ‹¤ķ–‰ė©ė‹ˆė‹¤. ģ €ģˆ˜ģ¤€ ģøķ„°ķŽ˜ģ“ģŠ¤ė„¼ ģ›ķ•˜ėŠ” 경우넼 ģœ„ķ•“ `ClientSession`ģ€ ģ—¬ģ „ķžˆ ź·ø ģ•„ėž˜ģ— ģžˆģœ¼ė©° `client.session`으딜 ģ–»ģ„ 수 ģžˆģŠµė‹ˆė‹¤. ė‹¤ė§Œ ģ“ ķ“ėž˜ģŠ¤ ģ—­ģ‹œ ė°”ė€Œģ—ˆģœ¼ėÆ€ė”œ(새 ė””ģŠ¤ķŒØģ²˜ 엔진 ģœ„ģ—ģ„œ ģ‹¤ķ–‰ė˜ź³ , ģžģ²“ ģ‹œź·øė‹ˆģ²˜ ģ¼ė¶€ė„ ė³€ź²½ė˜ģ—ˆģŠµė‹ˆė‹¤) ģ•„ėž˜ ź³„ģøµģœ¼ė”œ 낓려가기 전에 **[ė§ˆģ“ź·øė ˆģ“ģ…˜ ź°€ģ“ė“œ](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**넼 ģ½ģ–“ ė³“ģ„øģš”. +`Client`ėŠ” URL(Streamable HTTP), `StdioServerParameters`(stdio ķ•˜ģœ„ ķ”„ė”œģ„øģŠ¤), `sse_client(...)` ź°™ģ€ ź·ø ė°–ģ˜ ģž„ģ˜ģ˜ ķŠøėžœģŠ¤ķ¬ķŠø ģ»Øķ…ģŠ¤ķŠø ė§¤ė‹ˆģ €, ė˜ėŠ” ķ…ŒģŠ¤ķŠøģ—ģ„œėŠ” ģ„œė²„ ź°ģ²“ ģžģ²“(ķŠøėžœģŠ¤ķ¬ķŠø ģ—†ģ“ ģøė©”ėŖØė¦¬ė”œ ė™ģž‘ķ•©ė‹ˆė‹¤)넼 ė°›ģŠµė‹ˆė‹¤. `async with`에 ģ§„ģž…ķ•˜ė©“ ģ„œė²„ź°€ ģ–“ėŠ ģ‹œėŒ€ģ˜ ķ”„ė”œķ† ģ½œģ„ ė§ķ•˜ė“  ģ—°ź²°ģ„ ė§ŗź³  ķ”„ė”œķ† ģ½œ ė²„ģ „ģ„ ķ˜‘ģƒķ•©ė‹ˆė‹¤. ź·ø ė’¤ģ—ėŠ” `client.server_capabilities`와 `client.protocol_version`ģ“ 그냄 ģ¤€ė¹„ė˜ģ–“ ģžˆź³ , ģ„œė²„ź°€ ģžģ‹ ģ„ ģ‹ė³„ķ•˜ėŠ” ź²½ģš°ģ—ėŠ” `client.server_info`ė„ ė§ˆģ°¬ź°€ģ§€ģž…ė‹ˆė‹¤(2026 ģ‹œėŒ€ģ—ėŠ” ģ‹ė³„ 정볓가 ģ„ ķƒ ģ‚¬ķ•­ģ“ėÆ€ė”œ ģ“ģ œ ķƒ€ģž…ģ€ `Implementation | None`ģž…ė‹ˆė‹¤). v1ģ—ģ„œ ė“±ė”ķ•œ ģƒ˜ķ”Œė§ ė° ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜ ģ½œė°±ģ€ ģ—¬ģ „ķžˆ ė™ģž‘ķ•˜ė©°(콜백 ė³øė¬øģ—ėŠ” ģ“ ķŽ˜ģ“ģ§€ģ˜ 다넸 ėŖØė“  것과 ė§ˆģ°¬ź°€ģ§€ė”œ snake_case ģ†ģ„± ģ“ė¦„ ė³€ź²½ģ“ ģ ģš©ė©ė‹ˆė‹¤), ģ“ģ œ 2026 ė°©ģ‹ģ˜ ź²°ź³¼ ģ† ģš”ģ²­(ģ•„ėž˜ ģ°øź³ )ģ—ė„ ģ‘ė‹µķ•˜ź³ , ķ•œ ė²ˆģ— ķ•˜ė‚˜ģ”©ģ“ ģ•„ė‹ˆė¼ ė™ģ‹œģ— ģ‹¤ķ–‰ė©ė‹ˆė‹¤. ģ €ģˆ˜ģ¤€ ģøķ„°ķŽ˜ģ“ģŠ¤ė„¼ ģ›ķ•˜ėŠ” 경우넼 ģœ„ķ•“ `ClientSession`ģ€ ģ—¬ģ „ķžˆ ź·ø ģ•„ėž˜ģ— ģžˆģœ¼ė©° `client.session`으딜 ģ–»ģ„ 수 ģžˆģŠµė‹ˆė‹¤. ė‹¤ė§Œ ģ“ ķ“ėž˜ģŠ¤ ģ—­ģ‹œ ė°”ė€Œģ—ˆģœ¼ėÆ€ė”œ(새 ė””ģŠ¤ķŒØģ²˜ 엔진 ģœ„ģ—ģ„œ ģ‹¤ķ–‰ė˜ź³ , ģžģ²“ ģ‹œź·øė‹ˆģ²˜ ģ¼ė¶€ė„ ė³€ź²½ė˜ģ—ˆģŠµė‹ˆė‹¤) ģ•„ėž˜ ź³„ģøµģœ¼ė”œ 낓려가기 전에 **[ė§ˆģ“ź·øė ˆģ“ģ…˜ ź°€ģ“ė“œ](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**넼 ģ½ģ–“ ė³“ģ„øģš”. **[ķ“ė¼ģ“ģ–øķŠø](client/index.md)**ģ—ģ„œ ģ†Œź°œķ•˜ź³ , **[ķ“ė¼ģ“ģ–øķŠø ķŠøėžœģŠ¤ķ¬ķŠø](client/transports.md)**ģ—ģ„œ 네 가지 ģ—°ź²° ķ˜•ķƒœė„¼, **[ķ“ė¼ģ“ģ–øķŠø 콜백](client/callbacks.md)**ģ—ģ„œ 콜백 ģžģ²“ė„¼ 다루며, **[ķ…ŒģŠ¤ķŠø](get-started/testing.md)**ģ—ģ„œėŠ” v1ģ˜ `create_connected_server_and_client_session()` ķ—¬ķ¼ė„¼ ėŒ€ģ²“ķ•˜ėŠ” ģøė©”ėŖØė¦¬ ķŒØķ„“ģ„ 볓여 ģ¤ė‹ˆė‹¤. @@ -171,11 +171,15 @@ Streamable HTTPģ—ģ„œėŠ” 2026 ź²½ė”œģ— `Mcp-Session-Id`ź°€ ģ—†ģœ¼ė©°, 욓영 ėŒ€ģ²“ ė°©ģ‹ģ€ ķ˜øģ¶œģ˜ ė°©ķ–„ģ„ ė’¤ģ§‘ģŠµė‹ˆė‹¤. ģ‚¬ģš©ģžė”œė¶€ķ„° 묓언가가 ķ•„ģš”ķ•œ ė„źµ¬ėŠ” ģ§ˆė¬øģ„ **ė°˜ķ™˜**ķ•˜ź³ (`InputRequiredResult`), ķ“ė¼ģ“ģ–øķŠøėŠ” 늘 ź°–ź³  ģžˆė˜ 것과 ź°™ģ€ 콜백으딜 ģ§ˆė¬øģ— ė‹µķ•˜ė©°, ė‹µģ“ ģ²Øė¶€ėœ ģ±„ė”œ ķ˜øģ¶œģ“ ģž¬ģ‹œė„ė©ė‹ˆė‹¤. ģ“ ė£Øķ”„ėŠ” `Client`ź°€ ėŒ€ģ‹  ėŒė ¤ ģ¤ė‹ˆė‹¤. ģ„œė²„ģ—ģ„œ 결과넼 직접 ė§Œė“œėŠ” ģ¼ģ€ ė“œė­…ė‹ˆė‹¤. **[ģ˜ģ”“ģ„±](handlers/dependencies.md)**ģ“ ėŒ€ģ‹  ķ•“ 주기 ė•Œė¬øģž…ė‹ˆė‹¤. ė§¤ź°œė³€ģˆ˜ģ— `Resolve(ask_quantity)` ģ–“ė…øķ…Œģ“ģ…˜ģ„ 달멓(`ask_quantity`ėŠ” 직접 ģž‘ģ„±ķ•˜ėŠ” ķ‰ė²”ķ•œ ķ•Øģˆ˜ģž…ė‹ˆė‹¤), SDKź°€ ģ—°ź²°ģ“ ģ§€ģ›ķ•˜ėŠ” ė©”ģ»¤ė‹ˆģ¦˜, 즉 ė ˆź±°ģ‹œ ģ„øģ…˜ģ—ģ„œėŠ” ģ‹¤ģ‹œź°„ ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜ ģš”ģ²­, 2026ģ—ģ„œėŠ” 다중 ģ™•ė³µģ„ 통핓 ģ§ˆė¬øķ•©ė‹ˆė‹¤. ė„źµ¬ 본문 ķ•˜ė‚˜ė”œ 두 ģ‹œėŒ€ė„¼ ģ§€ģ›ķ•©ė‹ˆė‹¤. -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -ģ“ ķŒŒģ¼ ķ•˜ė‚˜ģ— ķ•µģ‹¬ģ“ 모두 닓겨 ģžˆģŠµė‹ˆė‹¤. ģ„œė²„ ķ•˜ė‚˜, `Resolve` 기반 ė„źµ¬ ķ•˜ė‚˜, 그리고 ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠøģ™€ ģµœģ‹  ķ“ė¼ģ“ģ–øķŠøź°€ 모두 ģøė©”ėŖØė¦¬ė”œ ė‹µģ„ ė°›ģŠµė‹ˆė‹¤. ė©”ģ»¤ė‹ˆģ¦˜(SDKź°€ ėŒ€ģ‹  ė“‰ģøķ•˜ź³  ź²€ģ¦ķ•˜ėŠ” `request_state` ķ¬ķ•Ø)ģ€ **[다중 왕복 ģš”ģ²­](handlers/multi-round-trip.md)**ģ—ģ„œ ģ„¤ėŖ…ķ•˜ź³ , ģ§ˆė¬øķ•˜ėŠ” ģŖ½ģ€ **[ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜](handlers/elicitation.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ģ“ 두 ķŒŒģ¼ģ— ķ•µģ‹¬ģ“ 모두 닓겨 ģžˆģŠµė‹ˆė‹¤. ģ„œė²„ ķ•˜ė‚˜, `Resolve` 기반 ė„źµ¬ ķ•˜ė‚˜, 그리고 ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠøģ™€ ģµœģ‹  ķ“ė¼ģ“ģ–øķŠøź°€ 모두 실행 ģ¤‘ģø ź°™ģ€ ģ„œė²„ģ—ģ„œ ė‹µģ„ ė°›ģŠµė‹ˆė‹¤(**[ė ˆź±°ģ‹œ ķ“ė¼ģ“ģ–øķŠø 지원](run/legacy-clients.md)**ģ—ģ„œ ģ“ 두 ķŒŒģ¼ģ„ ķ•˜ė‚˜ģ”© ģ§šģ–“ ė“…ė‹ˆė‹¤). ė©”ģ»¤ė‹ˆģ¦˜(SDKź°€ ėŒ€ģ‹  ė“‰ģøķ•˜ź³  ź²€ģ¦ķ•˜ėŠ” `request_state` ķ¬ķ•Ø)ģ€ **[다중 왕복 ģš”ģ²­](handlers/multi-round-trip.md)**ģ—ģ„œ ģ„¤ėŖ…ķ•˜ź³ , ģ§ˆė¬øķ•˜ėŠ” ģŖ½ģ€ **[ģ—˜ė¦¬ģ‹œķ…Œģ“ģ…˜](handlers/elicitation.md)**ģ—ģ„œ ė‹¤ė£¹ė‹ˆė‹¤. !!! warning "ķ¬ķŒ…ķ•œ v1 ģ„œė²„ģ˜ ė™ģž‘ģ“ ė°”ė€ŒėŠ” ģœ ģ¼ķ•œ ģ§€ģ ģž…ė‹ˆė‹¤" ź°€ģž„ 먼저 ė¶€ė”ŖķžˆėŠ” ź²ƒģ€ 직접 ģž‘ģ„±ķ•œ ķ…ŒģŠ¤ķŠøģž…ė‹ˆė‹¤. `Client(mcp)`ėŠ” 기본적으딜 v2 ģ„œė²„ģ™€ 2026-07-28ģ„ diff --git a/i18n/pt/pages/advanced/apps.md b/i18n/pt/pages/advanced/apps.md index 06a5c15d..49a41266 100644 --- a/i18n/pt/pages/advanced/apps.md +++ b/i18n/pt/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -25,7 +25,7 @@ e depois volte aqui. ## Um relógio com uma cara {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -56,15 +56,36 @@ O modelo lĆŖ `content`; o iframe Ć© para humanos. Um host com suporte a UI ainda o resultado em texto para o modelo, e um cliente só de texto recebe *apenas* isso. EntĆ£o o padrĆ£o canĆ“nico Ć© uma ferramenta, duas respostas. Olhe `get_time` de novo: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` `client_supports_apps(ctx)` Ć© `True` somente quando o cliente declarou a extensĆ£o `io.modelcontextprotocol/ui` **e** listou `text/html;profile=mcp-app` nas suas configuraƧƵes `mimeTypes`. O campo Ć© obrigatório, entĆ£o um cliente que o omite -nĆ£o conta. Ɖ exatamente isso que `main()` no mesmo arquivo declara: a -metade cliente da negociação, e a resposta rica volta. +nĆ£o conta. Aqui estĆ” a metade cliente da negociação: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Sirva `server.py` por HTTP e depois execute o cliente em um segundo terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +A resposta rica voltou. Tire `extensions=[APPS_SUPPORT]` da chamada a `Client` +e o mesmo programa imprime `The time is 2026-06-26T12:00:00Z.` no lugar, que Ć© +tudo o que um cliente só de texto chega a ver. !!! warning Nunca retorne um placeholder como `"[Rendered UI]"` como Ćŗnico conteĆŗdo. Se o diff --git a/i18n/pt/pages/advanced/extensions.md b/i18n/pt/pages/advanced/extensions.md index c9ceb86f..51debad3 100644 --- a/i18n/pt/pages/advanced/extensions.md +++ b/i18n/pt/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # ExtensƵes {#extensions} @@ -49,7 +49,7 @@ Use como prefixo um domĆ­nio que vocĆŖ controla. `io.modelcontextprotocol/*` Ć© A menor extensĆ£o Ćŗtil Ć© uma ferramenta e um mapa de configuraƧƵes: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ A menor extensĆ£o Ćŗtil Ć© uma ferramenta e um mapa de configuraƧƵes: * `settings()` Ć© o valor anunciado em `capabilities.extensions["com.example/stamps"]`. Retorne `{}` (o padrĆ£o) para anunciar a extensĆ£o sem configuraƧƵes. * A extensĆ£o nunca recebe o servidor. Ela declara contribuiƧƵes como dados; o `MCPServer` as consome. NĆ£o existe um `self.server` para modificar. -E `main()` Ć© a prova, um cliente em memória direto contra `mcp`: +Sirva-a por HTTP, e um cliente Ć© a prova: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Todo `server.py` desta pĆ”gina Ć© servido com esse comando, e todo `client.py` roda ao lado dele com `python client.py` em um segundo terminal. + ### Servindo seus próprios mĆ©todos {#serving-your-own-methods} Uma extensĆ£o pode registrar **novos mĆ©todos de requisição**: seus próprios verbos, servidos ao lado dos da especificação: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ Os mĆ©todos sĆ£o **estritamente aditivos**. O SDK impƵe isso na construção, n ### O lado do cliente {#the-client-side} -O `main()` do mesmo arquivo Ć© a história inteira do cliente, com as duas metades: +O cliente Ć© um programa Ć  parte, e carrega as duas metades da história do cliente: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` declara a extensĆ£o. As declaraƧƵes viram `ClientCapabilities.extensions`: em uma conexĆ£o 2026-07-28 o mapa viaja no envelope `_meta` de cada requisição, entĆ£o o servidor o vĆŖ em **toda** requisição; em uma conexĆ£o legada ele vai no handshake `initialize`. O código do servidor nĆ£o se importa com qual: `require_client_extension(ctx, ...)` e `ctx.session.check_client_capability(...)` leem a fonte certa nos dois caminhos. * MĆ©todos de fornecedor descem uma camada para `client.session.send_request(...)`; `Client` só ganha mĆ©todos de primeira classe para verbos da especificação. `send_request` aceita qualquer subclasse de `Request`, entĆ£o a requisição do fornecedor passa como estĆ”. +* `SearchRequest` e os dois modelos que ela carrega sĆ£o o contrato de protocolo da extensĆ£o, entĆ£o o cliente os declara por conta própria. Uma extensĆ£o publicada os distribuiria em um pacote que os dois lados importam. ### Interceptando `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ O hook envolve `tools/call` e nada mais. Para preocupaƧƵes que valem para toda ## Usando uma extensĆ£o de cliente {#using-a-client-extension} -Uma **extensĆ£o de cliente** Ć© o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um Ćŗnico identificador. Passe as instĆ¢ncias para `Client(extensions=[...])` e chame as ferramentas normalmente: +Uma **extensĆ£o de cliente** Ć© o mesmo contrato visto do lado consumidor: um pacote de comportamento do lado do cliente reunido sob um Ćŗnico identificador. Aqui o servidor responde a `buy` com um recibo a resgatar em vez da mercadoria, e só para um cliente que declarou a extensĆ£o: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +No cliente, passe as instĆ¢ncias para `Client(extensions=[...])` e chame as ferramentas normalmente: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` retorna um `CallToolResult` comum, como toda outra chamada. O que a extensĆ£o mudou: o servidor agora pode responder a `buy` com um **formato de resultado** `receipt` em vez de um resultado final, e `Receipts` o finaliza (aqui, resgatando o recibo com uma chamada seguinte) antes de `call_tool` retornar. Nada muda no ponto da chamada. Tire a extensĆ£o e nada disso existe: a barreira do servidor recusa um cliente que nĆ£o a declarou (erro -32021), e um formato reivindicado vindo de um servidor que pula a barreira falha na validação, exatamente como a especificação exige para um `resultType` nĆ£o reconhecido. Desligado por padrĆ£o, nas duas pontas da conexĆ£o. @@ -124,15 +137,15 @@ Para anunciar um identificador **sem** nenhum comportamento do lado do cliente ( ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## Escrevendo uma extensĆ£o de cliente {#writing-a-client-extension} Herde de `ClientExtension` e sobrescreva apenas o que precisar. TrĆŖs tipos de contribuição, cada um com um padrĆ£o: `settings()`, `claims()` e `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * O identificador segue a mesma gramĆ”tica do servidor, validada quando a classe Ć© definida. @@ -153,12 +166,18 @@ Duas regras discretas. As claims ficam ativas apenas em conexƵes 2026-07-28, e ### Verbos de extensĆ£o {#extension-verbs} -Os mĆ©todos de requisição próprios de uma extensĆ£o nĆ£o precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios mĆ©todos](#serving-your-own-methods). Um acrĆ©scimo: quando uma chave de params precisa viajar no header `Mcp-Name` (especificaƧƵes de extensĆ£o como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`: +Os mĆ©todos de requisição próprios de uma extensĆ£o nĆ£o precisam de registro no lado do cliente. Um tipo de requisição de fornecedor herda de `mcp.types.Request` e passa por `client.session.send_request`, como em [Servindo seus próprios mĆ©todos](#serving-your-own-methods). Considere um servidor cuja extensĆ£o serve um Ćŗnico verbo sobre um job nomeado: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +Um acrĆ©scimo no cliente: quando uma chave de params precisa viajar no header `Mcp-Name` (especificaƧƵes de extensĆ£o como tasks exigem isso para seus verbos), o tipo de requisição declara `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + A sessĆ£o espelha `params["jobId"]` em `Mcp-Name` em todo caminho de envio, e um valor ausente falha de forma explĆ­cita em vez de omitir silenciosamente um header obrigatório. ## O que uma extensĆ£o nĆ£o pode fazer {#what-an-extension-cannot-do} diff --git a/i18n/pt/pages/advanced/low-level-server.md b/i18n/pt/pages/advanced/low-level-server.md index 9c9cd540..a75014bb 100644 --- a/i18n/pt/pages/advanced/low-level-server.md +++ b/i18n/pt/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # O Server de baixo nĆ­vel {#the-low-level-server} @@ -36,18 +36,22 @@ TrĆŖs coisas mudaram, e elas sĆ£o a API de baixo nĆ­vel inteira: ### Experimente {#try-it} -NĆ£o existe Inspector para este aqui: `mcp dev` e `mcp run` só aceitam um `MCPServer`. O `Client` em memória nĆ£o se importa; ele recebe um `Server` de baixo nĆ­vel exatamente como recebe um `MCPServer`: +`mcp dev` e `mcp run` só aceitam um `MCPServer`, entĆ£o este aqui vocĆŖ serve por conta própria. A Ćŗltima linha de `server.py` monta um app ASGI comum a partir dele, e o uvicorn o executa: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Aponte o Inspector, ou qualquer cliente, para `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ O mesmo texto que a versĆ£o com `@mcp.tool()` produziu. Duas diferenƧas honesta * `result.structured_content` Ć© `None`. O servidor de alto nĆ­vel encapsula um `-> str` em `{"result": ...}` para vocĆŖ; aqui ninguĆ©m monta o que vocĆŖ nĆ£o montou. * `list_tools` retorna o schema que **vocĆŖ** digitou, caractere por caractere. A versĆ£o de alto nĆ­vel tinha `"title": "Query"` em cada propriedade e um `"title": "search_booksArguments"` na raiz: artefatos do Pydantic. Aqui embaixo, se estĆ” no fio, foi vocĆŖ quem colocou lĆ”. +Em um teste vocĆŖ dispensa o uvicorn e a porta: `Client(server)` recebe um `Server` de baixo nĆ­vel no mesmo processo exatamente como recebe um `MCPServer`, e **[Testes](../get-started/testing.md)** Ć© esse padrĆ£o. + ## Nada Ć© verificado por vocĆŖ {#nothing-is-checked-for-you} O `MCPServer` rejeita um argumento ruim antes mesmo de a sua função executar, validando a chamada contra o schema que ele gerou (**[Ferramentas](../servers/tools.md)**). @@ -208,11 +214,11 @@ Cada um destes Ć© uma ideia para a qual vocĆŖ jĆ” tem o vocabulĆ”rio; cada um te ## Recapitulando {#recap} * O `Server` de baixo nĆ­vel recebe os seus handlers como **parĆ¢metros do construtor** `on_*`; todo handler Ć© `async (ctx, params) -> result`. -* VocĆŖ escreve o dict `input_schema` e vocĆŖ monta o `CallToolResult`. Nada Ć© derivado, encapsulado ou validado por vocĆŖ. +* VocĆŖ escreve o dict `input_schema` e vocĆŖ monta o `CallToolResult`. Nada Ć© derivado, encapsulado ou validado para vocĆŖ. * Uma exceção em um handler Ć© um erro de protocolo `-32603`. Um erro de ferramenta que o modelo consegue ler Ć© um `CallToolResult` com `is_error=True` que **vocĆŖ** retorna. * O `_meta` no resultado Ć© endereƧado Ć  aplicação cliente, nĆ£o ao modelo. * `Server[T]` Ć© genĆ©rico no que o seu lifespan produz; `ctx.lifespan_context` Ć© um `T` tipado. * `add_request_handler(method, params_type, handler)` serve qualquer mĆ©todo. `initialize` Ć© reservado. * As capacidades que um `Server` anuncia sĆ£o derivadas de quais handlers vocĆŖ registrou. -`Client(server)` tratou os dois servidores de forma idĆŖntica porque eles *sĆ£o* o mesmo protocolo, e essa Ć© justamente a ideia. A próxima camada abaixo nem Ć© uma classe: Ć© **[Middleware](middleware.md)**. +O cliente tratou os dois servidores de forma idĆŖntica porque eles *sĆ£o* o mesmo protocolo, e essa Ć© justamente a ideia. A próxima camada abaixo nem Ć© uma classe: Ć© **[Middleware](middleware.md)**. diff --git a/i18n/pt/pages/advanced/pagination.md b/i18n/pt/pages/advanced/pagination.md index e0ff9876..e1e1f777 100644 --- a/i18n/pt/pages/advanced/pagination.md +++ b/i18n/pt/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Paginação {#pagination} @@ -31,9 +31,13 @@ O `@mcp.resource()` nĆ£o tem nenhum gancho para isso. Para paginar, vocĆŖ mesmo ### Experimente {#try-it} -`Client(server)` se conecta a um `Server` de baixo nĆ­vel em memória exatamente como se conecta a um `MCPServer`. +O `mcp run` só aceita um `MCPServer`, entĆ£o este aqui vocĆŖ mesmo serve. A Ćŗltima linha de `server.py` monta um app ASGI comum a partir do `Server`, e o uvicorn executa esse app: -Chame `list_resources()` sem argumentos. VocĆŖ recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` Ć© a string `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Aponte qualquer cliente (**[O cliente](../client/index.md)**, ou o Inspector) para `http://localhost:8000/mcp` e chame `list_resources()` sem argumentos. VocĆŖ recebe dez recursos, de `book-1` a `book-10`, e `next_cursor` Ć© a string `"10"`. Devolva-a com `list_resources(cursor="10")` e o primeiro recurso Ć© `book-11`, o novo `next_cursor` Ć© `"20"`. @@ -43,7 +47,7 @@ A dĆ©cima pĆ”gina volta com `next_cursor` definido como `None`. Pronto. Todo mĆ©todo `list_*` do `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) aceita a palavra-chave `cursor=`. Esgotar uma lista paginada Ć© um Ćŗnico `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Todo mĆ©todo `list_*` do `Client` (`list_tools`, `list_resources`, `list_resourc * Acumule **antes** de olhar para `next_cursor`: a Ćŗltima pĆ”gina tambĆ©m tem recursos. * `next_cursor is None` Ć© a saĆ­da. Qualquer outra coisa volta direto para `cursor=`, intocada. -Execute o `main()` dele e ele imprime `100 resources`: dez pĆ”ginas de dez, costuradas por um loop que nunca soube que havia dez pĆ”ginas. +Com o uvicorn ainda servindo `server.py`, execute `python client.py` em um segundo terminal. Ele imprime `100 resources`: dez pĆ”ginas de dez, costuradas por um loop que nunca soube que havia dez pĆ”ginas. Este Ć© o mesmo loop que **[O cliente](../client/index.md)** mostra para todo verbo `list_*`, e ele nĆ£o custa nada contra um servidor que nĆ£o pagina: `next_cursor` Ć© `None` na primeira resposta e o loop roda uma vez. diff --git a/i18n/pt/pages/client/caching.md b/i18n/pt/pages/client/caching.md index e6cfdf83..73b242b3 100644 --- a/i18n/pt/pages/client/caching.md +++ b/i18n/pt/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Dicas de cache {#caching-hints} @@ -30,7 +30,7 @@ Por padrĆ£o, todo resultado diz `ttlMs: 0, cacheScope: "private"`: obsoleto imed No `Server` de baixo nĆ­vel, os handlers montam seus resultados Ć  mĆ£o, e `ttl_ms` / `cache_scope` sĆ£o apenas campos nos modelos de resultado. Um handler que os define explicitamente sempre vence o mapa do construtor, campo a campo: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Uma ressalva sobre listas paginadas: o protocolo exige o **mesmo `cacheScope` em Numa sessĆ£o 2026-07-28, o `Client` respeita as dicas por vocĆŖ: ele tem um cache de respostas embutido, ligado por padrĆ£o. Um resultado que chega carregando um `ttlMs` Ć© armazenado, e uma chamada idĆŖntica dentro desse TTL Ć© servida do cache sem viagem de ida e volta. Um resultado que nĆ£o carrega *nenhuma* dica nĆ£o Ć© armazenado: resultados sem dica recebem `CacheConfig.default_ttl_ms`, cujo padrĆ£o Ć© `0` (obsoleto imediatamente), entĆ£o um servidor que nĆ£o declara nada vĆŖ exatamente o mesmo trĆ”fego chamada a chamada de sempre. -```python title="client.py" hl_lines="33 35 38" +Para ver isso acontecer, sirva o `server.py` da seção anterior com o uvicorn (a Ćŗltima linha dele monta o app ASGI). O handler imprime uma linha toda vez que executa de verdade: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Execute `python client.py` em um segundo terminal. Ele imprime as dicas que o primeiro resultado carregava, o `ttlMs` do handler ao lado do `cacheScope` do mapa: + +```text +1000 public +``` + +O terminal do servidor conta o resto da história: entre os logs de requisição do uvicorn, `tools/list served` aparece trĆŖs vezes. + Quatro chamadas, trĆŖs buscas. A segunda chamada encontrou uma entrada fresca e nunca chegou ao servidor; avanƧar o relógio (injetado) alĆ©m do TTL fez a terceira buscar de novo; a quarta disse `cache_mode="refresh"`. Esse argumento nomeado existe nos cinco verbos com cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (o padrĆ£o) serve uma entrada fresca se houver uma, e armazena a busca se nĆ£o houver. @@ -56,7 +70,7 @@ Quatro chamadas, trĆŖs buscas. A segunda chamada encontrou uma entrada fresca e Uma regra fica acima de `"use"`: **chamadas que carregam `meta` sempre chegam ao servidor.** Uma requisição com `meta` definido (um token de progresso, campos de rastreamento) espera uma requisição no fio, entĆ£o sob `cache_mode="use"` ela Ć© tratada como `"refresh"`: a leitura do cache Ć© pulada, e o resultado buscado ainda substitui a entrada em cache. `"bypass"` e um `"refresh"` explĆ­cito se comportam como sempre. -Para desligar o cache por completo, construa com `Client(server, cache=None)`: toda chamada volta a ser uma viagem de ida e volta, e `cache_mode`, embora ainda aceito, nĆ£o faz nada. +Para desligar o cache por completo, passe `cache=None` ao construir o `Client`: toda chamada volta a ser uma viagem de ida e volta, e `cache_mode`, embora ainda aceito, nĆ£o faz nada. O escopo tambĆ©m Ć© respeitado automaticamente: entradas `"private"` sĆ£o indexadas pela *partição* do cache (abaixo), enquanto as `"public"` podem optar por um compartilhamento mais amplo. E **notificaƧƵes vencem o TTL** para as entradas exatas que nomeiam: uma notificação `list_changed` remove a listagem em cache correspondente, e `resources/updated` remove a leitura em cache armazenada sob exatamente a sua URI, por mais frescas que estivessem. Numa conexĆ£o 2026-07-28 essas notificaƧƵes chegam num stream `subscriptions/listen` que vocĆŖ abre com `client.listen(...)`, e a remoção se completa antes de o seu observador ver o evento; **[Assinaturas](subscriptions.md)** Ć© essa pĆ”gina. diff --git a/i18n/pt/pages/client/callbacks.md b/i18n/pt/pages/client/callbacks.md index 39308a3e..5fa7b266 100644 --- a/i18n/pt/pages/client/callbacks.md +++ b/i18n/pt/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Callbacks do cliente {#client-callbacks} @@ -63,7 +63,7 @@ Um `tools/call` seu, um `elicitation/create` de volta do servidor, respondido pe O `mode="legacy"` na chamada `Client(...)` estĆ” fazendo trabalho de verdade. Por padrĆ£o, `Client(...)` negocia o caminho moderno do protocolo, e esse caminho nĆ£o tem canal de retorno (back-channel) para requisiƧƵes do servidor ao cliente: `ctx.elicit` falha antes mesmo de o seu callback rodar. NĆ£o Ć© o transporte que decide isso; Ć© o protocolo - negociado, tanto em memória quanto por uma URL. Fixe `mode="legacy"` sempre que o seu cliente tiver + negociado. Fixe `mode="legacy"` sempre que o seu cliente tiver que responder a uma; todos os testes por trĆ”s desta pĆ”gina fazem isso. **[VersƵes do protocolo](../protocol-versions.md)** tem a história completa. Em uma sessĆ£o 2026-07-28 o callback nĆ£o estĆ” morto, ele Ć© alimentado de outro jeito: quando uma ferramenta retorna um @@ -151,4 +151,4 @@ Mais dois. Nenhum deles declara nada. * `sampling_callback` e `list_roots_callback` funcionam do mesmo jeito, mas atendem funcionalidades descontinuadas; servidores modernos usam requisiƧƵes de mĆŗltiplas idas e voltas no lugar. * `logging_callback` e `message_handler` recebem notificaƧƵes. Eles nĆ£o declaram nada. -O primeiro argumento de `Client(...)` Ć© um objeto de transporte. **[Transportes do cliente](transports.md)** cobre todos os tipos. +O primeiro argumento de `Client(...)` escolhe o transporte. **[Transportes do cliente](transports.md)** cobre todos os tipos. diff --git a/i18n/pt/pages/client/index.md b/i18n/pt/pages/client/index.md index abffbbde..72c13cdf 100644 --- a/i18n/pt/pages/client/index.md +++ b/i18n/pt/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # O cliente {#the-client} @@ -11,13 +11,23 @@ Um **`Client`** Ć© como um programa Python conversa com um servidor MCP. ## Seu primeiro cliente {#your-first-client} -```python title="client.py" hl_lines="14-18" +Um cliente precisa de um servidor com quem conversar. Esta Bookshop Ć© o servidor a que todo trecho desta pĆ”gina se conecta. Salve-o como `server.py` e deixe-o rodando via HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -O servidor no topo só estĆ” ali para vocĆŖ ter algo a que se conectar. O cliente sĆ£o as cinco linhas destacadas. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` recebe o **próprio objeto servidor**. Esse Ć© o transporte em memória: sem subprocesso, sem porta, sem HTTP. Ɖ assim que todo exemplo nesta pĆ”gina, e todo teste que vocĆŖ escrever, se conecta. +Isso o serve em `http://localhost:8000/mcp`. O cliente Ć© um programa Ć  parte. Salve-o como `client.py` e execute `python client.py` em um segundo terminal: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` recebe uma **URL**, entĆ£o se conecta via Streamable HTTP ao servidor que vocĆŖ acabou de iniciar. * `async with` Ć© o **ciclo de vida**. Entrar nele conecta e negocia; sair dele desconecta. NĆ£o hĆ” um par `connect()` / `close()`, e um `Client` nĆ£o pode ser reutilizado depois que o bloco termina. * Dentro do bloco, os fatos da conexĆ£o jĆ” estĆ£o ali como propriedades comuns. @@ -25,10 +35,10 @@ O servidor no topo só estĆ” ali para vocĆŖ ter algo a que se conectar. O client `Client` recebe um argumento posicional e resolve o transporte a partir do tipo dele: -* Uma instĆ¢ncia de `MCPServer` (ou do `Server` de baixo nĆ­vel): conectada **no mesmo processo**. -* Uma string de URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, o caminho de produção. -* Um `StdioServerParameters`: o comando a iniciar como **subprocesso**, com o qual se conversa pelo stdin e stdout dele. +* Uma string de URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, o transporte atrĆ”s do qual vocĆŖ faz o deploy. +* Um `StdioServerParameters`: o comando a iniciar como **subprocesso** local, com o qual se conversa pelo stdin e stdout dele. * Um **transporte**: qualquer coisa com que vocĆŖ possa fazer `async with ... as (read, write)`, como `streamable_http_client(url, http_client=...)` em volta do seu próprio cliente HTTP. +* Uma instĆ¢ncia de `MCPServer` (ou do `Server` de baixo nĆ­vel): conectada **no mesmo processo**, sem subprocesso e sem porta. Essa Ć© para testes, e **[Testes](../get-started/testing.md)** se apoia nela. Todo o resto desta pĆ”gina Ć© idĆŖntico entre os quatro. CabeƧalhos, subprocessos, timeouts e o protocolo `Transport` tĆŖm sua própria pĆ”gina: **[Transportes do cliente](transports.md)**. @@ -49,11 +59,11 @@ VocĆŖ nunca escolheu uma versĆ£o do protocolo. Por padrĆ£o, o `Client` sonda o s ## Listando ferramentas {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` retorna um `ListToolsResult`; as ferramentas estĆ£o em `.tools`. Cada uma Ć© a definição completa que um host entregaria a um modelo: +`list_tools()` retorna um `ListToolsResult`; as ferramentas estĆ£o em `.tools`. Cada uma Ć© a definição completa que um host entregaria a um modelo. Eis a primeira: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ e `tool.input_schema` Ć© o JSON Schema que o servidor derivou das anotaƧƵes de Esse schema Ć© tudo o que uma UI precisa para renderizar um formulĆ”rio de argumentos, e tudo o que um modelo precisa para produzir argumentos vĆ”lidos. +A segunda ferramenta, `lookup_book`, foi registrada sem um `title=`, entĆ£o o `tool.title` dela Ć© `None`. + !!! tip `title` Ć© opcional, entĆ£o uma UI que mostra ferramentas a um humano tem que escolher: o `title` se houver um, o `name` se nĆ£o. `from mcp.shared.metadata_utils import get_display_name` faz exatamente isso, @@ -86,7 +98,7 @@ Esse schema Ć© tudo o que uma UI precisa para renderizar um formulĆ”rio de argum `call_tool(name, arguments)` executa a ferramenta e devolve um `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ Uma ferramenta que lanƧa uma exceção **nĆ£o** lanƧa no seu cliente. Ela volt Os verbos de recurso vĆŖm em pares: duas formas de listar, uma forma de ler. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ Um cliente tambĆ©m pode ser avisado quando um recurso muda. Em conexƵes da era ## Prompts {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ Um host entrega essas mensagens direto ao modelo. A funcionalidade inteira Ć© es Um servidor com um handler de completion pode autocompletar argumentos de prompts e de templates de recurso enquanto o usuĆ”rio digita. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ A resposta estĆ” em `result.completion.values`. Digite `"p"` e o servidor volta Todo mĆ©todo `list_*` aceita um argumento nomeado `cursor=` e todo resultado carrega um `next_cursor`. Quando `next_cursor` Ć© `None`, vocĆŖ tem tudo. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Esse loop estĆ” correto contra qualquer servidor. O `MCPServer` retorna tudo em uma pĆ”gina só, entĆ£o `next_cursor` Ć© `None` e o loop roda uma vez, e Ć© por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estĆ£o em **[Paginação](../advanced/pagination.md)**. +`list_all_tools` estĆ” correta contra qualquer servidor. O `MCPServer` retorna tudo em uma pĆ”gina só, entĆ£o `next_cursor` Ć© `None` e o loop roda uma vez, e Ć© por isso que a maioria do código nunca o escreve. Servidores que paginam de verdade, e as regras que os cursores obedecem, estĆ£o em **[Paginação](../advanced/pagination.md)**. ## Em testes {#in-tests} -`Client(mcp)`, sem processo e sem porta, jĆ” Ć© um harness de teste para o seu servidor. +Todo `client.py` desta pĆ”gina alcanƧou o `server.py` via HTTP. Em um teste vocĆŖ pula a rede e entrega ao `Client` o próprio objeto servidor: `from server import mcp`, depois `Client(mcp)`. Sem processo, sem porta, e todo mĆ©todo acima funciona igual. -Existe uma flag do construtor feita para isso: `Client(mcp, raise_exceptions=True)`. Ela só tem efeito em conexƵes em memória, e **[Testes](../get-started/testing.md)** Ć© a pĆ”gina que a explica e constrói todo o padrĆ£o em torno dela. +Existe uma flag do construtor feita para isso: `Client(mcp, raise_exceptions=True)`. Ela só tem efeito em conexƵes no mesmo processo, e **[Testes](../get-started/testing.md)** Ć© a pĆ”gina que a explica e constrói todo o padrĆ£o em torno dela. ## Recapitulando {#recap} -* `Client(x)` conecta em memória a um objeto servidor, via Streamable HTTP a uma string de URL, e por qualquer outra coisa via um transporte. +* `Client(x)` conecta via Streamable HTTP a uma string de URL, inicia um subprocesso para um `StdioServerParameters`, entra direto em um transporte e, em testes, recebe o próprio objeto servidor. * `async with` Ć© o ciclo de vida inteiro. Dentro dele, `server_capabilities` e `protocol_version` jĆ” estĆ£o preenchidos; `server_info` e `instructions` tambĆ©m, quando o servidor os fornece. * `list_tools()` dĆ” a vocĆŖ o `name`, `title`, `description` e `input_schema` de cada ferramenta. * `call_tool()` retorna `content` para o modelo, `structured_content` para o seu código e `is_error`. Uma ferramenta que lanƧa exceção Ć© um resultado, nĆ£o uma exceção. diff --git a/i18n/pt/pages/client/oauth-clients.md b/i18n/pt/pages/client/oauth-clients.md index 51d1824f..7464435f 100644 --- a/i18n/pt/pages/client/oauth-clients.md +++ b/i18n/pt/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # Clientes OAuth {#oauth-clients} @@ -81,18 +81,20 @@ Veja `main()`. O provider vai no **cliente httpx2**, o cliente httpx2 vai em `st Na primeira vez que `Client` envia uma requisição, o servidor responde `401`. O provider assume: -1. **Descoberta.** Ele lĆŖ o cabeƧalho `WWW-Authenticate`, busca os Protected Resource Metadata do servidor em `/.well-known/oauth-protected-resource`, descobre qual servidor de autorização protege este recurso e busca os metadados *desse* servidor. +1. **Descoberta.** Ele lĆŖ o cabeƧalho `WWW-Authenticate`, busca os Protected Resource Metadata do servidor em `/.well-known/oauth-protected-resource`, descobre qual servidor de autorização protege este recurso e busca os metadados *desse* servidor. (A um servidor mais antigo, que nĆ£o publica metadados de recurso, o provider pede os metadados do servidor de autorização na própria origem dele.) De um jeito ou de outro, os metadados precisam nomear, como `issuer`, o servidor para o qual foram buscados; qualquer outra coisa Ć© recusada. 2. **Registro.** Nada no armazenamento? Ele registra vocĆŖ dinamicamente com o seu `OAuthClientMetadata` e armazena o resultado. 3. **Autorização.** Ele gera o par PKCE e um `state`, monta a URL de autorização, faz await no seu `redirect_handler` e depois faz await no seu `callback_handler` para obter o code. 4. **Troca.** Ele troca o code por um `OAuthToken`, armazena e reenvia a sua requisição original com `Authorization: Bearer ...`. Depois disso ele fica quieto. Os tokens saem do armazenamento, um token de acesso expirado Ć© renovado com o refresh token, e só quando nada disso funciona ele executa o fluxo de novo. +Uma regra de transporte vale para todas essas requisiƧƵes: assim como a requisição MCP dentro da qual elas acontecem, elas só seguem um redirecionamento quando ele fica na mesma origem e mantĆ©m o mĆ©todo (um 307/308 de barra final, digamos), e tratam qualquer outro redirecionamento como se aquela URL nĆ£o respondesse. + VocĆŖ nĆ£o escreveu nada disso. Restam dois argumentos nomeados (`client_metadata_url` e `validate_resource_url`), e este arquivo nĆ£o precisa de nenhum dos dois. `client_metadata_url` Ć© o que vale a pena conhecer; ele ganha uma seção própria abaixo. ### Experimente {#try-it} -A maioria dos exemplos nesta documentação vocĆŖ consegue conferir com um `Client(server)` em memória. Este nĆ£o: o ponto central do fluxo Ć© um `401` HTTP, e nĆ£o hĆ” HTTP entre um cliente em memória e o seu servidor. +O `Client(server)` em memória que os seus testes usam nĆ£o ajuda aqui: o ponto central do fluxo Ć© um `401` HTTP, e nĆ£o hĆ” HTTP entre um cliente em memória e o seu servidor. O repositório traz a versĆ£o ao vivo. `examples/servers/simple-auth/` executa um servidor de autorização independente e um servidor MCP protegido; `examples/clients/simple-auth-client/` Ć© o cliente desta pĆ”gina crescido atĆ© virar uma pequena CLI. O README dele tem os dois comandos: inicie os servidores, execute o cliente contra eles e veja as quatro etapas passarem. @@ -110,13 +112,14 @@ Um job noturno, uma etapa de CI, outro serviƧo. NĆ£o hĆ” navegador nem ninguĆ©m `ClientCredentialsOAuthProvider` Ć© o mesmo `httpx2.Auth`, sem o humano: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` O que mudou: * Sem `OAuthClientMetadata`, sem handlers. VocĆŖ passa `client_id` e `client_secret`; o provider monta um registro `client_credentials` mĆ­nimo em torno deles e pula o registro dinĆ¢mico por completo. +* `issuer` nomeia o servidor de autorização que emitiu essas credenciais; use o valor `issuer` que o documento `/.well-known/oauth-authorization-server` dele retorna. A descoberta ainda acontece como acima, mas as requisiƧƵes de token só sĆ£o montadas a partir dos metadados *desse* issuer; se o servidor MCP apontar para qualquer outro lugar, o fluxo para com um `OAuthFlowError`. DeixĆ”-lo de fora estĆ” obsoleto e ele passa a ser obrigatório na 3.0 (veja **[Funcionalidades obsoletas](../deprecated.md#deprecated-sdk-helpers)**); atĆ© lĆ”, o provider emite um aviso e usa o servidor de autorização que a descoberta encontrar. * `scope` Ć© uma string separada por espaƧos, o formato OAuth usado na comunicação. * Tudo a partir daĆ­ Ć© idĆŖntico: o mesmo `TokenStorage`, o mesmo `httpx2.AsyncClient(auth=...)`, o mesmo `streamable_http_client`. @@ -129,7 +132,7 @@ Por padrĆ£o, o secret viaja como HTTP Basic auth na requisição de token (`clie Mais um provider mora em `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`**, para clientes que se autenticam com um JWT em vez de um segredo compartilhado (`private_key_jwt`, a variante de par de chaves e workload identity). Ele segue - o mesmo padrĆ£o: construa um, coloque em `auth=`. O mesmo módulo traz + o mesmo padrĆ£o: construa um (ele aceita o mesmo `issuer` opcional), coloque em `auth=`. O mesmo módulo traz `SignedJWTParameters` e `static_assertion_provider`, dois helpers que montam a assertion dele. HĆ” mais uma situação sem humano: o cliente pertence a uma empresa cujo provedor de identidade, e nĆ£o o usuĆ”rio, decide quais servidores MCP ele pode alcanƧar. Esse Ć© um grant diferente, com seu próprio modelo de confianƧa e sua própria pĆ”gina, **[Asserção de identidade](identity-assertion.md)**. diff --git a/i18n/pt/pages/client/session-groups.md b/i18n/pt/pages/client/session-groups.md index 7f42531d..8b2324b2 100644 --- a/i18n/pt/pages/client/session-groups.md +++ b/i18n/pt/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Grupos de sessƵes {#session-groups} @@ -78,7 +78,7 @@ Se vocĆŖ jĆ” tem em mĆ£os uma `ClientSession` conectada (`Client.session` Ć© uma ## Recapitulando {#recap} * `ClientSessionGroup` mantĆ©m vĆ”rias conexƵes de servidor e reĆŗne as ferramentas, recursos e prompts delas em um `dict` para cada tipo. -* `connect_to_server(params)` por servidor. Ele recebe parĆ¢metros de transporte, nunca o objeto de servidor ou a URL que um `Client` recebe. +* `connect_to_server(params)` por servidor. Ele recebe parĆ¢metros de transporte, nunca a URL ou o `Transport` que um `Client` recebe. * `group.call_tool(name, arguments)` roteia para o servidor dono por vocĆŖ. * Os nomes precisam ser Ćŗnicos no grupo inteiro; dois servidores com uma ferramenta `search` nĆ£o conseguem coexistir por conta própria. * `component_name_hook=` reescreve cada nome registrado. A chave do dict muda, o nome na rede nĆ£o. diff --git a/i18n/pt/pages/client/transports.md b/i18n/pt/pages/client/transports.md index 31504bac..7d8c4801 100644 --- a/i18n/pt/pages/client/transports.md +++ b/i18n/pt/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Transportes do cliente {#client-transports} @@ -11,30 +11,15 @@ VocĆŖ nunca configura um transporte separadamente. `Client` recebe um Ćŗnico arg O lado do *servidor* de cada um (o que `mcp.run()` faz e o que vocĆŖ coloca no deploy) estĆ” em **[Executando seu servidor](../run/index.md)**. -## Em memória {#in-memory} - -Passe o próprio objeto do servidor: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Sem subprocesso, sem porta, sem bytes trafegando na rede. O cliente e o servidor sĆ£o dois objetos no mesmo processo, e a chamada ainda passa pela camada real do protocolo: `search_books` Ć© listada, validada e invocada exatamente como seria sobre HTTP. - -Isso faz dele duas coisas ao mesmo tempo: - -* **Uma estrutura de testes.** Todo exemplo desta documentação Ć© exercitado dessa forma, e a pĆ”gina **[Testes](../get-started/testing.md)** constrói o padrĆ£o inteiro em torno disso. -* **Uma API de embutimento.** Uma aplicação que constrói o servidor nĆ£o precisa de um salto pela rede para chamar as ferramentas dele. - ## Streamable HTTP {#streamable-http} -Passe uma string de URL e vocĆŖ tem **Streamable HTTP**, o transporte atrĆ”s do qual vocĆŖ faz o deploy: +Passe uma string de URL e vocĆŖ tem **Streamable HTTP**, o transporte atrĆ”s do qual vocĆŖ faz o deploy e o primeiro a que vocĆŖ deve recorrer: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Esse Ć© o cliente de produção inteiro. `Client` envolve a URL em `streamable_http_client(...)` para vocĆŖ, sobre um `httpx2.AsyncClient` configurado do jeito que o MCP precisa: `follow_redirects=True`, um timeout de 30 segundos para connect/write/pool e um timeout de leitura de 300 segundos, porque o servidor pode manter um stream de resposta aberto. +Esse Ć© o cliente de produção inteiro. `Client` envolve a URL em `streamable_http_client(...)` para vocĆŖ, sobre um `httpx2.AsyncClient` configurado do jeito que o MCP precisa: um timeout de 30 segundos para connect/write/pool e um timeout de leitura de 300 segundos, porque o servidor pode manter um stream de resposta aberto. !!! check Um `Client` que vocĆŖ construiu **nĆ£o** estĆ” conectado. A construção só escolhe o transporte; @@ -50,7 +35,7 @@ Esse Ć© o cliente de produção inteiro. `Client` envolve a URL em `streamable_h No momento em que vocĆŖ precisar de um header `Authorization`, um cookie, um proxy, mTLS ou um timeout diferente, construa o `httpx2.AsyncClient` vocĆŖ mesmo e entregue-o a `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ sem um repositório de CAs do sistema utilizĆ”vel (alguns contĆŖineres mĆ­nimos) !!! info `httpx2` mantĆ©m a API conhecida do `httpx`, entĆ£o se vocĆŖ conhece `httpx` jĆ” sabe como fazer auth, proxies, event hooks, retentativas e limites de conexĆ£o aqui. O SDK nĆ£o acrescenta nada por cima nem - tira nada. Ɖ tambĆ©m onde o OAuth se encaixa: + tira nada, exceto o [tratamento de redirecionamentos](#redirects). Ɖ tambĆ©m onde o OAuth se encaixa: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Esse fluxo inteiro estĆ” em **[Clientes OAuth](oauth-clients.md)**. +### Redirecionamentos {#redirects} + +O transporte se conecta Ć  URL que vocĆŖ forneceu, e somente a essa origem. + +* Um redirecionamento `307`/`308` que permanece no mesmo esquema, host e porta Ć© seguido, e `http://` → `https://` no mesmo host tambĆ©m. Isso cobre o redirecionamento habitual de barra final `/mcp` → `/mcp/`. +* Um redirecionamento para qualquer outro lugar **nĆ£o** Ć© seguido. A chamada falha com: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Se essa URL Ć© o servidor que vocĆŖ queria, coloque-a na sua configuração. Se nĆ£o Ć©, o servidor ou um proxy na frente dele estĆ” mal configurado. + +Isso vale para qualquer `httpx2.AsyncClient` que vocĆŖ passe: a configuração `follow_redirects` dele nĆ£o Ć© consultada para requisiƧƵes MCP, em nenhuma das direƧƵes. Os provedores OAuth do SDK aplicam a mesma regra Ć s próprias requisiƧƵes. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` significa que o + servidor estĆ” atrĆ”s de um proxy com terminação TLS que ele desconhece e estĆ” emitindo redirecionamentos `http://`. + Isso se corrige no servidor (**[Deploy e escala](../run/deploy.md#behind-a-tls-terminating-proxy)**) + ou usando a URL exata `https://…/` que a mensagem sugere. + ## stdio {#stdio} Um servidor **stdio** Ć© um subprocesso. O cliente o inicia, escreve JSON-RPC no stdin dele e lĆŖ JSON-RPC do stdout dele. Ɖ assim que um host de desktop executa um servidor na sua mĆ”quina: um host *Ć©* este código mais uma interface, e **[Conecte a um host real](../get-started/real-host.md)** Ć© a mesma relação vista do lado do host, como um arquivo de configuração. @@ -105,6 +111,18 @@ O stderr do processo filho vai para o seu. Para mandĆ”-lo para outro lugar, cons Um servidor que precise de uma chave de API nĆ£o vai encontrĆ”-la ali. Passe-a explicitamente com `env=`; essas variĆ”veis sĆ£o mescladas por cima da allow-list. Ɖ isso que `BOOKSHOP_API_KEY` estĆ” fazendo acima. +## Em memória {#in-memory} + +Em um teste nĆ£o hĆ” nada para colocar no deploy nem nada para iniciar. Passe o próprio objeto do servidor: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Sem subprocesso, sem porta, sem bytes trafegando na rede. O cliente e o servidor sĆ£o dois objetos no mesmo processo, e a chamada ainda passa pela camada real do protocolo: `search_books` Ć© listada, validada e invocada exatamente como seria sobre HTTP. A pĆ”gina **[Testes](../get-started/testing.md)** constrói o padrĆ£o inteiro em torno disso. + +A mesma forma serve tambĆ©m como API de embutimento: uma aplicação que constrói o servidor por conta própria pode chamar as ferramentas dele sem um salto pela rede. + ## SSE {#sse} `sse_client(url)`, de `mcp.client.sse`, Ć© o transporte HTTP que o Streamable HTTP substituiu. Envolva-o da mesma forma, `Client(sse_client("http://localhost:8000/sse"))`, para conversar com um servidor que ainda o fala, e nĆ£o construa nada novo em cima dele. @@ -113,15 +131,16 @@ O stderr do processo filho vai para o seu. Para mandĆ”-lo para outro lugar, cons Para o `Client`, tudo acima Ć© a mesma coisa. -Um **transporte** Ć© qualquer gerenciador de contexto assĆ­ncrono que produz um par `(read, write)` de streams de mensagens: formalmente, o protocolo `Transport` em `mcp.client`. `Client` resolve seu argumento pelo tipo: um objeto de servidor conecta no próprio processo, uma `str` vira `streamable_http_client(url)`, um `StdioServerParameters` vira `stdio_client(params)` e qualquer outra coisa Ć© aberta diretamente como transporte. Ɖ por causa dessa Ćŗltima regra que `stdio_client(...)`, `streamable_http_client(...)` e `sse_client(...)` se encaixam todos no mesmo lugar, e que vocĆŖ pode escrever o seu próprio. +Um **transporte** Ć© qualquer gerenciador de contexto assĆ­ncrono que produz um par `(read, write)` de streams de mensagens: formalmente, o protocolo `Transport` em `mcp.client`. `Client` resolve seu argumento pelo tipo: uma `str` vira `streamable_http_client(url)`, um `StdioServerParameters` vira `stdio_client(params)`, um objeto de servidor conecta no próprio processo e qualquer outra coisa Ć© aberta diretamente como transporte. Ɖ por causa dessa Ćŗltima regra que `stdio_client(...)`, `streamable_http_client(...)` e `sse_client(...)` se encaixam todos no mesmo lugar, e que vocĆŖ pode escrever o seu próprio. ## Recapitulando {#recap} -* `Client(mcp)` (o objeto do servidor) conecta em memória. Use para testes e para embutir. * `Client("http://.../mcp")` (uma URL) conecta por Streamable HTTP, o transporte de produção. * Headers, auth, proxies e timeouts pertencem a um `httpx2.AsyncClient` que vocĆŖ passa a `streamable_http_client(url, http_client=...)`. NĆ£o existe o argumento `headers=`. +* Redirecionamentos só sĆ£o seguidos dentro da própria origem da URL (um `307`/`308` de barra final), mais `http`→`https` no mesmo host. Qualquer outra coisa falha com `Redirect to … not followed`; configure a URL final. * stdio Ć© `Client(StdioServerParameters(...))`. Envolva-o em `stdio_client(...)` vocĆŖ mesmo apenas para redirecionar o stderr do processo filho. * O subprocesso recebe um ambiente em allow-list, nĆ£o o seu; `env=` acrescenta a ele. +* `Client(mcp)` (o objeto do servidor) conecta em memória. Use em testes, ou para embutir um servidor na aplicação que o construiu. * Um transporte Ć© qualquer coisa com que vocĆŖ possa fazer `async with x as (read, write)`. `Client` entrega direto a esse protocolo tudo que nĆ£o for um objeto de servidor, uma URL ou um `StdioServerParameters`. * Construir um `Client` escolhe o transporte. `async with` o abre. diff --git a/i18n/pt/pages/deprecated.md b/i18n/pt/pages/deprecated.md index 564d9ae2..a4d8f0e1 100644 --- a/i18n/pt/pages/deprecated.md +++ b/i18n/pt/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Funcionalidades descontinuadas {#deprecated-features} -A especificação 2026-07-28 aposenta cinco coisas. O SDK ainda implementa cada uma delas, e cada uma agora carrega um **aviso de descontinuação**. Um helper do SDK estĆ” descontinuado por conta própria e aparece listado [no final](#deprecated-sdk-helpers). +A especificação 2026-07-28 aposenta cinco coisas. O SDK ainda implementa cada uma delas, e cada uma agora carrega um **aviso de descontinuação**. Algumas descontinuaƧƵes no nĆ­vel do SDK valem por conta própria e aparecem listadas [no final](#deprecated-sdk-helpers). A tabela abaixo nomeia cada funcionalidade descontinuada, o motivo de ela estar saindo e o substituto sobre o qual construir. @@ -137,11 +137,13 @@ A API inteira Ć© essa. NĆ£o hĆ” uma chave por mĆ©todo, e vocĆŖ nĆ£o quer uma: o ## Helpers descontinuados do SDK {#deprecated-sdk-helpers} -Estas nĆ£o sĆ£o mudanƧas de especificação, apenas detalhes internos do SDK com um substituto melhor. Avisam com o mesmo `MCPDeprecationWarning` e serĆ£o removidos na 3.0. +Estas nĆ£o sĆ£o mudanƧas de especificação, apenas usos do SDK com um substituto melhor. Avisam com o mesmo `MCPDeprecationWarning`, e a 3.0 remove a forma antiga. | Descontinuado | O que fazer no lugar | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()` e depois `FuncMetadata.call_fn()`. Só código que conduz `FuncMetadata` diretamente (uma subclasse personalizada de `Tool`, digamos) chegou a chamĆ”-lo. | +| `AuthSettings(resource_server_url=...)` sem `validate_token_resource=` | Defina-o: `True` faz o servidor recusar bearer tokens que o seu verificador nĆ£o informa como emitidos para `resource_server_url`; `False` diz que o seu verificador confere a audiĆŖncia do token por conta própria (veja **[Autorização](run/authorization.md#a-token-verifier)**). Sem definir, ele se comporta como `False`; a 3.0 torna `True` o padrĆ£o sempre que `resource_server_url` estiver definido. | +| `ClientCredentialsOAuthProvider(...)` ou `PrivateKeyJWTOAuthProvider(...)` sem `issuer=` | Passe `issuer=` nomeando o servidor de autorização que emitiu as credenciais (veja **[Escrevendo clientes OAuth](client/oauth-clients.md#machine-to-machine)**). Sem ele, Ć© o servidor MCP que decide qual servidor de autorização as recebe; a 3.0 torna o parĆ¢metro obrigatório. | ## Recapitulando {#recap} @@ -150,7 +152,7 @@ Estas nĆ£o sĆ£o mudanƧas de especificação, apenas detalhes internos do SDK co * Descontinuado Ć© consultivo: sem mudanƧas no protocolo de transmissĆ£o, tudo continua funcionando em sessƵes prĆ©-2026, e vocĆŖ recebe um `MCPDeprecationWarning` visĆ­vel (um `UserWarning`, entĆ£o estĆ” ligado por padrĆ£o). * Amostragem e roots precisam, alĆ©m disso, de um canal de retorno que uma sessĆ£o 2026-07-28 nĆ£o tem. Em uma conexĆ£o moderna elas avisam e depois lanƧam uma exceção. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` silencia a categoria inteira; `"error::mcp.MCPDeprecationWarning"` no pytest a transforma em falha de teste. -* Um helper do SDK, `FuncMetadata.call_fn_with_arg_validation()`, estĆ” descontinuado separadamente para remoção na 3.0. +* As [descontinuaƧƵes no nĆ­vel do SDK](#deprecated-sdk-helpers) seguem a mesma regra: avisam agora, e a 3.0 remove a forma antiga. * Código novo nĆ£o deve ser construĆ­do sobre nenhuma delas. Todas as outras pĆ”ginas desta documentação ensinam a API atual. diff --git a/i18n/pt/pages/get-started/first-steps.md b/i18n/pt/pages/get-started/first-steps.md index 32b8bb46..baeb501a 100644 --- a/i18n/pt/pages/get-started/first-steps.md +++ b/i18n/pt/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # Primeiros passos {#first-steps} @@ -17,7 +17,7 @@ TrĆŖs palavras que vocĆŖ vai ver em todas as pĆ”ginas daqui em diante: * Um **cliente** vive dentro do host e fala MCP. O host executa um cliente para cada servidor ao qual estĆ” conectado. * Um **servidor** Ć© o que vocĆŖ constrói com este SDK. Ele expƵe coisas aos clientes. Nunca fala diretamente com o modelo. -VocĆŖ escreve o servidor. Os hosts sĆ£o produto de terceiros. O SDK tambĆ©m traz um `Client`. VocĆŖ vai usĆ”-lo para testar seus servidores, e ele aparece mais adiante nesta pĆ”gina. +VocĆŖ escreve o servidor. Os hosts sĆ£o produto de terceiros. O SDK tambĆ©m traz um `Client`, a mesma classe que um host usaria para alcanƧar um servidor por URL ou iniciĆ”-lo como subprocesso. Ele aparece mais adiante nesta pĆ”gina, e Ć© tambĆ©m com ele que vocĆŖ vai testar seus servidores. ## As trĆŖs primitivas {#the-three-primitives} @@ -83,22 +83,20 @@ VocĆŖ viu trĆŖs abas no Inspector. Como ele sabia que eram trĆŖs? Quando um cliente se conecta, o servidor declara suas **capacidades**: quais famĆ­lias de requisiƧƵes ele vai responder. O cliente usa essa declaração para decidir o que faz sentido pedir. VocĆŖ nunca escreveu isso; o `MCPServer` declara por vocĆŖ. -Veja vocĆŖ mesmo. O `Client` do SDK aceita o objeto do servidor diretamente e se conecta a ele **em memória** (sem subprocesso, sem porta): +Veja vocĆŖ mesmo. Deixe o `server.py` rodando via HTTP em um terminal: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +e aponte um cliente para ele a partir de outro: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ O `MCPServer` serve as trĆŖs primitivas, entĆ£o as trĆŖs sĆ£o sempre declaradas. Repare no que nĆ£o aparece ali. `completions` (autocompletar de argumentos para templates de recurso e prompts) precisa de um handler escrito por vocĆŖ; este servidor nĆ£o tem nenhum, entĆ£o a capacidade fica de fora e um cliente bem-comportado nem pede. Essa Ć© a regra para tudo que Ć© opcional: registre a coisa e a capacidade aparece; **[Completions](../servers/completions.md)** comprova isso. !!! info - `Client(mcp)` Ć© o mesmo cliente em memória com que todos os exemplos desta documentação sĆ£o - testados, e Ć© assim que vocĆŖ vai testar os seus. Ele ganha uma pĆ”gina inteira: **[Testes](testing.md)**. + Esse `client.py` Ć© um cliente MCP completo, e **[O cliente](../client/index.md)** Ć© a pĆ”gina dele. + Em um teste, vocĆŖ pula o terminal e a porta e entrega ao `Client` o próprio objeto do servidor, + `Client(mcp)`. Isso tambĆ©m ganha uma pĆ”gina inteira: **[Testes](testing.md)**. ## O que vocĆŖ nĆ£o escreveu {#what-you-did-not-write} @@ -128,7 +127,7 @@ Olhe de novo esta pĆ”gina. VocĆŖ escreveu trĆŖs funƧƵes Python pequenas. VocĆŖ * Um JSON Schema. `a: int, b: int` *Ć©* o schema de `add`. * Um handler de requisição. `tools/list`, `resources/read`, `prompts/get`: o SDK atende todos por vocĆŖ. * Uma declaração de capacidades. O `MCPServer` fez isso por vocĆŖ. -* Uma linha de protocolo. A negociação de versĆ£o, o enquadramento JSON-RPC, a troca de capacidades: tudo isso aconteceu dentro de `mcp dev` e `Client(mcp)`, e vocĆŖ nunca viu. +* Uma linha de protocolo. A negociação de versĆ£o, o enquadramento JSON-RPC, a troca de capacidades: tudo isso aconteceu dentro de `mcp dev` e `client.py`, e vocĆŖ nunca viu. Essa proporção Ć© a razĆ£o de ser do SDK. @@ -139,6 +138,6 @@ Essa proporção Ć© a razĆ£o de ser do SDK. * Um decorador por primitiva: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Nome, descrição e schema vĆŖm da função. * Uma URI com um `{param}` cria um **template** de recurso, listado separadamente dos recursos concretos. * As **capacidades** do servidor jĆ” vĆŖm declaradas para vocĆŖ, e um cliente só pede o que o servidor declara. -* `Client(mcp)` se conecta ao objeto do servidor em memória: seu ambiente de testes desde o primeiro dia. +* `Client("http://localhost:8000/mcp")` conversa com seu servidor em execução. Entregue a ele o objeto do servidor no lugar, `Client(mcp)`, e ele vira seu ambiente de testes desde o primeiro dia. A seguir vem **[Conecte a um host real](real-host.md)**: este servidor dentro do Claude Desktop ou de uma IDE, de verdade. Depois, **[Testes](testing.md)**: uma pĆ”gina, um cliente em memória, e vocĆŖ nunca mais fica adivinhando se funciona. Depois disso, cada primitiva ganha sua própria pĆ”gina, comeƧando pela que o modelo comanda: **[Ferramentas](../servers/tools.md)**. diff --git a/i18n/pt/pages/get-started/testing.md b/i18n/pt/pages/get-started/testing.md index 2eb6c062..d2b49686 100644 --- a/i18n/pt/pages/get-started/testing.md +++ b/i18n/pt/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Testes {#testing} -O SDK Python traz uma classe `Client` com um **transporte em memória**: passe a ela o objeto do seu servidor e ela se conecta diretamente a ele. +A classe `Client` do SDK, a mesma que se conecta a uma URL ou inicia um subprocesso, tambĆ©m se conecta **em memória**: passe a ela o objeto do seu servidor e ela conversa diretamente com ele. -Sem subprocesso. Sem porta. Sem transporte nenhum. Ɖ a mesma ideia do `TestClient` do FastAPI. +Sem subprocesso. Sem porta. Nada trafegando na rede. Ɖ a mesma ideia do `TestClient` do FastAPI. ## Uso bĆ”sico {#basic-usage} @@ -97,7 +97,7 @@ em vez da sanitizada. Deixe-a ligada nos testes. Em código de produção, ela nĆ£o significa nada. -## No mesmo processo por padrĆ£o {#in-process-by-default} +## Neutro quanto Ć  era por padrĆ£o {#era-neutral-by-default} !!! note `Client(mcp)` se conecta no mesmo processo e Ć© **neutro quanto Ć  era** por padrĆ£o: ele sonda o servidor e diff --git a/i18n/pt/pages/handlers/multi-round-trip.md b/i18n/pt/pages/handlers/multi-round-trip.md index 77432a57..ff7b0448 100644 --- a/i18n/pt/pages/handlers/multi-round-trip.md +++ b/i18n/pt/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # RequisiƧƵes com mĆŗltiplas idas e voltas {#multi-round-trip-requests} @@ -164,7 +164,7 @@ O `Server` de baixo nĆ­vel Ć© o nĆ­vel sem pilhas inclusas: diferente do `MCPSer ## Um resultado de 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` só existe na versĆ£o de protocolo **2026-07-28**. O `Client(server)` em memória a negocia por vocĆŖ; pela rede, `mode="auto"` a descobre. Depois de conectar, `client.protocol_version` diz o que vocĆŖ obteve. +`InputRequiredResult` só existe na versĆ£o de protocolo **2026-07-28**. O `mode="auto"` padrĆ£o do `Client` a descobre em qualquer conexĆ£o. Depois de conectar, `client.protocol_version` diz o que vocĆŖ obteve. !!! warning Uma sessĆ£o prĆ©-2026 nĆ£o tem onde colocar um `InputRequiredResult`. Retorne um do seu handler em uma diff --git a/i18n/pt/pages/handlers/progress.md b/i18n/pt/pages/handlers/progress.md index caa2c094..f6b78f8c 100644 --- a/i18n/pt/pages/handlers/progress.md +++ b/i18n/pt/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # Progresso {#progress} @@ -29,19 +29,17 @@ TrĆŖs argumentos, e vocĆŖ decide o que eles significam: O cliente opta por receber **por chamada**, passando `progress_callback=` para `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,28 +54,31 @@ anyio.run(main) O callback Ć© uma função `async` que recebe exatamente o que o servidor informou: `progress`, `total`, `message`. !!! info - `Client(mcp)` conecta direto ao objeto do servidor, em memória, o mesmo cliente sobre o qual a pĆ”gina **[Testes](../get-started/testing.md)** - Ć© construĆ­da. `progress_callback` Ć© o mesmo parĆ¢metro seja qual for o transporte que o `Client` - usa; o *timing* que vocĆŖ estĆ” prestes a ver Ć© o da conexĆ£o em memória. Ela executa seu callback - inline, entĆ£o todo informe chega antes de `call_tool` retornar. Em um transporte real, as - notificaƧƵes disputam corrida com o resultado, e um callback lento ainda pode estar executando depois que `call_tool` - retornou. + `progress_callback` Ć© o mesmo parĆ¢metro seja lĆ” o que vocĆŖ tenha passado ao `Client`: uma URL como aqui, um + `StdioServerParameters`, ou o objeto do servidor em um teste. Mas fique atento ao timing em um transporte + real. Cada notificação Ć© entregue por conta própria, ao lado da resposta, entĆ£o um callback lento + ainda pode estar executando depois que `call_tool` retornou. Só a conexĆ£o de teste em processo + executa o callback inline e garante que todo informe chegue antes. ### Experimente {#try-it} -Coloque `client.py` ao lado de `server.py` e execute: +Sirva `server.py` por HTTP e depois execute o cliente em um segundo terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Cada `await ctx.report_progress(...)` no servidor virou uma chamada a `show` no cliente, em ordem, e as duas linhas foram impressas **antes** de `call_tool` retornar. O progresso nĆ£o vem embutido no resultado; ele Ć© transmitido enquanto a ferramenta ainda estĆ” trabalhando. +Cada `await ctx.report_progress(...)` no servidor virou uma chamada a `show` no cliente, em ordem. O progresso nĆ£o vem embutido no resultado. Ele Ć© transmitido enquanto a ferramenta ainda estĆ” trabalhando. !!! warning `progress_callback` pertence Ć  **chamada**, nĆ£o ao `Client`. NĆ£o hĆ” argumento de construtor diff --git a/i18n/pt/pages/protocol-versions.md b/i18n/pt/pages/protocol-versions.md index 6ce3ae2f..44cec718 100644 --- a/i18n/pt/pages/protocol-versions.md +++ b/i18n/pt/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # VersƵes do protocolo {#protocol-versions} @@ -11,9 +11,17 @@ Os servidores lanƧados antes de 2026-07-28 abrem toda conexĆ£o com o **handshak VocĆŖ quase nunca precisa se preocupar com isso, porque o `Client` negocia por vocĆŖ. Esta pĆ”gina trata do Ćŗnico argumento do construtor que controla isso, `mode=`, e das trĆŖs situaƧƵes em que vocĆŖ o altera. +Cada trecho de código desta pĆ”gina Ć© um `client.py` que conversa com o `server.py` da Bookshop de **[O cliente](client/index.md)**. Inicie esse servidor em um terminal: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Depois execute cada trecho em um segundo terminal com `python client.py`. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ De um jeito ou de outro vocĆŖ sai conectado, e `client.protocol_version` diz qua A funcionalidade inteira Ć© essa. Um `Client`, qualquer era de servidor, sem ramificaƧƵes no seu código. !!! info - O `MCPServer` responde a `server/discover` em todos os transportes — em memória, stdio, streamable - HTTP — entĆ£o, contra o seu próprio servidor, `auto` sempre chega em `2026-07-28`. O fallback só - dispara contra um servidor real anterior a 2026, que Ć© exatamente quando vocĆŖ quer que ele dispare. + O `MCPServer` responde a `server/discover` em todos os transportes — Streamable HTTP, stdio e a + conexĆ£o no mesmo processo que os seus testes usam —, entĆ£o, contra o seu próprio servidor, `auto` + sempre chega em `2026-07-28`. O fallback só dispara contra um servidor real anterior a 2026, que Ć© + exatamente quando vocĆŖ quer que ele dispare. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ Em 2026-07-28 ele nĆ£o existe mais. O servidor *retorna* suas perguntas e vocĆŖ `mode` tambĆ©m aceita uma string de versĆ£o moderna do protocolo. Hoje esse conjunto Ć© exatamente `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ A sondagem Ć© barata, mas ainda Ć© uma ida e volta que vocĆŖ paga a cada reconex EntĆ£o guarde-a. Depois de uma conexĆ£o `auto`, `client.session.discover_result` contĆ©m o `DiscoverResult` exato que o servidor enviou: seu `supported_versions`, seu `capabilities`, seu `instructions` e a identidade que o servidor carimbou no `_meta` do resultado. Passe-o de volta como `prior_discover=` na próxima vez: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/pt/pages/run/asgi.md b/i18n/pt/pages/run/asgi.md index 563ef3d9..940c732b 100644 --- a/i18n/pt/pages/run/asgi.md +++ b/i18n/pt/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # Adicione a um app existente {#add-to-an-existing-app} @@ -99,7 +99,7 @@ Aquele `/mcp` no final Ć© o `streamable_http_path`. Defina-o como `"/"` e o pref --8<-- "docs_src/asgi/tutorial004.py" ``` -Agora os clientes se conectam a `/notes`, nĆ£o a `/notes/mcp`. +Agora os clientes se conectam a `/notes/`, nĆ£o a `/notes/mcp`. ## CORS para clientes no navegador {#cors-for-browser-clients} @@ -142,4 +142,4 @@ Um cliente que roda no navegador precisa de duas permissƵes suas: para **enviar * Clientes no navegador precisam de CORS: `allow_headers` para os headers de requisição `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` para a resposta. * `@mcp.custom_route()` adiciona endpoints HTTP comuns, sem autenticação, ao lado de `/mcp`. -Com o servidor acessĆ­vel em uma URL real, **[O cliente](../client/index.md)** se conecta a ele com essa URL em vez de um objeto servidor. +Com o servidor acessĆ­vel em uma URL real, **[O cliente](../client/index.md)** se conecta a ele com essa URL. diff --git a/i18n/pt/pages/run/authorization.md b/i18n/pt/pages/run/authorization.md index 468206c7..1adb3576 100644 --- a/i18n/pt/pages/run/authorization.md +++ b/i18n/pt/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Autorização {#authorization} @@ -23,12 +23,12 @@ O triĆ¢ngulo inteiro Ć© esse. Tudo nesta pĆ”gina Ć© o item do meio. O SDK nĆ£o tem opiniĆ£o sobre como Ć© um token vĆ”lido. VocĆŖ diz a ele, implementando **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` Ć© um protocolo com um Ćŗnico mĆ©todo assĆ­ncrono. `verify_token` recebe o token bruto do header `Authorization` e retorna um **`AccessToken`** se ele for vĆ”lido, `None` se nĆ£o for. NĆ£o hĆ” mais nada a implementar. -* Este aqui procura o token em uma tabela. Um de verdade verifica a assinatura de um JWT ou chama o endpoint de introspecção de tokens do servidor de autorização. Esse código Ć© seu; o SDK apenas o chama. +* Este aqui procura o token em uma tabela; cada entrada registra o recurso para o qual ele foi emitido. Um de verdade verifica a assinatura de um JWT ou chama o endpoint de introspecção de tokens do servidor de autorização, e informa para quem o token foi emitido (seu `aud`) em `AccessToken.resource`. Esse código Ć© seu; o SDK apenas o chama. * `token_verifier=` e `auth=` sempre andam juntos. Passe um sem o outro e `MCPServer(...)` levanta um `ValueError` antes mesmo de atender uma requisição. `AuthSettings` Ć© a face pĆŗblica do seu resource server: @@ -36,6 +36,10 @@ O SDK nĆ£o tem opiniĆ£o sobre como Ć© um token vĆ”lido. VocĆŖ diz a ele, impleme * `issuer_url`: o servidor de autorização que emite seus tokens. * `resource_server_url`: a URL pĆŗblica deste endpoint MCP. Ela indica *a qual* recurso um token se destina, e Ć© onde fica o documento de descoberta. * `required_scopes`: todo token deve conter todos eles. +* `validate_token_resource`: recusa qualquer token cujo `AccessToken.resource` nĆ£o seja `resource_server_url`. DeixĆ”-lo sem definir enquanto `resource_server_url` estĆ” definido emite um aviso (`MCPDeprecationWarning`) e se comporta como `False`; a versĆ£o 3.0 torna `True` o padrĆ£o para resource servers. + * Ative-o quando seu servidor de autorização vincula os tokens ao `resource` que o cliente pediu, o que clientes MCP sempre enviam. Mantenha `resource_server_url` como a URL exata Ć  qual os clientes se conectam. + * Deixe-o desligado quando seu servidor de autorização usa seus próprios identificadores de audiĆŖncia (um identificador de API do Auth0, um ID de aplicação do Entra) e, em vez disso, verifique `aud` no seu verificador, retornando `None` para um token que nĆ£o Ć© para este servidor. + * Se `aud` for uma lista, coloque em `resource` a entrada que Ć© igual a `resource_server_url`. !!! tip `examples/servers/simple-auth/` no repositório do SDK tem um `IntrospectionTokenVerifier` que chama @@ -91,7 +95,7 @@ Esse documento Ć© como um cliente que nunca ouviu falar do seu servidor encontra Dentro de qualquer handler, **`get_access_token()`** Ć© o `AccessToken` que seu verificador retornou para a requisição atual: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Um servidor de autorização tambĆ©m pode aceitar a asserção assinada de um pr * `token_verifier=` e `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` sempre andam juntos. * O SDK publica o Protected Resource Metadata da [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) em `/.well-known/oauth-protected-resource/...` e responde a requisiƧƵes nĆ£o autenticadas com um 401 cujo header `WWW-Authenticate` aponta para ele. A história da descoberta Ć© toda essa. * `get_access_token()` em qualquer handler diz quem estĆ” chamando. -* Autorização Ć© assunto do HTTP. O `stdio` e o cliente em memória nunca a veem. +* Autorização Ć© assunto do HTTP. O `stdio` e o cliente de teste em memória nunca a veem. A metade do cliente (descobrir seu servidor de autorização e buscar o token para vocĆŖ) estĆ” em **[Clientes OAuth](../client/oauth-clients.md)**. E um cliente que *afirma* uma identidade em vez de pedir uma ao usuĆ”rio estĆ” em **[Asserção de identidade](../client/identity-assertion.md)**. diff --git a/i18n/pt/pages/run/deploy.md b/i18n/pt/pages/run/deploy.md index 8561e0e1..c4d05448 100644 --- a/i18n/pt/pages/run/deploy.md +++ b/i18n/pt/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Deploy e escala {#deploy-scale} @@ -47,6 +47,22 @@ Depois do deploy atrĆ”s de um hostname de verdade, esse mesmo padrĆ£o rejeita ** que recusa toda conexĆ£o Ć© uma allowlist de Host atĆ© que se prove o contrĆ”rio. **[Solução de problemas](../troubleshooting.md)** tambĆ©m comeƧa por aqui. +## AtrĆ”s de um proxy que termina TLS {#behind-a-tls-terminating-proxy} + +Se o TLS termina em um proxy (um ingress, um balanceador de carga, Caddy, nginx) e o uvicorn serve HTTP puro atrĆ”s dele, diga ao uvicorn para confiar nos headers `X-Forwarded-*` do proxy: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Sem isso, o app acredita que estĆ” sendo servido por `http://`, e qualquer redirecionamento que ele emita (o habitual Ć© `/mcp` → `/mcp/`) aponta para `http://…`. O cliente Python se recusa a seguir um endpoint HTTPS para HTTP puro e avisa: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +O paliativo do lado do cliente Ć© configurar a URL exata que o servidor serve (`https://mcp.example.com/mcp/`, com a barra) para que nenhum redirecionamento aconteƧa. A correção Ć© a flag acima. `FORWARDED_ALLOW_IPS` Ć© a grafia como variĆ”vel de ambiente; `*` confia em todo salto, o que só estĆ” certo quando nada alĆ©m do proxy consegue alcanƧar o uvicorn. + ## Workers, e quem precisa de afinidade {#workers-and-who-has-to-be-sticky} Quando o hostname responder, coloque mais de um worker atrĆ”s dele. NĆ£o hĆ” botĆ£o no SDK para isso; vocĆŖ escala um app Starlette do jeito que escala qualquer app ASGI, entregando o objeto a algo que saiba fazer fork: @@ -170,6 +186,7 @@ Um `MCPServer` Ć© uma implementação de protocolo, nĆ£o um servidor de aplicaƧ ## Recapitulando {#recap} * Por padrĆ£o, o app responde apenas a requisiƧƵes endereƧadas ao localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` Ć© o portĆ£o para ir ao ar: atĆ© vocĆŖ passar isso, toda requisição atrĆ”s de um hostname de verdade Ć© um `421` e o motivo só estĆ” no log do servidor. +* AtrĆ”s de um proxy que termina TLS, execute o uvicorn com `--proxy-headers --forwarded-allow-ips=...`, senĆ£o os redirecionamentos dele apontam para `http://` e o cliente os recusa. * No 2026-07-28 nĆ£o hĆ” sessĆ£o e nada em que um balanceador de carga possa ter afinidade. `stateless_http=True` Ć© um botĆ£o só para o legado porque uma requisição moderna Ć© roteada e respondida antes de essa flag ser lida. * A chave padrĆ£o do `requestState` Ć© `os.urandom(32)`, cunhada por processo. Uma nova tentativa de mĆŗltiplas idas e voltas que chega a um worker diferente falha com `-32602` *"Invalid or expired requestState"*. * A correção Ć© `RequestStateSecurity(keys=[...])` **e** o mesmo nome de servidor em toda instĆ¢ncia. O nome Ć© a claim de audiĆŖncia padrĆ£o do token. Mesmas chaves, mesmo nome. diff --git a/i18n/pt/pages/run/index.md b/i18n/pt/pages/run/index.md index 791f05e4..f190bd5c 100644 --- a/i18n/pt/pages/run/index.md +++ b/i18n/pt/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Executando seu servidor {#running-your-server} @@ -75,6 +75,11 @@ Cada transporte tem seus próprios argumentos nomeados, todos em `run()`: * `max_request_body_size`: maior corpo de requisição aceito, em bytes. O padrĆ£o Ć© 4 MiB; requisiƧƵes maiores recebem HTTP 413 antes do parsing ou da criação da sessĆ£o. Aumente apenas quando mensagens MCP legĆ­timas ultrapassarem esse tamanho. +* `session_idle_timeout`: segundos que uma sessĆ£o legada pode ficar sem nada em andamento antes de o + servidor fechĆ”-la. PadrĆ£o 1800. `None` desativa. Veja + [Tempo de vida e limites de sessĆ£o](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: quantas sessƵes legadas um processo mantĆ©m ao mesmo tempo. PadrĆ£o 10 000. `None` + remove o limite. Tratado na mesma seção. * `event_store`, `retry_interval`, `transport_security`: retomada e proteção contra DNS rebinding. Podem esperar atĆ© vocĆŖ fazer o deploy em algum lugar que nĆ£o seja o localhost; **[Deploy e escala](deploy.md)** cobre `transport_security`. !!! warning diff --git a/i18n/pt/pages/run/legacy-clients.md b/i18n/pt/pages/run/legacy-clients.md index e6f0349d..23adca9c 100644 --- a/i18n/pt/pages/run/legacy-clients.md +++ b/i18n/pt/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Atendendo clientes legados {#serving-legacy-clients} @@ -21,15 +21,25 @@ EntĆ£o um cliente legado nĆ£o Ć© algo *para* o qual vocĆŖ constrói. Ɖ algo que ## Um handler, as duas eras {#one-handler-both-eras} -Aqui estĆ” uma ferramenta (tool) que precisa perguntar algo ao usuĆ”rio, e clientes das duas eras chamando-a: +Aqui estĆ” uma ferramenta (tool) que precisa perguntar algo ao usuĆ”rio: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` precisa de uma coisa que o modelo nĆ£o forneceu: quantas cópias. `Annotated[..., Resolve(ask_quantity)]` Ć© como uma ferramenta declara isso (**[DependĆŖncias](../handlers/dependencies.md)** tem essa história completa). Nada em `reserve` cita uma versĆ£o, verifica uma capacidade ou ramifica. -Os dois clientes ficam abertos **ao mesmo tempo**, no mesmo objeto `mcp`. `mode="legacy"` executa o handshake `initialize`: exatamente a conexĆ£o que um cliente prĆ©-2026 abre. O outro usa o padrĆ£o e cai em `2026-07-28`. +Sirva-a via HTTP, e aqui estĆ£o clientes das duas eras chamando-a: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Os dois clientes ficam abertos **ao mesmo tempo**, contra o mesmo servidor em execução. `mode="legacy"` executa o handshake `initialize`: exatamente a conexĆ£o que um cliente prĆ©-2026 abre. O outro usa o padrĆ£o e cai em `2026-07-28`. Execute `python client.py` em um segundo terminal: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -61,6 +71,40 @@ Com um worker, isso Ć© invisĆ­vel. Com dois, Ć© o problema inteiro: uma requisi perdidos para um cliente que se reconecta Ć  *mesma* sessĆ£o), nĆ£o um armazenamento de sessƵes. Ele nunca torna uma sessĆ£o alcanƧƔvel a partir de outro processo. +## Tempo de vida e limites de sessĆ£o {#session-lifetime-and-limits} + +Uma sessĆ£o legada nĆ£o vive para sempre, e um processo nĆ£o mantĆ©m um nĆŗmero ilimitado +delas. Duas configuraƧƵes controlam isso. Ambas sĆ£o argumentos nomeados em `run()`, `streamable_http_app()` +e `Server.streamable_http_app()`. ConexƵes modernas (`2026-07-28`) e `stateless_http=True` +nĆ£o tĆŖm sessƵes, entĆ£o nenhuma das duas configuraƧƵes se aplica a elas. + +| Configuração | PadrĆ£o | O que faz | O que o cliente vĆŖ | Para desligar | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 min) | Fecha uma sessĆ£o que nĆ£o teve nada em andamento por esse tempo. | `404 Session not found`. Ele precisa fazer `initialize` de novo. | `None` | +| `max_sessions` | `10_000` | Recusa abrir uma sessĆ£o alĆ©m dessa quantidade. As sessƵes existentes ficam intactas e nada Ć© despejado. | `503 Too many open sessions` com o código JSON-RPC `-32603`. | `None` | + +O que conta como "em andamento": + +* Um stream `GET` aberto. Os clientes do SDK mantĆŖm um aberto, entĆ£o a sessĆ£o de um cliente conectado nunca + expira. +* Uma requisição que ainda estĆ” sendo respondida. Uma chamada de ferramenta que roda por mais tempo que o timeout nĆ£o Ć© + interrompida, e a contagem regressiva só comeƧa quando ela termina. +* Nada mais. Entre requisiƧƵes, o relógio corre. Qualquer requisição na sessĆ£o o reinicia, + `ping` inclusive. Depois que uma sessĆ£o expirou, nada a revive. + +Um cliente que encerra sua sessĆ£o com `DELETE` a libera imediatamente. O mesmo vale para um cliente cuja +requisição de abertura foi recusada. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +Os dois eventos aparecem no log do servidor. Uma expiração Ć© `Session idle timeout` em `INFO`. Uma +abertura recusada Ć© `Refusing to open a new session: sessions are already open` em `WARNING`. + +Os limites sĆ£o por processo. Com quatro workers, o teto Ć© quatro vezes `max_sessions`, e cada +worker expira suas próprias sessƵes. + ## A Ćŗnica chave: `stateless_http` {#the-one-knob-stateless_http} Se stickiness Ć© um custo que vocĆŖ se recusa a pagar, existe exatamente uma coisa que vocĆŖ pode mudar. @@ -86,7 +130,7 @@ Duas coisas sobre ele importam mais do que o que ele faz. !!! check FaƧa a coisa errada. `reserve` Ć© exatamente a ferramenta que acabou de atender os dois clientes. FaƧa o deploy dela com - `stateless_http=True`, conecte os mesmos dois clientes via HTTP e chame-a de cada um. + `stateless_http=True`, conecte os mesmos dois clientes e chame-a de cada um. O cliente moderno ainda recebe `Reserved 2 of 'Dune'.` A perna moderna nĆ£o mudou. diff --git a/i18n/pt/pages/troubleshooting.md b/i18n/pt/pages/troubleshooting.md index ed7b4b7e..29e6e3c1 100644 --- a/i18n/pt/pages/troubleshooting.md +++ b/i18n/pt/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Solução de problemas {#troubleshooting} @@ -13,6 +13,12 @@ VĆ”rias entradas usam este mesmo servidor. Uma ferramenta (tool) e um recurso co --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Essas entradas o acessam em `http://localhost:8000/mcp`, entĆ£o deixe-o rodando via HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Os erros que esta pĆ”gina cita sĆ£o reais: a própria suĆ­te de testes do SDK reproduz cada um deles. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Isto nĆ£o Ć© um erro do MCP. Ɖ ruĆ­do do anyio, e o seu erro de verdade Ć© a ** ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Duas coisas a fazer com isso: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ Entre nele. `__aenter__` Ć© a conexĆ£o: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -O servidor nĆ£o reconhece o `Mcp-Session-Id` que o seu cliente enviou, quase sempre porque o servidor **reiniciou** (ou vocĆŖ foi roteado para uma instĆ¢ncia diferente). As sessƵes vivem na memória daquele Ćŗnico processo. +O servidor nĆ£o reconhece o `Mcp-Session-Id` que o seu cliente enviou. Ou o servidor **reiniciou** (ou vocĆŖ foi roteado para uma instĆ¢ncia diferente), ou a sessĆ£o **expirou** porque nada estava em andamento durante o `session_idle_timeout`, que Ć© de 30 minutos por padrĆ£o. Veja [Tempo de vida e limites da sessĆ£o](run/legacy-clients.md#session-lifetime-and-limits). As sessƵes vivem na memória daquele Ćŗnico processo. NĆ£o hĆ” bug de servidor para encontrar. A resposta HTTP Ć© um `404` cujo corpo *Ć©* JSON-RPC, entĆ£o, ao contrĆ”rio do `421` acima, o `Client` python mostra esta aqui palavra por palavra: @@ -261,9 +267,9 @@ NĆ£o hĆ” bug de servidor para encontrar. A resposta HTTP Ć© um `404` cujo corpo A correção Ć© reconectar: saia do bloco `async with Client(...)` e entre em um novo, que negocia uma sessĆ£o nova. Para um cliente de vida longa, isso significa capturar `MCPError` em volta das suas chamadas e reconectar ao ver esta mensagem, em vez de tentar de novo dentro de uma sessĆ£o morta. -Se isso acontece *sem* um reinĆ­cio, vocĆŖ estĆ” rodando mais de um worker sem sticky sessions: cada worker mantĆ©m a própria tabela de sessƵes, entĆ£o uma requisição roteada para o errado cai aqui. **[Deploy e escala](run/deploy.md)** e **[Atendendo clientes legados](run/legacy-clients.md)** sĆ£o donos dessa história e das suas duas correƧƵes (roteamento sticky, ou `stateless_http=True`). +Se isso acontece *sem* um reinĆ­cio e sem que o cliente tenha ficado quieto por tanto tempo, vocĆŖ estĆ” rodando mais de um worker sem sticky sessions: cada worker mantĆ©m a própria tabela de sessƵes, entĆ£o uma requisição roteada para o errado cai aqui. **[Deploy e escala](run/deploy.md)** e **[Atendendo clientes legados](run/legacy-clients.md)** sĆ£o donos dessa história e das suas duas correƧƵes (roteamento sticky, ou `stateless_http=True`). -Para quem opera o servidor, a linha de log correspondente Ć© `Rejected request with unknown or expired session ID: `. Ela Ć© registrada em `INFO`, entĆ£o Ć© invisĆ­vel no limite usual de `WARNING`. VĆŖ-la em rajadas logo depois de um deploy Ć© normal; todo cliente conectado estĆ” reconectando. +Para quem opera o servidor, a linha de log correspondente Ć© `Rejected request with unknown or expired session ID: `. Ela Ć© registrada em `INFO`, entĆ£o Ć© invisĆ­vel no limite usual de `WARNING`. VĆŖ-la em rajadas logo depois de um deploy Ć© normal; todo cliente conectado estĆ” reconectando. Quando, em vez disso, a sessĆ£o expirou, essa linha vem precedida de `Session idle timeout`, tambĆ©m em `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ Uma coisa **nĆ£o** produz este erro, apesar de ser uma requisição que o protoc Seu servidor quer perguntar algo ao usuĆ”rio, e este cliente nunca disse que pode receber perguntas. -Um resolvedor de elicitação (elicitation) recusa logo de inĆ­cio quando o cliente conectado nĆ£o declarou elicitação por formulĆ”rio, e `e.error.data` nomeia exatamente o que falta: +Este Bistro pergunta antes de reservar, por meio de um resolvedor: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Sirva-o no lugar do servidor Weather e chame `book_table` a partir de um cliente que nĆ£o passou nenhum `elicitation_callback`. O resolvedor recusa logo de inĆ­cio, porque o cliente conectado nunca declarou elicitação (elicitation) por formulĆ”rio, e `e.error.data` nomeia exatamente o que falta: ```json { @@ -289,7 +301,7 @@ Passe `elicitation_callback=` para `Client(...)`. Registrar o callback *Ć©* a de ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ VocĆŖ vĆŖ esta a partir de `ctx.elicit()` em uma conexĆ£o legada, e em qualquer Seu handler tentou alcanƧar o cliente no meio da requisição, em uma conexĆ£o cuja chamada nĆ£o tem canal capaz de carregar uma requisição vinda do servidor. HĆ” trĆŖs configuraƧƵes de servidor que colocam uma chamada nessa situação. -**Uma conexĆ£o `2026-07-28`: qualquer transporte, sempre.** O protocolo moderno nĆ£o tem nenhuma requisição iniciada pelo servidor, entĆ£o o servidor recusa antes que qualquer coisa seja enviada. `ctx.elicit()` dentro de uma ferramenta Ć© o jeito clĆ”ssico de topar com isso (logo no primeiro teste em memória, jĆ” que `Client(server)` negocia `2026-07-28` sem que ninguĆ©m peƧa), e passar `elicitation_callback=` nĆ£o muda nada, porque nenhuma requisição chega ao cliente para ele responder: +**Uma conexĆ£o `2026-07-28`: qualquer transporte, sempre.** O protocolo moderno nĆ£o tem nenhuma requisição iniciada pelo servidor, entĆ£o o servidor recusa antes que qualquer coisa seja enviada. `ctx.elicit()` dentro de uma ferramenta Ć© o jeito clĆ”ssico de topar com isso, em geral logo no primeirĆ­ssimo **[teste](get-started/testing.md)** em memória dessa ferramenta, jĆ” que `Client(mcp)` negocia `2026-07-28` sem que ninguĆ©m peƧa. Passar `elicitation_callback=` nĆ£o muda nada, porque nenhuma requisição chega ao cliente para ele responder: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ O servidor nĆ£o conseguiu verificar o token `requestState` que o seu cliente dev ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` no log do servidor Ć© o Ćŗnico sinal de que duas ferramentas com o mesmo nome viraram uma só. * Um 421, trĆŖs grafias: `Server returned an error response` (o `Client` python), `421 Misdirected Request` / `Invalid Host header` (todo o resto), `Invalid Host header: ` (o log do servidor). Correção: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> um app montado cujo lifespan do host nunca entrou em `mcp.session_manager.run()`. -* `Session not found` -> o servidor reiniciou; reconecte. +* `Session not found` -> o servidor reiniciou ou a sessĆ£o expirou (`session_idle_timeout`); reconecte. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` precisa de um canal do servidor para o cliente: uma conexĆ£o `2026-07-28` nunca tem um, `stateless_http=True` tira o legado, e `json_response=True` tira o de escopo de requisição. Use um resolvedor (um cliente legado tambĆ©m precisa de um servidor que mantenha o canal). O vizinho `Method not found` Ć© uma requisição para um mĆ©todo que a revisĆ£o do protocolo do outro lado nĆ£o tem. * `Client did not declare the form elicitation capability ...` e `Elicitation not supported` -> falta `elicitation_callback=` no cliente. * `Invalid or expired requestState` nunca diz o porquĆŖ na rede. O log do servidor diz; `unknown key` significa compartilhar `RequestStateSecurity(keys=[...])` entre os workers. diff --git a/i18n/pt/pages/whats-new.md b/i18n/pt/pages/whats-new.md index 6957e001..8c68702e 100644 --- a/i18n/pt/pages/whats-new.md +++ b/i18n/pt/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # O que hĆ” de novo na v2 {#whats-new-in-v2} @@ -43,11 +43,11 @@ Nem tudo de que uma ferramenta (tool) precisa deve vir do modelo. Novidade na v2 A v1 entregava trĆŖs camadas aninhadas: um gerenciador de contexto de transporte que produzia streams brutos, uma `ClientSession` em volta deles e um `await session.initialize()` chamado Ć  mĆ£o. A v2 tem um objeto só: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` recebe um objeto de servidor (em memória, sem transporte: Ć© o cenĆ”rio dos testes), uma URL (Streamable HTTP), um `StdioServerParameters` (um subprocesso stdio) ou qualquer outro gerenciador de contexto de transporte, como `sse_client(...)`. Entrar no `async with` conecta e negocia a versĆ£o do protocolo, seja qual for a era que o servidor fale; `client.server_capabilities` e `client.protocol_version` simplesmente estĆ£o lĆ” depois disso, e `client.server_info` tambĆ©m, quando o servidor se identifica (agora ele Ć© `Implementation | None`, jĆ” que na era 2026 a identidade Ć© opcional). Os callbacks de amostragem e de elicitação que vocĆŖ registrou na v1 continuam funcionando (o corpo deles passa pela mesma renomeação de atributos para snake_case que todo o resto desta pĆ”gina), agora tambĆ©m respondem Ć s requisiƧƵes-dentro-de-resultados no estilo 2026 (abaixo), e rodam de forma concorrente em vez de um por vez. `ClientSession` continua por baixo para quem quer a superfĆ­cie de baixo nĆ­vel, e `client.session` a entrega para vocĆŖ; ela tambĆ©m mudou (roda sobre o novo motor de dispatcher, e algumas das próprias assinaturas dela mudaram), entĆ£o leia o **[Guia de migração](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de descer de nĆ­vel. +`Client` recebe uma URL (Streamable HTTP), um `StdioServerParameters` (um subprocesso stdio), qualquer outro gerenciador de contexto de transporte, como `sse_client(...)`, ou, nos testes, o próprio objeto do servidor (em memória, sem transporte). Entrar no `async with` conecta e negocia a versĆ£o do protocolo, seja qual for a era que o servidor fale; `client.server_capabilities` e `client.protocol_version` simplesmente estĆ£o lĆ” depois disso, e `client.server_info` tambĆ©m, quando o servidor se identifica (agora ele Ć© `Implementation | None`, jĆ” que na era 2026 a identidade Ć© opcional). Os callbacks de amostragem e de elicitação que vocĆŖ registrou na v1 continuam funcionando (o corpo deles passa pela mesma renomeação de atributos para snake_case que todo o resto desta pĆ”gina), agora tambĆ©m respondem Ć s requisiƧƵes-dentro-de-resultados no estilo 2026 (abaixo), e rodam de forma concorrente em vez de um por vez. `ClientSession` continua por baixo para quem quer a superfĆ­cie de baixo nĆ­vel, e `client.session` a entrega para vocĆŖ; ela tambĆ©m mudou (roda sobre o novo motor de dispatcher, e algumas das próprias assinaturas dela mudaram), entĆ£o leia o **[Guia de migração](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** antes de descer de nĆ­vel. **[O Client](client/index.md)** o apresenta, **[Transportes do cliente](client/transports.md)** cobre as quatro formas de conexĆ£o, **[Callbacks do cliente](client/callbacks.md)** cobre os callbacks em si, e **[Testes](get-started/testing.md)** mostra o padrĆ£o em memória que substitui o helper `create_connected_server_and_client_session()` da v1. @@ -172,11 +172,15 @@ Toda requisição iniciada pelo servidor sumiu na 2026-07-28: elicitação por p A substituição inverte a chamada. Uma ferramenta que precisa de algo do usuĆ”rio *retorna* a pergunta (`InputRequiredResult`), o cliente a responde com os mesmos callbacks que sempre teve, e a chamada Ć© repetida com as respostas anexadas. `Client` conduz esse loop para vocĆŖ. No servidor vocĆŖ raramente monta o resultado por conta própria, porque uma **[dependĆŖncia](handlers/dependencies.md)** faz isso: anote um parĆ¢metro com `Resolve(ask_quantity)`, onde `ask_quantity` Ć© uma função comum que vocĆŖ escreve, e o SDK pergunta pelo mecanismo que a conexĆ£o suportar, uma requisição de elicitação ao vivo em uma sessĆ£o legada ou um multi-round-trip na 2026. Um corpo de ferramenta, as duas eras: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Esse arquivo Ć© a proposta inteira em um lugar só: um servidor, uma ferramenta apoiada em `Resolve`, e um cliente legado mais um cliente moderno, os dois recebendo a sua resposta, em memória. **[RequisiƧƵes multi-round-trip](handlers/multi-round-trip.md)** explica o mecanismo (incluindo o `request_state`, que o SDK sela e verifica para vocĆŖ); **[Elicitação](handlers/elicitation.md)** cobre a parte de perguntar. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Esses dois arquivos sĆ£o a proposta inteira: um servidor, uma ferramenta apoiada em `Resolve`, e um cliente legado mais um cliente moderno, os dois recebendo a sua resposta do mesmo servidor em execução (**[Servindo clientes legados](run/legacy-clients.md)** percorre os dois). **[RequisiƧƵes multi-round-trip](handlers/multi-round-trip.md)** explica o mecanismo (incluindo o `request_state`, que o SDK sela e verifica para vocĆŖ); **[Elicitação](handlers/elicitation.md)** cobre a parte de perguntar. !!! warning "Este Ć© o Ćŗnico lugar em que um servidor v1 portado muda de comportamento" Os seus próprios testes esbarram nisso primeiro: `Client(mcp)` negocia 2026-07-28 com o seu diff --git a/i18n/ru/pages/advanced/apps.md b/i18n/ru/pages/advanced/apps.md index c4de3c2d..772437d0 100644 --- a/i18n/ru/pages/advanced/apps.md +++ b/i18n/ru/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ translation: ## Часы с циферблатом {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ translation: МоГель читает `content`; iframe — Š“Š»Ń Š»ŃŽŠ“ŠµŠ¹. Єост с поГГержкой UI всё равно переГаёт текстовый Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ моГели, а чисто текстовый клиент ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ *Ń‚Š¾Š»ŃŒŠŗŠ¾* его. ŠŸŠ¾ŃŃ‚Š¾Š¼Ńƒ канонический паттерн — оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, Гва ответа. Š’Š·Š³Š»ŃŠ½ŠøŃ‚Šµ на `get_time` ещё раз: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` возвращает `True`, Ń‚Š¾Š»ŃŒŠŗŠ¾ когГа клиент Š¾Š±ŃŠŃŠ²ŠøŠ» Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ `io.modelcontextprotocol/ui` **Šø** указал `text/html;profile=mcp-app` в настройке `mimeTypes`. Поле Š¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š¾Šµ, так что клиент, который его Š¾ŠæŃƒŃŃ‚ŠøŠ», не ŃŃ‡ŠøŃ‚Š°ŠµŃ‚ŃŃ. Именно ŃŃ‚Š¾ Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ `main()` в том же файле: ŠŗŠ»ŠøŠµŠ½Ń‚ŃŠŗŃƒŃŽ половину ŃŠ¾Š³Š»Š°ŃŠ¾Š²Š°Š½ŠøŃ — Šø в ответ прихоГит Ń€Š°Š·Š²Ń‘Ń€Š½ŃƒŃ‚Ń‹Š¹ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. +`client_supports_apps(ctx)` возвращает `True`, Ń‚Š¾Š»ŃŒŠŗŠ¾ когГа клиент Š¾Š±ŃŠŃŠ²ŠøŠ» Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ `io.modelcontextprotocol/ui` **Šø** указал `text/html;profile=mcp-app` в настройке `mimeTypes`. Поле Š¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š¾Šµ, так что клиент, который его Š¾ŠæŃƒŃŃ‚ŠøŠ», не ŃŃ‡ŠøŃ‚Š°ŠµŃ‚ŃŃ. Вот ŠŗŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ°Ń половина ŃŠ¾Š³Š»Š°ŃŠ¾Š²Š°Š½ŠøŃ: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `server.py` по HTTP, затем во втором терминале Š·Š°ŠæŃƒŃŃ‚ите клиент: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +ŠŸŃ€ŠøŃˆŃ‘Š» Ń€Š°Š·Š²Ń‘Ń€Š½ŃƒŃ‚Ń‹Š¹ ответ. Уберите `extensions=[APPS_SUPPORT]` ŠøŠ· вызова `Client` — Šø та же программа напечатает `The time is 2026-06-26T12:00:00Z.`: ŃŃ‚Š¾ всё, что когГа-либо ŃƒŠ²ŠøŠ“ŠøŃ‚ чисто текстовый клиент. !!! warning ŠŠøŠŗŠ¾Š³Š“Š° не возвращайте заглушку вроГе `"[Rendered UI]"` в качестве еГинственного соГержимого. Если запасной текст бесполезен, ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ бесполезен Š“Š»Ń Š»ŃŽŠ±Š¾Š³Š¾ текстового клиента Šø Š“Š»Ń самой моГели. ŠŠ°ŠæŠøŃˆŠøŃ‚Šµ Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾Šµ преГложение. diff --git a/i18n/ru/pages/advanced/extensions.md b/i18n/ru/pages/advanced/extensions.md index a2d23367..688c4a2d 100644 --- a/i18n/ru/pages/advanced/extensions.md +++ b/i18n/ru/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Š Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string Дамое маленькое полезное Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ — оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ Šø карта настроек: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` — значение, Š¾Š±ŃŠŃŠ²Š»ŃŠµŠ¼Š¾Šµ в `capabilities.extensions["com.example/stamps"]`. Верните `{}` (значение по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ), чтобы Š¾Š±ŃŠŃŠ²ŠøŃ‚ŃŒ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ без настроек. * Š Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ никогГа не ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ сервер. ŠžŠ½Š¾ описывает свой вклаГ как Ганные; `MCPServer` ŠøŃ… ŠæŠ¾Ń‚Ń€ŠµŠ±Š»ŃŠµŃ‚. ŠŠøŠŗŠ°ŠŗŠ¾Š³Š¾ `self.server`, который можно было бы Š¼ŠµŠ½ŃŃ‚ŃŒ, нет. -А `main()` ŃŠ»ŃƒŠ¶ŠøŃ‚ Š“Š¾ŠŗŠ°Š·Š°Ń‚ŠµŠ»ŃŒŃŃ‚Š²Š¾Š¼: клиент в ŠæŠ°Š¼ŃŃ‚Šø, ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Š¹ Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ Šŗ `mcp`: +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ его по HTTP — Šø Š“Š¾ŠŗŠ°Š·Š°Ń‚ŠµŠ»ŃŒŃŃ‚Š²Š¾Š¼ ŠæŠ¾ŃŠ»ŃƒŠ¶ŠøŃ‚ клиент: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +ŠšŠ°Š¶Š“Ń‹Š¹ `server.py` на ŃŃ‚Š¾Š¹ странице Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ся ŃŃ‚Š¾Š¹ команГой, а кажГый `client.py` работает Ń€ŃŠ“Š¾Š¼ с ним: `python client.py` во втором терминале. + ### ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ собственных метоГов {#serving-your-own-methods} Š Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ может Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€Š¾Š²Š°Ń‚ŃŒ **новые метоГы запросов** — собственные глаголы, Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŠ¼Ń‹Šµ Ń€ŃŠ“Š¾Š¼ с метоГами спецификации: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ°Ń сторона {#the-client-side} -`main()` ŠøŠ· того же файла — ŃŃ‚Š¾ Š²ŃŃ ŠŗŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ°Ń Ń‡Š°ŃŃ‚ŃŒ, обе её половины: +ŠšŠ»ŠøŠµŠ½Ń‚ — Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š°Ń программа, Šø в ней обе половины клиентской части: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ. ŠžŠ±ŃŠŃŠ²Š»ŠµŠ½ŠøŃ ŠæŃ€ŠµŠ²Ń€Š°Ń‰Š°ŃŽŃ‚ŃŃ в `ClientCapabilities.extensions`: на ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø версии 2026-07-28 карта ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ в конверте `_meta` кажГого запроса, так что сервер виГит её в **кажГом** запросе; на ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ она еГет в Ń€ŃƒŠŗŠ¾ŠæŠ¾Š¶Š°Ń‚ŠøŠø `initialize`. Š”ŠµŃ€Š²ŠµŃ€Š½Š¾Š¼Ńƒ коГу всё равно, какой ŠøŠ· вариантов: `require_client_extension(ctx, ...)` Šø `ctx.session.check_client_capability(...)` Ń‡ŠøŃ‚Š°ŃŽŃ‚ Š½ŃƒŠ¶Š½Ń‹Š¹ источник на обоих ŠæŃƒŃ‚ŃŃ…. * ВенГорные метоГы ŃŠæŃƒŃŠŗŠ°ŃŽŃ‚ŃŃ на ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ ниже, Šŗ `client.session.send_request(...)`; `Client` Š¾Š±Š·Š°Š²Š¾Š“ŠøŃ‚ŃŃ полноценными метоГами Ń‚Š¾Š»ŃŒŠŗŠ¾ Š“Š»Ń глаголов спецификации. `send_request` принимает Š»ŃŽŠ±Š¾Š¹ поГкласс `Request`, так что венГорный запрос прохоГит как ŠµŃŃ‚ŃŒ. +* `SearchRequest` Šø Гве моГели, которые он несёт, — ŃŃ‚Š¾ контракт Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ в переГаваемых Ганных, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ клиент Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ ŠøŃ… у ŃŠµŠ±Ń сам. ŠžŠæŃƒŠ±Š»ŠøŠŗŠ¾Š²Š°Š½Š½Š¾Šµ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ ŠæŠ¾ŃŃ‚Š°Š²Š»ŃŠ»Š¾ бы ŠøŃ… в пакете, который ŠøŠ¼ŠæŠ¾Ń€Ń‚ŠøŃ€ŃƒŃŽŃ‚ обе стороны. ### ŠŸŠµŃ€ŠµŃ…Š²Š°Ń‚ `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## Использование клиентского Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ {#using-a-client-extension} -**ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ¾Šµ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ** — тот же контракт со стороны ŠæŠ¾Ń‚Ń€ŠµŠ±ŠøŃ‚ŠµŠ»Ń: набор клиентского ŠæŠ¾Š²ŠµŠ“ŠµŠ½ŠøŃ за оГним иГентификатором. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Ń‹ в `Client(extensions=[...])` Šø вызывайте ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ как обычно: +**ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ¾Šµ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ** — тот же контракт со стороны ŠæŠ¾Ń‚Ń€ŠµŠ±ŠøŃ‚ŠµŠ»Ń: набор клиентского ŠæŠ¾Š²ŠµŠ“ŠµŠ½ŠøŃ за оГним иГентификатором. Дервер зГесь отвечает на `buy` не самим товаром, а квитанцией, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ нужно ŠæŠ¾Š³Š°ŃŠøŃ‚ŃŒ, — Šø Ń‚Š¾Š»ŃŒŠŗŠ¾ ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ, Š¾Š±ŃŠŃŠ²ŠøŠ²ŃˆŠµŠ¼Ńƒ Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +ŠŠ° клиенте переГайте ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Ń‹ в `Client(extensions=[...])` Šø вызывайте ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ как обычно: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` возвращает обычный `CallToolResult`, как Šø Š»ŃŽŠ±Š¾Š¹ Š“Ń€ŃƒŠ³Š¾Š¹ вызов. Что изменило Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ: Ń‚ŠµŠæŠµŃ€ŃŒ сервер может Š¾Ń‚Š²ŠµŃ‚ŠøŃ‚ŃŒ на `buy` **формой Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š°** `receipt` вместо Š¾ŠŗŠ¾Š½Ń‡Š°Ń‚ŠµŠ»ŃŒŠ½Š¾Š³Š¾ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š°, а `Receipts` ГовоГит её Го конца (зГесь — ŠæŠ¾Š³Š°ŃˆŠ°Ń ŠŗŠ²ŠøŃ‚Š°Š½Ń†ŠøŃŽ Š“Š¾ŠæŠ¾Š»Š½ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Š¼ вызовом) Го того, как `call_tool` вернёт ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ. Š’ месте вызова не Š¼ŠµŠ½ŃŠµŃ‚ся ничего. Уберите Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ — Šø ничего ŃŃ‚Š¾Š³Š¾ не Š±ŃƒŠ“ет: ŠæŃ€Š¾ŠæŃƒŃŠŗŠ½Š¾Š¹ ŠæŃƒŠ½ŠŗŃ‚ сервера отклонит клиент, который его не Š¾Š±ŃŠŃŠ²ŠøŠ» (ошибка -32021), а Š·Š°ŃŠ²Š»ŠµŠ½Š½Š°Ń форма от сервера, ŠæŃ€Š¾ŠæŃƒŃŠŗŠ°ŃŽŃ‰ŠµŠ³Š¾ эту ŠæŃ€Š¾Š²ŠµŃ€ŠŗŃƒ, не пройГёт Š²Š°Š»ŠøŠ“Š°Ń†ŠøŃŽ — ровно так, как ŃŠæŠµŃ†ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ Ń‚Ń€ŠµŠ±ŃƒŠµŃ‚ Š“Š»Ń нераспознанного `resultType`. Š’Ń‹ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ, на обоих концах ŃŠ¾ŠµŠ“ŠøŠ½ŠµŠ½ŠøŃ. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ŠŠ°ŠæŠøŃŠ°Š½ŠøŠµ клиентского Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ {#writing-a-client-extension} Š£Š½Š°ŃŠ»ŠµŠ“ŃƒŠ¹Ń‚ŠµŃŃŒ от `ClientExtension` Šø переопреГелите Ń‚Š¾Š»ŃŒŠŗŠ¾ то, что нужно. Три виГа вклаГа, у кажГого Ń€ŠµŠ°Š»ŠøŠ·Š°Ń†ŠøŃ по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ: `settings()`, `claims()` Šø `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * Š˜Š“ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń‚Š¾Ń€ ŠæŠ¾Š“Ń‡ŠøŠ½ŃŠµŃ‚ŃŃ той же грамматике, что Šø на сервере, Šø ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ŃŃ при опреГелении класса. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Глаголы Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ {#extension-verbs} -Добственные метоГы запросов Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ не Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ регистрации на стороне клиента. Тип венГорного запроса Š½Š°ŃŠ»ŠµŠ“ŃƒŠµŃ‚ŃŃ от `mcp.types.Request` Šø Š¾Ń‚ŠæŃ€Š°Š²Š»ŃŠµŃ‚ŃŃ через `client.session.send_request`, как в разГеле [ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ собственных метоГов](#serving-your-own-methods). ŠžŠ“Š½Š¾ Гополнение: когГа ŠŗŠ»ŃŽŃ‡ ŠøŠ· params Голжен ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŃŒŃŃ в заголовке `Mcp-Name` (спецификации Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠ¹, например tasks, Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ ŃŃ‚Š¾Š³Š¾ Š“Š»Ń своих глаголов), тип запроса Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ `name_param`: +Добственные метоГы запросов Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŃ не Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ регистрации на стороне клиента. Тип венГорного запроса Š½Š°ŃŠ»ŠµŠ“ŃƒŠµŃ‚ŃŃ от `mcp.types.Request` Šø Š¾Ń‚ŠæŃ€Š°Š²Š»ŃŠµŃ‚ŃŃ через `client.session.send_request`, как в разГеле [ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ собственных метоГов](#serving-your-own-methods). Š’Š¾Š·ŃŒŠ¼Ń‘Š¼ сервер, Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ которого Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ оГин глагол, Š¾Ń‚Š½Š¾ŃŃŃ‰ŠøŠ¹ŃŃ Šŗ именованному Š·Š°Š“Š°Š½ŠøŃŽ: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +ŠžŠ“Š½Š¾ Гополнение на клиенте: когГа ŠŗŠ»ŃŽŃ‡ ŠøŠ· params Голжен ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŃŒŃŃ в заголовке `Mcp-Name` (спецификации Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠ¹, например tasks, Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ ŃŃ‚Š¾Š³Š¾ Š“Š»Ń своих глаголов), тип запроса Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Š”ŠµŃŃŠøŃ Š“ŃƒŠ±Š»ŠøŃ€ŃƒŠµŃ‚ `params["jobId"]` в `Mcp-Name` на кажГом ŠæŃƒŃ‚Šø отправки, а Š¾Ń‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŃŽŃ‰ŠµŠµ значение привоГит Šŗ ŃŠ²Š½Š¾Š¹ ошибке, а не Šŗ Š¼Š¾Š»Ń‡Š°Š»ŠøŠ²Š¾Š¼Ńƒ ŠæŃ€Š¾ŠæŃƒŃŠŗŃƒ Š¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š¾Š³Š¾ заголовка. ## Чего Ń€Š°ŃŃˆŠøŃ€ŠµŠ½ŠøŠµ не может {#what-an-extension-cannot-do} diff --git a/i18n/ru/pages/advanced/low-level-server.md b/i18n/ru/pages/advanced/low-level-server.md index a982fb0f..2bd34e60 100644 --- a/i18n/ru/pages/advanced/low-level-server.md +++ b/i18n/ru/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # ŠŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Ń‹Š¹ Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### ŠŸŠ¾ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ сами {#try-it} -Inspector зГесь не поможет: `mcp dev` Šø `mcp run` ŠæŃ€ŠøŠ½ŠøŠ¼Š°ŃŽŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ `MCPServer`. ŠšŠ»ŠøŠµŠ½Ń‚Ńƒ `Client`, Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠµŠ¼Ńƒ в ŠæŠ°Š¼ŃŃ‚Šø, всё равно — он принимает Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Ń‹Š¹ `Server` точно так же, как `MCPServer`: +`mcp dev` Šø `mcp run` ŠæŃ€ŠøŠ½ŠøŠ¼Š°ŃŽŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ `MCPServer`, так что ŃŃ‚Š¾Ń‚ сервер вы Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚Šµ сами. ŠŸŠ¾ŃŠ»ŠµŠ“Š½ŃŃ строка `server.py` строит ŠøŠ· него обычное ASGI-приложение, а uvicorn его Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +ŠŠ°ŠæŃ€Š°Š²ŃŒŃ‚Šµ Inspector или Š»ŃŽŠ±Š¾Š¹ клиент на `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` равен `None`. Š’Ń‹ŃŠ¾ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Ń‹Š¹ сервер сам оборачивает `-> str` в `{"result": ...}`; зГесь никто не соберёт то, чего не собрали вы. * `list_tools` возвращает ŃŃ…ŠµŠ¼Ńƒ, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ набрали **вы**, символ в символ. Š’ Š²Ń‹ŃŠ¾ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š¾Š¹ версии у кажГого свойства было `"title": "Query"`, а в корне — `"title": "search_booksArguments"`: артефакты Pydantic. Š—Š“ŠµŃŃŒ всё, что ŠµŃŃ‚ŃŒ в переГаваемых Ганных, положили Ń‚ŃƒŠ“Š° вы. +Š’ тесте uvicorn Šø порт не Š½ŃƒŠ¶Š½Ń‹: `Client(server)` принимает Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Ń‹Š¹ `Server` Š²Š½ŃƒŃ‚Ń€Šø процесса точно так же, как `MCPServer`, Šø именно ŃŃ‚Š¾Ń‚ поГхоГ описан на странице **[Тестирование](../get-started/testing.md)**. + ## За вас ничего не ŠæŃ€Š¾Š²ŠµŃ€ŃŃŽŃ‚ {#nothing-is-checked-for-you} `MCPServer` Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ некорректный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ещё Го запуска вашей Ń„ŃƒŠ½ŠŗŃ†ŠøŠø, ŠæŃ€Š¾Š²ŠµŃ€ŃŃ вызов по сгенерированной им схеме (**[Š˜Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ Š»ŃŽŠ±Š¾Š¹ метоГ. `initialize` зарезервирован. * Возможности, которые Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ `Server`, Š²Ń‹Š²Š¾Š“ŃŃ‚ŃŃ ŠøŠ· того, какие обработчики вы зарегистрировали. -`Client(server)` Š¾Š±Ń€Š°Ń‰Š°Š»ŃŃ с обоими серверами оГинаково, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что ŃŃ‚Š¾ *Šø ŠµŃŃ‚ŃŒ* оГин Šø тот же протокол — в ŃŃ‚Š¾Š¼ весь смысл. Š”Š»ŠµŠ“ŃƒŃŽŃ‰ŠøŠ¹ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ вниз — вообще не класс: ŃŃ‚Š¾ **[Middleware](middleware.md)**. +ŠšŠ»ŠøŠµŠ½Ń‚ Š¾Š±Ń€Š°Ń‰Š°Š»ŃŃ с обоими серверами оГинаково, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что ŃŃ‚Š¾ *Šø ŠµŃŃ‚ŃŒ* оГин Šø тот же протокол — в ŃŃ‚Š¾Š¼ весь смысл. Š”Š»ŠµŠ“ŃƒŃŽŃ‰ŠøŠ¹ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ вниз — вообще не класс: ŃŃ‚Š¾ **[Middleware](middleware.md)**. diff --git a/i18n/ru/pages/advanced/pagination.md b/i18n/ru/pages/advanced/pagination.md index adc35963..dba7d7bd 100644 --- a/i18n/ru/pages/advanced/pagination.md +++ b/i18n/ru/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # ŠŸŠ°Š³ŠøŠ½Š°Ń†ŠøŃ {#pagination} @@ -31,9 +31,13 @@ translation: ### ŠŸŠ¾ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ сами {#try-it} -`Client(server)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š¾Š¼Ńƒ `Server` в ŠæŠ°Š¼ŃŃ‚Šø точно так же, как Šŗ `MCPServer`. +`mcp run` принимает Ń‚Š¾Š»ŃŒŠŗŠ¾ `MCPServer`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŃŃ‚Š¾Ń‚ сервер ŠæŃ€ŠøŠ“Ń‘Ń‚ŃŃ Š·Š°ŠæŃƒŃŠŗŠ°Ń‚ŃŒ ŃŠ°Š¼Š¾ŃŃ‚Š¾ŃŃ‚ŠµŠ»ŃŒŠ½Š¾. ŠŸŠ¾ŃŠ»ŠµŠ“Š½ŃŃ строка `server.py` собирает ŠøŠ· `Server` обычное ASGI-приложение, Šø его Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ uvicorn: -Вызовите `list_resources()` без Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ов. ŠŸŃ€ŠøŠ“ŃƒŃ‚ Š“ŠµŃŃŃ‚ŃŒ Ń€ŠµŃŃƒŃ€ŃŠ¾Š², от `book-1` Го `book-10`, а `next_cursor` Š±ŃƒŠ“ŠµŃ‚ строкой `"10"`. +```console +uvicorn server:app --port 8000 +``` + +ŠŠ°ŠæŃ€Š°Š²ŃŒŃ‚Šµ Š»ŃŽŠ±Š¾Š¹ клиент (**[ŠšŠ»ŠøŠµŠ½Ń‚](../client/index.md)** или Inspector) на `http://localhost:8000/mcp` Šø вызовите `list_resources()` без Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ов. ŠŸŃ€ŠøŠ“ŃƒŃ‚ Š“ŠµŃŃŃ‚ŃŒ Ń€ŠµŃŃƒŃ€ŃŠ¾Š², от `book-1` Го `book-10`, а `next_cursor` Š±ŃƒŠ“ŠµŃ‚ строкой `"10"`. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ его обратно через `list_resources(cursor="10")` — первым Ń€ŠµŃŃƒŃ€ŃŠ¾Š¼ Š¾ŠŗŠ°Š¶ŠµŃ‚ŃŃ `book-11`, а новый `next_cursor` Š±ŃƒŠ“ŠµŃ‚ `"20"`. @@ -43,7 +47,7 @@ translation: ŠšŠ°Š¶Š“Ń‹Š¹ метоГ `list_*` у `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) принимает именованный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `cursor=`. Š’Ń‹Ń‡ŠøŃ‚Š°Ń‚ŃŒ список постранично целиком — ŃŃ‚Š¾ оГин `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ translation: * Š”Š¾Š±Š°Š²Š»ŃŠ¹Ń‚Šµ ŃŠ»ŠµŠ¼ŠµŠ½Ń‚Ń‹ **Го** того, как ŃŠ¼Š¾Ń‚Ń€ŠµŃ‚ŃŒ на `next_cursor`: на послеГней странице тоже ŠµŃŃ‚ŃŒ Ń€ŠµŃŃƒŃ€ŃŃ‹. * `next_cursor is None` — условие выхоГа. Всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ без изменений Š¾Ń‚ŠæŃ€Š°Š²Š»ŃŠµŃ‚ŃŃ обратно в `cursor=`. -Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ его `main()`, Šø он напечатает `100 resources`: Š“ŠµŃŃŃ‚ŃŒ страниц по Š“ŠµŃŃŃ‚ŃŒ, ŃŃˆŠøŃ‚Ń‹Ń… циклом, который Šø не знал, что страниц было Š“ŠµŃŃŃ‚ŃŒ. +Пока uvicorn проГолжает Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Ń‚ŃŒ `server.py`, Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `python client.py` во втором терминале. ŠžŠ½ напечатает `100 resources`: Š“ŠµŃŃŃ‚ŃŒ страниц по Š“ŠµŃŃŃ‚ŃŒ, ŃŃˆŠøŃ‚Ń‹Ń… циклом, который Šø не знал, что страниц было Š“ŠµŃŃŃ‚ŃŒ. Это тот же цикл, который **[ŠšŠ»ŠøŠµŠ½Ń‚](../client/index.md)** показывает Š“Š»Ń кажГого метоГа `list_*`, Šø против сервера без пагинации он ничего не стоит: `next_cursor` равен `None` уже в первом ответе, Šø цикл Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ся оГин раз. diff --git a/i18n/ru/pages/client/caching.md b/i18n/ru/pages/client/caching.md index f23dbc7b..fa35cdcc 100644 --- a/i18n/ru/pages/client/caching.md +++ b/i18n/ru/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # ПоГсказки по ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½ŠøŃŽ {#caching-hints} @@ -30,7 +30,7 @@ translation: Š’ Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š¾Š¼ классе `Server` обработчики ŃŠ¾Š±ŠøŃ€Š°ŃŽŃ‚ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń‹ Š²Ń€ŃƒŃ‡Š½ŃƒŃŽ, а `ttl_ms` Šø `cache_scope` — ŃŃ‚Š¾ просто ŠæŠ¾Š»Ń моГелей Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š°. ŠžŠ±Ń€Š°Š±Š¾Ń‚Ń‡ŠøŠŗ, который заГаёт ŠøŃ… ŃŠ²Š½Š¾, всегГа берёт верх наГ словарём ŠøŠ· ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, поле за полем: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: Š’ сессии 2026-07-28 `Client` ŃƒŃ‡ŠøŃ‚Ń‹Š²Š°ŠµŃ‚ поГсказки за вас: у него ŠµŃŃ‚ŃŒ встроенный ŠŗŃŃˆ ответов, Š²ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Š¹ по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ. Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚, ŠæŃ€ŠøŃˆŠµŠ“ŃˆŠøŠ¹ с `ttlMs`, ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ŃŃ, Šø иГентичный вызов в преГелах ŃŃ‚Š¾Š³Š¾ TTL Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ŃŃ ŠøŠ· ŠŗŃŃˆŠ° без Š¾Š±Ń€Š°Ń‰ŠµŠ½ŠøŃ Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ. Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ *без* поГсказки не ŠŗŃŃˆŠøŃ€ŃƒŠµŃ‚ся: Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń‹ без поГсказок ŠæŠ¾Š»ŃƒŃ‡Š°ŃŽŃ‚ `CacheConfig.default_ttl_ms`, по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ равный `0` (ŃŃ€Š°Š·Ńƒ ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠ¹), так что сервер, ничего не Š¾Š±ŃŠŃŠ²Š»ŃŃŽŃ‰ŠøŠ¹, виГит ровно тот же трафик — вызов за вызовом, — что Šø всегГа. -```python title="client.py" hl_lines="33 35 38" +Чтобы ŃƒŠ²ŠøŠ“ŠµŃ‚ŃŒ ŃŃ‚Š¾ в Гействии, Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `server.py` ŠøŠ· ŠæŃ€ŠµŠ“Ń‹Š“ŃƒŃ‰ŠµŠ³Š¾ разГела поГ uvicorn (его ŠæŠ¾ŃŠ»ŠµŠ“Š½ŃŃ строка собирает ASGI-приложение). ŠžŠ±Ń€Š°Š±Š¾Ń‚Ń‡ŠøŠŗ печатает ŃŃ‚Ń€Š¾ŠŗŃƒ кажГый раз, когГа Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ŃŃ: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `python client.py` во втором терминале. ŠžŠ½ напечатает поГсказки, с которыми ŠæŃ€ŠøŃˆŃ‘Š» первый Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚: `ttlMs` обработчика Ń€ŃŠ“Š¾Š¼ с `cacheScope` ŠøŠ· ŃŠ»Š¾Š²Š°Ń€Ń: + +```text +1000 public +``` + +ŠžŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ виГно в терминале сервера: межГу логами запросов uvicorn строка `tools/list served` ŠæŠ¾ŃŠ²Š»ŃŠµŃ‚ŃŃ три раза. + Четыре вызова, три Š¾Š±Ń€Š°Ń‰ŠµŠ½ŠøŃ Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ. Второй вызов Š½Š°ŃˆŃ‘Š» ŃŠ²ŠµŠ¶ŃƒŃŽ запись Šø Го сервера не Š“Š¾ŃˆŃ‘Š»; перевоГ (внеГрённых) часов за преГелы TTL заставил третий снова Š¾Š±Ń€Š°Ń‚ŠøŃ‚ŃŒŃŃ Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ; четвёртый указал `cache_mode="refresh"`. Этот именованный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠµŃŃ‚ŃŒ у ŠæŃŃ‚Šø ŠŗŃŃˆŠøŃ€ŃƒŃŽŃ‰ŠøŃ… метоГов (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ) отГаёт ŃŠ²ŠµŠ¶ŃƒŃŽ запись, если она ŠµŃŃ‚ŃŒ, а если нет — Š·Š°ŠæŃ€Š°ŃˆŠøŠ²Š°ŠµŃ‚ Šø ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. @@ -56,7 +70,7 @@ translation: ŠŠ°Š“ `"use"` стоит оГно правило: **вызовы с `meta` всегГа Š“Š¾Ń…Š¾Š“ŃŃ‚ Го сервера.** Запрос с заГанным `meta` (токен прогресса, ŠæŠ¾Š»Ń трассировки) рассчитывает на Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ сетевой запрос, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ при `cache_mode="use"` он Š¾Š±Ń€Š°Š±Š°Ń‚Ń‹Š²Š°ŠµŃ‚ŃŃ как `"refresh"`: чтение ŠøŠ· ŠŗŃŃˆŠ° ŠæŃ€Š¾ŠæŃƒŃŠŗŠ°ŠµŃ‚ŃŃ, а ŠæŠ¾Š»ŃƒŃ‡ŠµŠ½Š½Ń‹Š¹ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ всё равно Š·Š°Š¼ŠµŠ½ŃŠµŃ‚ Š·Š°ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½Š½ŃƒŃŽ запись. `"bypass"` Šø ŃŠ²Š½Ń‹Š¹ `"refresh"` Š²ŠµŠ“ŃƒŃ‚ ŃŠµŠ±Ń как обычно. -Чтобы совсем Š²Ń‹ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŒ ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½ŠøŠµ, созГайте клиент как `Client(server, cache=None)`: кажГый вызов снова Š¾Š±Ń€Š°Ń‰Š°ŠµŃ‚ся Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ, а `cache_mode`, Ń…Š¾Ń‚Ń Šø ŠæŃ€ŠøŠ½ŠøŠ¼Š°ŠµŃ‚ŃŃ, ничего не Гелает. +Чтобы совсем Š²Ń‹ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŒ ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½ŠøŠµ, переГайте `cache=None` при созГании `Client`: кажГый вызов снова Š¾Š±Ń€Š°Ń‰Š°ŠµŃ‚ся Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ, а `cache_mode`, Ń…Š¾Ń‚Ń Šø ŠæŃ€ŠøŠ½ŠøŠ¼Š°ŠµŃ‚ŃŃ, ничего не Гелает. ŠžŠ±Š»Š°ŃŃ‚ŃŒ тоже ŃƒŃ‡ŠøŃ‚Ń‹Š²Š°ŠµŃ‚ŃŃ автоматически: записи `"private"` ŠæŃ€ŠøŠ²ŃŠ·Š°Š½Ń‹ Šŗ *Ń€Š°Š·Š“ŠµŠ»Ńƒ* (partition) ŠŗŃŃˆŠ° (о нём ниже), тогГа как записи `"public"` Š¼Š¾Š³ŃƒŃ‚ Š±Ń‹Ń‚ŃŒ разГелены ŃˆŠøŃ€Šµ. И **ŃƒŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ важнее TTL** Š“Š»Ń ровно тех записей, которые они Š½Š°Š·Ń‹Š²Š°ŃŽŃ‚: увеГомление `list_changed` Š²Ń‹Ń‚ŠµŃŠ½ŃŠµŃ‚ ŃŠ¾Š¾Ń‚Š²ŠµŃ‚ŃŃ‚Š²ŃƒŃŽŃ‰ŠøŠ¹ Š·Š°ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½Š½Ń‹Š¹ список, а `resources/updated` Š²Ń‹Ń‚ŠµŃŠ½ŃŠµŃ‚ Š·Š°ŠŗŃŃˆŠøŃ€Š¾Š²Š°Š½Š½Š¾Šµ чтение, сохранённое ровно поГ его URI, какими бы свежими они ни были. ŠŠ° ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø 2026-07-28 ŃŃ‚Šø ŃƒŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ ŠæŃ€ŠøŃ…Š¾Š“ŃŃ‚ по ŠæŠ¾Ń‚Š¾ŠŗŃƒ `subscriptions/listen`, который Š¾Ń‚ŠŗŃ€Ń‹Š²Š°ŠµŃ‚ŃŃ через `client.listen(...)`, Šø вытеснение Š·Š°Š²ŠµŃ€ŃˆŠ°ŠµŃ‚ся Ń€Š°Š½ŃŒŃˆŠµ, чем Š½Š°Š±Š»ŃŽŠ“Š°Ń‚ŠµŠ»ŃŒ ŃƒŠ²ŠøŠ“ŠøŃ‚ событие; поГробнее — на странице **[ПоГписки](subscriptions.md)**. diff --git a/i18n/ru/pages/client/callbacks.md b/i18n/ru/pages/client/callbacks.md index c0c29f50..c5392146 100644 --- a/i18n/ru/pages/client/callbacks.md +++ b/i18n/ru/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # ŠšŠ¾Š»Š±ŃŠŗŠø клиента {#client-callbacks} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `mode="legacy"` в вызове `Client(...)` стоит не просто так. По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ `Client(...)` согласовывает современный вариант протокола, а в нём нет обратного канала (back-channel) Š“Š»Ń запросов от сервера Šŗ ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ: `ctx.elicit` Š·Š°Š²ŠµŃ€ŃˆŠ°ŠµŃ‚ŃŃ ошибкой ещё Го того, как ŠŗŠ¾Š»Š±ŃŠŗ ŃƒŃŠæŠµŠ²Š°ŠµŃ‚ Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚ŃŒŃŃ. Š ŠµŃˆŠ°ŠµŃ‚ ŃŃ‚Š¾ не транспорт, а согласованный - протокол — Šø в ŠæŠ°Š¼ŃŃ‚Šø, Šø по URL оГинаково. Š¤ŠøŠŗŃŠøŃ€ŃƒŠ¹Ń‚Šµ `mode="legacy"` Š²ŃŃŠŗŠøŠ¹ раз, когГа клиент Голжен + протокол. Š¤ŠøŠŗŃŠøŃ€ŃƒŠ¹Ń‚Šµ `mode="legacy"` Š²ŃŃŠŗŠøŠ¹ раз, когГа клиент Голжен Š¾Ń‚Š²ŠµŃ‡Š°Ń‚ŃŒ на такие запросы; так Гелает кажГый тест, ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ за ŃŃ‚Š¾Š¹ страницей. ŠŸŠ¾Š“Ń€Š¾Š±Š½ŠµŠµ — на странице **[Версии протокола](../protocol-versions.md)**. Š’ сессии 2026-07-28 ŠŗŠ¾Š»Š±ŃŠŗ не бесполезен — просто Ганные ŠæŠ¾ŃŃ‚ŃƒŠæŠ°ŃŽŃ‚ Šŗ нему иначе: когГа ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ возвращает @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` Šø `list_roots_callback` Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‚ так же, но Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŃŽŃ‚ ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ возможности; современные серверы вместо ŃŃ‚Š¾Š³Š¾ ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŃŽŃ‚ Š¼Š½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń‹Šµ запросы. * `logging_callback` Šø `message_handler` ŠæŠ¾Š»ŃƒŃ‡Š°ŃŽŃ‚ ŃƒŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ. ŠžŠ½Šø ничего не Š¾Š±ŃŠŃŠ²Š»ŃŃŽŃ‚. -ŠŸŠµŃ€Š²Ń‹Š¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `Client(...)` — Š¾Š±ŃŠŠµŠŗŃ‚ транспорта. Все ŠøŃ… виГы описаны на странице **[Транспорты клиента](transports.md)**. +ŠŸŠµŃ€Š²Ń‹Š¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `Client(...)` выбирает транспорт. Все ŠøŃ… виГы описаны на странице **[Транспорты клиента](transports.md)**. diff --git a/i18n/ru/pages/client/index.md b/i18n/ru/pages/client/index.md index 5a86d3dc..d653c886 100644 --- a/i18n/ru/pages/client/index.md +++ b/i18n/ru/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # ŠžŠ±ŃŠŠµŠŗŃ‚ Client {#the-client} @@ -11,13 +11,23 @@ translation: ## ŠŸŠµŃ€Š²Ń‹Š¹ клиент {#your-first-client} -```python title="client.py" hl_lines="14-18" +ŠšŠ»ŠøŠµŠ½Ń‚Ńƒ нужен сервер, с которым он Š±ŃƒŠ“ет Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ. К ŃŃ‚Š¾Š¼Ńƒ ŃŠµŃ€Š²ŠµŃ€Ńƒ Bookshop ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ кажГый пример на ŃŃ‚Š¾Š¹ странице. Дохраните его как `server.py` Šø Š¾ŃŃ‚Š°Š²ŃŒŃ‚Šµ Ń€Š°Š±Š¾Ń‚Š°Ń‚ŃŒ по HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Дервер в начале файла нужен лишь Š“Š»Ń того, чтобы было Šŗ Ń‡ŠµŠ¼Ńƒ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°Ń‚ŃŒŃŃ. ŠšŠ»ŠøŠµŠ½Ń‚ — ŃŃ‚Š¾ ŠæŃŃ‚ŃŒ выГеленных строк. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ **сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера**. Это транспорт в ŠæŠ°Š¼ŃŃ‚Šø: ни поГпроцесса, ни порта, ни HTTP. Именно так ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ кажГый пример на ŃŃ‚Š¾Š¹ странице Šø кажГый тест, который вы Š½Š°ŠæŠøŃˆŠµŃ‚Šµ. +Š¢ŠµŠæŠµŃ€ŃŒ он Š“Š¾ŃŃ‚ŃƒŠæŠµŠ½ по Š°Š“Ń€ŠµŃŃƒ `http://localhost:8000/mcp`. ŠšŠ»ŠøŠµŠ½Ń‚ — Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š°Ń программа. Дохраните её как `client.py` Šø Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `python client.py` во втором терминале: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ **URL**, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ по Streamable HTTP Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ, который вы Ń‚Š¾Š»ŃŒŠŗŠ¾ что Š·Š°ŠæŃƒŃŃ‚или. * `async with` — ŃŃ‚Š¾ **жизненный цикл**. ВхоГ в блок ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ Šø согласовывает возможности; выхоГ — Š¾Ń‚ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚. ŠŸŠ°Ń€Ń‹ `connect()` / `close()` нет, Šø `Client` Š½ŠµŠ»ŃŒŠ·Ń ŠøŃŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŃŒ повторно после Š·Š°Š²ŠµŃ€ŃˆŠµŠ½ŠøŃ блока. * Š’Š½ŃƒŃ‚Ń€Šø блока ŃŠ²ŠµŠ“ŠµŠ½ŠøŃ о ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø уже Š“Š¾ŃŃ‚ŃƒŠæŠ½Ń‹ как обычные свойства. @@ -25,10 +35,10 @@ translation: `Client` принимает оГин позиционный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ Šø Š¾ŠæŃ€ŠµŠ“ŠµŠ»ŃŠµŃ‚ транспорт по его Ń‚ŠøŠæŃƒ: -* Š­ŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ `MCPServer` (или Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š¾Š³Š¾ `Server`): ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ **Š²Š½ŃƒŃ‚Ń€Šø процесса**. -* Дтрока с URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, основной вариант Š“Š»Ń Ń€ŠµŠ°Š»ŃŒŠ½Š¾Š³Š¾ Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Š½ŠøŃ. -* `StdioServerParameters`: команГа, ŠŗŠ¾Ń‚Š¾Ń€Š°Ń Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ŃŃ как **поГпроцесс**; общение с ним иГёт через его stdin Šø stdout. +* Дтрока с URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, транспорт Š“Š»Ń Ń€ŠµŠ°Š»ŃŒŠ½Š¾Š³Š¾ Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Š½ŠøŃ. +* `StdioServerParameters`: команГа, ŠŗŠ¾Ń‚Š¾Ń€Š°Ń Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ŃŃ как Š»Š¾ŠŗŠ°Š»ŃŒŠ½Ń‹Š¹ **поГпроцесс**; общение с ним иГёт через его stdin Šø stdout. * **Транспорт**: всё, что можно ŠøŃŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŃŒ как `async with ... as (read, write)`, например `streamable_http_client(url, http_client=...)` поверх вашего собственного HTTP-клиента. +* Š­ŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ `MCPServer` (или Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š¾Š³Š¾ `Server`): ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ **Š²Š½ŃƒŃ‚Ń€Šø процесса**, без поГпроцесса Šø без порта. Этот вариант — Š“Š»Ń тестов, Šø на нём ŃŃ‚Ń€Š¾ŠøŃ‚ŃŃ страница **[Тестирование](../get-started/testing.md)**. Всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ на ŃŃ‚Š¾Š¹ странице оГинаково Š“Š»Ń всех четырёх вариантов. Заголовкам, поГпроцессам, тайм-Š°ŃƒŃ‚Š°Š¼ Šø ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ `Transport` ŠæŠ¾ŃŠ²ŃŃ‰ŠµŠ½Š° Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š°Ń страница: **[Транспорты клиента](transports.md)**. @@ -49,11 +59,11 @@ translation: ## ŠŸŠ¾Š»ŃƒŃ‡ŠµŠ½ŠøŠµ списка ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š¾Š² {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` возвращает `ListToolsResult`; ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ лежат в `.tools`. ŠšŠ°Š¶Š“Ń‹Š¹ ŠøŠ· них — полное опреГеление, которое хост переГал бы моГели: +`list_tools()` возвращает `ListToolsResult`; ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ лежат в `.tools`. ŠšŠ°Š¶Š“Ń‹Š¹ ŠøŠ· них — полное опреГеление, которое хост переГал бы моГели. Вот первый: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Этой схемы Гостаточно Šø ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŠ¹ŃŃƒ, чтобы Š¾Ń‚Ń€ŠøŃŠ¾Š²Š°Ń‚ŃŒ Ń„Š¾Ń€Š¼Ńƒ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Š¾Š², Šø моГели, чтобы ŃŃ„Š¾Ń€Š¼ŠøŃ€Š¾Š²Š°Ń‚ŃŒ корректные Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń‹. +Второй ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, `lookup_book`, зарегистрирован без `title=`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ его `tool.title` равен `None`. + !!! tip `title` Š½ŠµŠ¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŠµŠ½, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŠøŠ½Ń‚ŠµŃ€Ń„ŠµŠ¹ŃŃƒ, ŠæŠ¾ŠŗŠ°Š·Ń‹Š²Š°ŃŽŃ‰ŠµŠ¼Ńƒ ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ Ń‡ŠµŠ»Š¾Š²ŠµŠŗŃƒ, ŠæŃ€ŠøŃ…Š¾Š“ŠøŃ‚ŃŃ Š²Ń‹Š±ŠøŃ€Š°Ń‚ŃŒ: `title`, если он ŠµŃŃ‚ŃŒ, иначе `name`. `from mcp.shared.metadata_utils import get_display_name` Гелает именно ŃŃ‚Š¾ — @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ Šø возвращает `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ result.is_error # False ŠžŠæŠµŃ€Š°Ń†ŠøŠø с Ń€ŠµŃŃƒŃ€ŃŠ°Š¼Šø ŠøŠ“ŃƒŃ‚ парами: Гва способа ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚ŃŒ список Šø оГин способ ŠæŃ€Š¾Ń‡ŠøŃ‚Š°Ń‚ŃŒ. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ result.is_error # False ## ŠŸŃ€Š¾Š¼ŠæŃ‚Ń‹ {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Дервер с обработчиком Š°Š²Ń‚Š¾Š“Š¾ŠæŠ¾Š»Š½ŠµŠ½ŠøŃ может Š“Š¾ŠæŠ¾Š»Š½ŃŃ‚ŃŒ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń‹ промптов Šø шаблонов Ń€ŠµŃŃƒŃ€ŃŠ¾Š² по мере того, как ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ ŠøŃ… ввоГит. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ŠšŠ°Š¶Š“Ń‹Š¹ метоГ `list_*` принимает именованный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `cursor=`, а кажГый Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ соГержит `next_cursor`. КогГа `next_cursor` равен `None`, у вас ŠµŃŃ‚ŃŒ всё. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Этот цикл корректен Š“Š»Ń Š»ŃŽŠ±Š¾Š³Š¾ сервера. `MCPServer` возвращает всё оГной страницей, так что `next_cursor` равен `None` Šø цикл Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ся оГин раз — ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ в Š±Š¾Š»ŃŒŃˆŠøŠ½ŃŃ‚ве программ его Šø не ŠæŠøŃˆŃƒŃ‚. Šž серверах, которые Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ Ń€Š°Š·Š±ŠøŠ²Š°ŃŽŃ‚ Š²Ń‹Š“Š°Ń‡Ńƒ на страницы, Šø о правилах, которым ŠæŠ¾Š“Ń‡ŠøŠ½ŃŃŽŃ‚ŃŃ ŠŗŃƒŃ€ŃŠ¾Ń€Ń‹, — на странице **[ŠŸŠ°Š³ŠøŠ½Š°Ń†ŠøŃ](../advanced/pagination.md)**. +Š¤ŃƒŠ½ŠŗŃ†ŠøŃ `list_all_tools` корректна Š“Š»Ń Š»ŃŽŠ±Š¾Š³Š¾ сервера. `MCPServer` возвращает всё оГной страницей, так что `next_cursor` равен `None` Šø цикл Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ся оГин раз — ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ в Š±Š¾Š»ŃŒŃˆŠøŠ½ŃŃ‚ве программ его Šø не ŠæŠøŃˆŃƒŃ‚. Šž серверах, которые Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ Ń€Š°Š·Š±ŠøŠ²Š°ŃŽŃ‚ Š²Ń‹Š“Š°Ń‡Ńƒ на страницы, Šø о правилах, которым ŠæŠ¾Š“Ń‡ŠøŠ½ŃŃŽŃ‚ŃŃ ŠŗŃƒŃ€ŃŠ¾Ń€Ń‹, — на странице **[ŠŸŠ°Š³ŠøŠ½Š°Ń†ŠøŃ](../advanced/pagination.md)**. ## Š’ тестах {#in-tests} -`Client(mcp)` без процесса Šø без порта — уже Š³Š¾Ń‚Š¾Š²Š°Ń Ń‚ŠµŃŃ‚Š¾Š²Š°Ń Š¾Š±Š²ŃŠ·ŠŗŠ° Š“Š»Ń сервера. +ŠšŠ°Š¶Š“Ń‹Š¹ `client.py` на ŃŃ‚Š¾Š¹ странице Š¾Š±Ń€Š°Ń‰Š°Š»ŃŃ Šŗ `server.py` по HTTP. Š’ тесте ŃŠµŃ‚ŃŒ не нужна: `Client` ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера — `from server import mcp`, затем `Client(mcp)`. ŠŠø процесса, ни порта, Šø все описанные Š²Ń‹ŃˆŠµ метоГы Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‚ так же. -Š”Š»Ń ŃŃ‚Š¾Š³Š¾ ŠµŃŃ‚ŃŒ оГин ŃŠæŠµŃ†ŠøŠ°Š»ŃŒŠ½Ń‹Š¹ флаг ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°: `Client(mcp, raise_exceptions=True)`. ŠžŠ½ Š“ŠµŠ¹ŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ на ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃŃ… в ŠæŠ°Š¼ŃŃ‚Šø, а Š¾Š±ŃŠŃŃŠ½ŃŠµŃ‚ его Šø строит Š²Š¾ŠŗŃ€ŃƒŠ³ него весь поГхоГ страница **[Тестирование](../get-started/testing.md)**. +Š”Š»Ń ŃŃ‚Š¾Š³Š¾ ŠµŃŃ‚ŃŒ оГин ŃŠæŠµŃ†ŠøŠ°Š»ŃŒŠ½Ń‹Š¹ флаг ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°: `Client(mcp, raise_exceptions=True)`. ŠžŠ½ Š“ŠµŠ¹ŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ на ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃŃ… Š²Š½ŃƒŃ‚Ń€Šø процесса, а Š¾Š±ŃŠŃŃŠ½ŃŠµŃ‚ его Šø строит Š²Š¾ŠŗŃ€ŃƒŠ³ него весь поГхоГ страница **[Тестирование](../get-started/testing.md)**. ## Š˜Ń‚Š¾Š³Šø {#recap} -* `Client(x)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ в ŠæŠ°Š¼ŃŃ‚Šø Šŗ Š¾Š±ŃŠŠµŠŗŃ‚Ńƒ сервера, по Streamable HTTP — Šŗ строке с URL, а ко всему Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Š¼Ńƒ — через транспорт. +* `Client(x)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ по Streamable HTTP Šŗ строке с URL, Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ поГпроцесс Š“Š»Ń `StdioServerParameters`, Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ вхоГит в переГанный транспорт, а в тестах принимает сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера. * `async with` — ŃŃ‚Š¾ весь жизненный цикл. Š’Š½ŃƒŃ‚Ń€Šø него `server_capabilities` Šø `protocol_version` уже заполнены; `server_info` Šø `instructions` — тоже, если сервер ŠøŃ… ŠæŃ€ŠµŠ“Š¾ŃŃ‚Š°Š²Š»ŃŠµŃ‚. * `list_tools()` Гаёт `name`, `title`, `description` Šø `input_schema` кажГого ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°. * `call_tool()` возвращает `content` Š“Š»Ń моГели, `structured_content` Š“Š»Ń коГа Šø `is_error`. Š˜Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, Š²Ń‹Š±Ń€Š¾ŃŠøŠ²ŃˆŠøŠ¹ ŠøŃŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ, — ŃŃ‚Š¾ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚, а не ŠøŃŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ. diff --git a/i18n/ru/pages/client/oauth-clients.md b/i18n/ru/pages/client/oauth-clients.md index 3af177a2..987455f0 100644 --- a/i18n/ru/pages/client/oauth-clients.md +++ b/i18n/ru/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth-клиенты {#oauth-clients} @@ -82,18 +82,20 @@ translation: КогГа `Client` Š¾Ń‚ŠæŃ€Š°Š²Š»ŃŠµŃ‚ первый запрос, сервер отвечает `401`. Š”Š°Š»ŃŒŃˆŠµ Š“ŠµŠ¹ŃŃ‚Š²ŃƒŠµŃ‚ провайГер: -1. **ŠžŠ±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ.** ŠžŠ½ читает заголовок `WWW-Authenticate`, Š·Š°Š³Ń€ŃƒŠ¶Š°ŠµŃ‚ метаГанные защищённого Ń€ŠµŃŃƒŃ€ŃŠ° (Protected Resource Metadata) сервера с `/.well-known/oauth-protected-resource`, ŃƒŠ·Š½Š°Ń‘Ń‚, какой сервер авторизации защищает ŃŃ‚Š¾Ń‚ Ń€ŠµŃŃƒŃ€Ń, Šø Š·Š°Š³Ń€ŃƒŠ¶Š°ŠµŃ‚ метаГанные уже *того* сервера. +1. **ŠžŠ±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ.** ŠžŠ½ читает заголовок `WWW-Authenticate`, Š·Š°Š³Ń€ŃƒŠ¶Š°ŠµŃ‚ метаГанные защищённого Ń€ŠµŃŃƒŃ€ŃŠ° (Protected Resource Metadata) сервера с `/.well-known/oauth-protected-resource`, ŃƒŠ·Š½Š°Ń‘Ń‚, какой сервер авторизации защищает ŃŃ‚Š¾Ń‚ Ń€ŠµŃŃƒŃ€Ń, Šø Š·Š°Š³Ń€ŃƒŠ¶Š°ŠµŃ‚ метаГанные уже *того* сервера. (Š£ сервера ŠæŠ¾ŃŃ‚Š°Ń€ŃˆŠµ, который не ŠæŃƒŠ±Š»ŠøŠŗŃƒŠµŃ‚ метаГанные Ń€ŠµŃŃƒŃ€ŃŠ°, вместо ŃŃ‚Š¾Š³Š¾ Š·Š°ŠæŃ€Š°ŃˆŠøŠ²Š°ŃŽŃ‚ŃŃ метаГанные сервера авторизации по его ŃŠ¾Š±ŃŃ‚Š²ŠµŠ½Š½Š¾Š¼Ńƒ origin.) Š’ Š»ŃŽŠ±Š¾Š¼ ŃŠ»ŃƒŃ‡Š°Šµ метаГанные Голжны Š½Š°Š·Ń‹Š²Š°Ń‚ŃŒ в поле `issuer` тот сервер, Š“Š»Ń которого они были Š·Š°Š³Ń€ŃƒŠ¶ŠµŠ½Ń‹; всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ŃŃ. 2. **Š ŠµŠ³ŠøŃŃ‚Ń€Š°Ń†ŠøŃ.** Š’ хранилище ŠæŃƒŃŃ‚о? ŠžŠ½ Гинамически Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€ŃƒŠµŃ‚ вас с вашим `OAuthClientMetadata` Šø ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. 3. **ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ.** ŠžŠ½ Š³ŠµŠ½ŠµŃ€ŠøŃ€ŃƒŠµŃ‚ ŠæŠ°Ń€Ńƒ PKCE Šø `state`, собирает URL авторизации, жГёт ваш `redirect_handler`, а затем жГёт от `callback_handler` коГ. 4. **ŠžŠ±Š¼ŠµŠ½.** ŠžŠ½ обменивает коГ на `OAuthToken`, ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ его Šø ŠæŠ¾Š²Ń‚Š¾Ń€ŃŠµŃ‚ исхоГный запрос уже с `Authorization: Bearer ...`. После ŃŃ‚Š¾Š³Š¾ он работает незаметно. Токены Š±ŠµŃ€ŃƒŃ‚ŃŃ ŠøŠ· хранилища, ŠøŃŃ‚Ń‘ŠŗŃˆŠøŠ¹ токен Š“Š¾ŃŃ‚ŃƒŠæŠ° Š¾Š±Š½Š¾Š²Š»ŃŠµŃ‚ŃŃ с ŠæŠ¾Š¼Š¾Ń‰ŃŒŃŽ токена Š¾Š±Š½Š¾Š²Š»ŠµŠ½ŠøŃ, Šø Ń‚Š¾Š»ŃŒŠŗŠ¾ когГа ничего ŠøŠ· ŃŃ‚Š¾Š³Š¾ не срабатывает, сценарий Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ся заново. +Ко всем ŃŃ‚ŠøŠ¼ запросам ŠæŃ€ŠøŠ¼ŠµŠ½ŃŠµŃ‚ся оГно транспортное правило: как Šø MCP-запрос, Š²Š½ŃƒŃ‚Ń€Šø которого они Š²Ń‹ŠæŠ¾Š»Š½ŃŃŽŃ‚ся, они ŃŠ»ŠµŠ“ŃƒŃŽŃ‚ ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½ŠøŃŽ Ń‚Š¾Š»ŃŒŠŗŠ¾ тогГа, когГа оно Š¾ŃŃ‚Š°Ń‘Ń‚ŃŃ на том же origin Šø ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ метоГ (скажем, 307/308 на Š·Š°Š²ŠµŃ€ŃˆŠ°ŃŽŃ‰ŠøŠ¹ слеш), а Š»ŃŽŠ±Š¾Šµ Š“Ń€ŃƒŠ³Š¾Šµ перенаправление ŃŃ‡ŠøŃ‚Š°ŃŽŃ‚ тем, что ŃŃ‚Š¾Ń‚ URL не отвечает. + ŠŠøŃ‡ŠµŠ³Š¾ ŠøŠ· ŃŃ‚Š¾Š³Š¾ вы не писали. ŠžŃŃ‚Š°ŃŽŃ‚ŃŃ Гва именованных Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Š° (`client_metadata_url` Šø `validate_resource_url`), Šø ŃŃ‚Š¾Š¼Ńƒ Ń„Š°Š¹Š»Ńƒ не нужен ни оГин ŠøŠ· них. Šž `client_metadata_url` стоит Š·Š½Š°Ń‚ŃŒ — ему ŠæŠ¾ŃŠ²ŃŃ‰Ń‘Š½ Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Ń‹Š¹ разГел ниже. ### ŠŸŠ¾ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ сами {#try-it} -Š‘Š¾Š»ŃŒŃˆŠøŠ½ŃŃ‚Š²Š¾ примеров в ŃŃ‚Š¾Š¹ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŠø можно ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ с `Client(server)` в ŠæŠ°Š¼ŃŃ‚Šø. Этот — нет: весь смысл ŃŃ†ŠµŠ½Š°Ń€ŠøŃ в HTTP-ответе `401`, а межГу клиентом в ŠæŠ°Š¼ŃŃ‚Šø Šø его сервером никакого HTTP нет. +`Client(server)` в ŠæŠ°Š¼ŃŃ‚Šø, которым ŠæŠ¾Š»ŃŒŠ·ŃƒŃŽŃ‚ŃŃ ваши тесты, зГесь не поможет: весь смысл ŃŃ†ŠµŠ½Š°Ń€ŠøŃ в HTTP-ответе `401`, а межГу клиентом в ŠæŠ°Š¼ŃŃ‚Šø Šø его сервером никакого HTTP нет. Š’ репозитории ŠµŃŃ‚ŃŒ Š¶ŠøŠ²Š°Ń Š²ŠµŃ€ŃŠøŃ. `examples/servers/simple-auth/` Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Ń‹Š¹ сервер авторизации Šø защищённый MCP-сервер; `examples/clients/simple-auth-client/` — ŃŃ‚Š¾ клиент с ŃŃ‚Š¾Š¹ страницы, Š²Ń‹Ń€Š¾ŃŃˆŠøŠ¹ в небольшой CLI. Š’ его README — Гве команГы: Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ серверы, Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ клиент, ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŠ² его Šŗ ним, — Šø Š½Š°Š±Š»ŃŽŠ“Š°Š¹Ń‚Šµ, как ŠæŃ€Š¾Ń…Š¾Š“ŃŃ‚ все четыре шага. @@ -111,13 +113,14 @@ URL Голжен Š±Ń‹Ń‚ŃŒ HTTPS Šø с некорневым ŠæŃƒŃ‚ём; всё о `ClientCredentialsOAuthProvider` — тот же `httpx2.Auth`, Ń‚Š¾Š»ŃŒŠŗŠ¾ без человека: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` Что изменилось: * ŠŠµŃ‚ `OAuthClientMetadata`, нет обработчиков. Š’Ń‹ переГаёте `client_id` Šø `client_secret`; провайГер строит Š²Š¾ŠŗŃ€ŃƒŠ³ них Š¼ŠøŠ½ŠøŠ¼Š°Š»ŃŒŠ½ŃƒŃŽ Ń€ŠµŠ³ŠøŃŃ‚Ń€Š°Ń†ŠøŃŽ `client_credentials` Šø ŠæŠ¾Š»Š½Š¾ŃŃ‚ŃŒŃŽ ŠæŃ€Š¾ŠæŃƒŃŠŗŠ°ŠµŃ‚ Š“ŠøŠ½Š°Š¼ŠøŃ‡ŠµŃŠŗŃƒŃŽ Ń€ŠµŠ³ŠøŃŃ‚Ń€Š°Ń†ŠøŃŽ. +* `issuer` называет сервер авторизации, который выГал ŃŃ‚Šø ŃƒŃ‡Ń‘Ń‚Š½Ń‹Šµ Ганные; ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ значение `issuer`, которое возвращает его Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ `/.well-known/oauth-authorization-server`. ŠžŠ±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ прохоГит, как описано Š²Ń‹ŃˆŠµ, но запросы токена ŃŃ‚Ń€Š¾ŃŃ‚ŃŃ Ń‚Š¾Š»ŃŒŠŗŠ¾ по метаГанным *ŃŃ‚Š¾Š³Š¾* ŠøŠ·Š“Š°Ń‚ŠµŠ»Ń; если MCP-сервер ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ куГа-то ещё, сценарий Š¾ŃŃ‚Š°Š½Š°Š²Š»ŠøŠ²Š°ŠµŃ‚ŃŃ с `OAuthFlowError`. ŠŠµ ŃƒŠŗŠ°Š·Ń‹Š²Š°Ń‚ŃŒ его — ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠ¹ вариант, Šø в 3.0 параметр станет Š¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¼ (см. **[Š£ŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ возможности](../deprecated.md#deprecated-sdk-helpers)**); Го тех пор провайГер выГаёт ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ Šø ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ тот сервер авторизации, который найГёт Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ. * `scope` — строка с разГелением пробелами, формат OAuth Š“Š»Ń переГачи по сети. * Всё Гальше по цепочке иГентично: тот же `TokenStorage`, тот же `httpx2.AsyncClient(auth=...)`, тот же `streamable_http_client`. @@ -130,7 +133,7 @@ URL Голжен Š±Ń‹Ń‚ŃŒ HTTPS Šø с некорневым ŠæŃƒŃ‚ём; всё о Ещё оГин провайГер Š½Š°Ń…Š¾Š“ŠøŃ‚ŃŃ в `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`** — Š“Š»Ń клиентов, которые Š°ŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŃ†ŠøŃ€ŃƒŃŽŃ‚ŃŃ с ŠæŠ¾Š¼Š¾Ń‰ŃŒŃŽ JWT вместо общего секрета (`private_key_jwt`, вариант с парой ŠŗŠ»ŃŽŃ‡ŠµŠ¹ Šø workload identity). - Дхема та же: созГайте ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ Šø переГайте его в `auth=`. Š’ том же моГуле ŠµŃŃ‚ŃŒ + Дхема та же: созГайте ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ (он принимает тот же Š½ŠµŠ¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¹ `issuer`) Šø переГайте его в `auth=`. Š’ том же моГуле ŠµŃŃ‚ŃŒ `SignedJWTParameters` Šø `static_assertion_provider` — Гва Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Ń… среГства, которые ŃŠ¾Š±ŠøŃ€Š°ŃŽŃ‚ Š“Š»Ń него ŃƒŃ‚Š²ŠµŃ€Š¶Š“ŠµŠ½ŠøŠµ (assertion). diff --git a/i18n/ru/pages/client/session-groups.md b/i18n/ru/pages/client/session-groups.md index 7014c960..7b0fade0 100644 --- a/i18n/ru/pages/client/session-groups.md +++ b/i18n/ru/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Š“Ń€ŃƒŠæŠæŃ‹ сессий {#session-groups} @@ -78,7 +78,7 @@ translation: ## Š˜Ń‚Š¾Š³Šø {#recap} * `ClientSessionGroup` Гержит много ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠ¹ Šŗ серверам Šø своГит ŠøŃ… ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹, Ń€ŠµŃŃƒŃ€ŃŃ‹ Šø промпты в оГин `dict` Š“Š»Ń кажГого виГа. -* `connect_to_server(params)` — по оГному на сервер. ŠŸŃ€ŠøŠ½ŠøŠ¼Š°ŠµŃ‚ параметры транспорта, а не Š¾Š±ŃŠŠµŠŗŃ‚ сервера или URL, как `Client`. +* `connect_to_server(params)` — по оГному на сервер. ŠŸŃ€ŠøŠ½ŠøŠ¼Š°ŠµŃ‚ параметры транспорта, а не URL или `Transport`, которые принимает `Client`. * `group.call_tool(name, arguments)` сам Š½Š°ŠæŃ€Š°Š²Š»ŃŠµŃ‚ вызов на сервер-влаГелец. * Имена Голжны Š±Ń‹Ń‚ŃŒ ŃƒŠ½ŠøŠŗŠ°Š»ŃŒŠ½Ń‹ в преГелах всей Š³Ń€ŃƒŠæŠæŃ‹; Гва сервера с ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š¾Š¼ `search` сами по себе ŃƒŠ¶ŠøŃ‚ŃŒŃŃ не Š¼Š¾Š³ŃƒŃ‚. * `component_name_hook=` переписывает кажГое Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€ŃƒŠµŠ¼Š¾Šµ ŠøŠ¼Ń. ŠœŠµŠ½ŃŠµŃ‚ŃŃ ŠŗŠ»ŃŽŃ‡ ŃŠ»Š¾Š²Š°Ń€Ń, но не ŠøŠ¼Ń в переГаваемых Ганных. diff --git a/i18n/ru/pages/client/transports.md b/i18n/ru/pages/client/transports.md index 1c24a01e..28a44383 100644 --- a/i18n/ru/pages/client/transports.md +++ b/i18n/ru/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠøŠµ транспорты {#client-transports} @@ -11,30 +11,15 @@ translation: *Š”ŠµŃ€Š²ŠµŃ€Š½Š°Ń* сторона кажГого ŠøŠ· них (то, что Гелает `mcp.run()` Šø что вы развёртываете) описана на странице **[Š—Š°ŠæŃƒŃŠŗ сервера](../run/index.md)**. -## Š’ ŠæŠ°Š¼ŃŃ‚Šø {#in-memory} - -ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ŠŠø поГпроцесса, ни порта, ни байтов в переГаваемых Ганных. ŠšŠ»ŠøŠµŠ½Ń‚ Šø сервер — Гва Š¾Š±ŃŠŠµŠŗŃ‚а в оГном процессе, Šø вызов всё равно прохоГит через Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»ŃŒŠ½Ń‹Š¹ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ: `search_books` ŠæŠµŃ€ŠµŃ‡ŠøŃŠ»ŃŠµŃ‚ŃŃ, Š²Š°Š»ŠøŠ“ŠøŃ€ŃƒŠµŃ‚ŃŃ Šø Š²Ń‹Š·Ń‹Š²Š°ŠµŃ‚ŃŃ ровно так же, как ŃŃ‚Š¾ было бы по HTTP. - -ŠŸŠ¾ŃŃ‚Š¾Š¼Ńƒ ŃŃ‚Š¾ ŃŃ€Š°Š·Ńƒ Гве вещи: - -* **Тестовый стенГ.** ŠšŠ°Š¶Š“Ń‹Š¹ пример в ŃŃ‚Š¾Š¹ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŠø ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ŃŃ именно так, а страница **[Тестирование](../get-started/testing.md)** строит Š²Š¾ŠŗŃ€ŃƒŠ³ ŃŃ‚Š¾Š³Š¾ весь поГхоГ. -* **API Š“Š»Ń Š²ŃŃ‚Ń€Š°ŠøŠ²Š°Š½ŠøŃ.** ŠŸŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃŽ, которое созГаёт сервер, не нужен сетевой перехоГ, чтобы Š²Ń‹Š·Ń‹Š²Š°Ń‚ŃŒ его ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹. - ## Streamable HTTP {#streamable-http} -ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ ŃŃ‚Ń€Š¾ŠŗŃƒ с URL — Šø ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚Šµ **Streamable HTTP**, транспорт, за которым вы развёртываете сервер: +ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ ŃŃ‚Ń€Š¾ŠŗŃƒ с URL — Šø ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚Šµ **Streamable HTTP**, транспорт, за которым вы развёртываете сервер Šø с которого стоит Š½Š°Ń‡ŠøŠ½Š°Ń‚ŃŒ: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Это уже готовый клиент Š“Š»Ń ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Š°. `Client` сам оборачивает URL в `streamable_http_client(...)` поверх `httpx2.AsyncClient`, настроенного так, как нужно MCP: `follow_redirects=True`, Ń‚Š°Š¹Š¼Š°ŃƒŃ‚ 30 секунГ на connect/write/pool Šø Ń‚Š°Š¹Š¼Š°ŃƒŃ‚ Ń‡Ń‚ŠµŠ½ŠøŃ 300 секунГ, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что сервер может Š“ŠµŃ€Š¶Š°Ń‚ŃŒ поток ответа открытым. +Это уже готовый клиент Š“Š»Ń ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Š°. `Client` сам оборачивает URL в `streamable_http_client(...)` поверх `httpx2.AsyncClient`, настроенного так, как нужно MCP: Ń‚Š°Š¹Š¼Š°ŃƒŃ‚ 30 секунГ на connect/write/pool Šø Ń‚Š°Š¹Š¼Š°ŃƒŃ‚ Ń‡Ń‚ŠµŠ½ŠøŃ 300 секунГ, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что сервер может Š“ŠµŃ€Š¶Š°Ń‚ŃŒ поток ответа открытым. !!! check ДозГанный `Client` **не** ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½. ŠšŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€ Ń‚Š¾Š»ŃŒŠŗŠ¾ выбирает транспорт; @@ -50,7 +35,7 @@ translation: Как Ń‚Š¾Š»ŃŒŠŗŠ¾ ŠæŠ¾Š½Š°Š“Š¾Š±ŠøŃ‚ŃŃ заголовок `Authorization`, cookie, прокси, mTLS или Š“Ń€ŃƒŠ³Š¾Š¹ Ń‚Š°Š¹Š¼Š°ŃƒŃ‚, созГайте `httpx2.AsyncClient` сами Šø переГайте его в `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ translation: !!! info `httpx2` ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ привычный API `httpx`, так что, если вы знаете `httpx`, вы уже ŃƒŠ¼ŠµŠµŃ‚Šµ Š“ŠµŠ»Š°Ń‚ŃŒ зГесь Š°ŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃŽ, прокси, Ń…ŃƒŠŗŠø событий, повторные попытки Šø Š¾Š³Ń€Š°Š½ŠøŃ‡ŠµŠ½ŠøŃ соеГинений. SDK ничего не Š“Š¾Š±Š°Š²Š»ŃŠµŃ‚ ŃŠ²ŠµŃ€Ń…Ńƒ - Šø ничего не ŃƒŠ±ŠøŃ€Š°ŠµŃ‚. Š—Š“ŠµŃŃŒ же ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ OAuth: + Šø ничего не ŃƒŠ±ŠøŃ€Š°ŠµŃ‚, кроме [обработки реГиректов](#redirects). Š—Š“ŠµŃŃŒ же ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Š’ŠµŃŃŒ ŃŃ‚Š¾Ń‚ сценарий — на странице **[OAuth-клиенты](oauth-clients.md)**. +### РеГиректы {#redirects} + +Транспорт ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ URL, который вы ему переГали, Šø ни Šŗ какому Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ ŠøŃŃ‚Š¾Ń‡Š½ŠøŠŗŃƒ (origin). + +* РеГирект `307`/`308`, который Š¾ŃŃ‚Š°Ń‘Ń‚ŃŃ на той же схеме, хосте Šø ŠæŠ¾Ń€Ń‚Ńƒ, Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ŃŃ; то же ŠŗŠ°ŃŠ°ŠµŃ‚ся перехоГа `http://` → `https://` на том же хосте. Это покрывает обычный реГирект с Гобавлением косой черты `/mcp` → `/mcp/`. +* РеГирект куГа-либо ещё **не** Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ŃŃ. Вызов Š·Š°Š²ŠµŃ€ŃˆŠ°ŠµŃ‚ся ошибкой: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Если ŃŃ‚Š¾Ń‚ URL Šø ŠµŃŃ‚ŃŒ Š½ŃƒŠ¶Š½Ń‹Š¹ сервер, ŃƒŠŗŠ°Š¶ŠøŃ‚Šµ его в ŠŗŠ¾Š½Ń„ŠøŠ³ŃƒŃ€Š°Ń†ŠøŠø. Если нет — сервер или прокси переГ ним настроен Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾. + +Это верно Š“Š»Ń Š»ŃŽŠ±Š¾Š³Š¾ `httpx2.AsyncClient`, который вы переГаёте: его настройка `follow_redirects` Š“Š»Ń MCP-запросов не ŃƒŃ‡ŠøŃ‚Ń‹Š²Š°ŠµŃ‚ŃŃ — ни в оГну, ни в Š“Ń€ŃƒŠ³ŃƒŃŽ ŃŃ‚Š¾Ń€Š¾Š½Ńƒ. OAuth-провайГеры SDK ŠæŃ€ŠøŠ¼ŠµŠ½ŃŃŽŃ‚ то же правило Šŗ своим собственным запросам. + +!!! tip + Дообщение `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` означает, что + сервер стоит за прокси с терминацией TLS, о котором не знает, Šø выГаёт реГиректы на `http://`. + Это ŠøŃŠæŃ€Š°Š²Š»ŃŠµŃ‚ся на сервере (**[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](../run/deploy.md#behind-a-tls-terminating-proxy)**) + либо использованием ровно того URL `https://…/`, который преГлагает сообщение. + ## stdio {#stdio} Дервер **stdio** — ŃŃ‚Š¾ поГпроцесс. ŠšŠ»ŠøŠµŠ½Ń‚ Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ его, ŠæŠøŃˆŠµŃ‚ JSON-RPC в его stdin Šø читает JSON-RPC ŠøŠ· его stdout. Именно так Гесктопный хост Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ сервер на вашей машине: хост — ŃŃ‚Š¾ *Šø ŠµŃŃ‚ŃŒ* ŃŃ‚Š¾Ń‚ коГ ŠæŠ»ŃŽŃ UI, а страница **[ŠŸŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ Šŗ Ń€ŠµŠ°Š»ŃŒŠ½Š¾Š¼Ńƒ Ń…Š¾ŃŃ‚Ńƒ](../get-started/real-host.md)** показывает те же Š¾Ń‚Š½Š¾ŃˆŠµŠ½ŠøŃ со стороны хоста, в виГе файла ŠŗŠ¾Š½Ń„ŠøŠ³ŃƒŃ€Š°Ń†ŠøŠø. @@ -105,6 +111,18 @@ stderr Гочернего процесса иГёт в ваш. Чтобы нап Дервер, ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ нужен API-ŠŗŠ»ŃŽŃ‡, там его не найГёт. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ его ŃŠ²Š½Š¾ через `env=`; ŃŃ‚Šø переменные Š“Š¾Š±Š°Š²Š»ŃŃŽŃ‚ŃŃ поверх Ń€Š°Š·Ń€ŠµŃˆŃ‘Š½Š½Š¾Š³Š¾ списка. Именно ŃŃ‚Š¾ Гелает `BOOKSHOP_API_KEY` Š²Ń‹ŃˆŠµ. +## Š’ ŠæŠ°Š¼ŃŃ‚Šø {#in-memory} + +Š’ тесте нечего Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Ń‚ŃŒ Šø нечего Š·Š°ŠæŃƒŃŠŗŠ°Ń‚ŃŒ. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ŠŠø поГпроцесса, ни порта, ни байтов в сети. ŠšŠ»ŠøŠµŠ½Ń‚ Šø сервер — Гва Š¾Š±ŃŠŠµŠŗŃ‚а в оГном процессе, Šø вызов всё равно прохоГит через Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»ŃŒŠ½Ń‹Š¹ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ: `search_books` ŠæŠµŃ€ŠµŃ‡ŠøŃŠ»ŃŠµŃ‚ŃŃ, Š²Š°Š»ŠøŠ“ŠøŃ€ŃƒŠµŃ‚ŃŃ Šø Š²Ń‹Š·Ń‹Š²Š°ŠµŃ‚ŃŃ ровно так же, как ŃŃ‚Š¾ было бы по HTTP. Дтраница **[Тестирование](../get-started/testing.md)** строит Š²Š¾ŠŗŃ€ŃƒŠ³ ŃŃ‚Š¾Š³Š¾ весь поГхоГ. + +Та же форма ŃŠ»ŃƒŠ¶ŠøŃ‚ Šø API Š“Š»Ń Š²ŃŃ‚Ń€Š°ŠøŠ²Š°Š½ŠøŃ: приложение, которое само созГаёт сервер, может Š²Ń‹Š·Ń‹Š²Š°Ń‚ŃŒ его ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹ без Š¾Š±Ń€Š°Ń‰ŠµŠ½ŠøŃ Šŗ сети. + ## SSE {#sse} `sse_client(url)` ŠøŠ· `mcp.client.sse` — ŃŃ‚Š¾ HTTP-транспорт, который заменил Streamable HTTP. ŠžŠ±Š¾Ń€Š°Ń‡ŠøŠ²Š°Š¹Ń‚Šµ его так же, `Client(sse_client("http://localhost:8000/sse"))`, чтобы Š¾Š±Ń‰Š°Ń‚ŃŒŃŃ с сервером, который всё ещё на нём говорит, Šø не стройте на нём ничего нового. @@ -113,15 +131,16 @@ stderr Гочернего процесса иГёт в ваш. Чтобы нап Š”Š»Ń `Client` всё перечисленное — оГно Šø то же. -**Транспорт** — ŃŃ‚Š¾ Š»ŃŽŠ±Š¾Š¹ асинхронный контекстный менеГжер, который отГаёт ŠæŠ°Ń€Ńƒ потоков сообщений `(read, write)`: Ń„Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾ — протокол `Transport` ŠøŠ· `mcp.client`. `Client` Ń€Š°Š·Ń€ŠµŃˆŠ°ŠµŃ‚ свой Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ по Ń‚ŠøŠæŃƒ: Š¾Š±ŃŠŠµŠŗŃ‚ сервера ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Š²Š½ŃƒŃ‚Ń€Šø процесса, `str` ŠæŃ€ŠµŠ²Ń€Š°Ń‰Š°ŠµŃ‚ŃŃ в `streamable_http_client(url)`, `StdioServerParameters` — в `stdio_client(params)`, а всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ŃŃ как транспорт Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ. Š‘Š»Š°Š³Š¾Š“Š°Ń€Ń послеГнему ŠæŃ€Š°Š²ŠøŠ»Ńƒ `stdio_client(...)`, `streamable_http_client(...)` Šø `sse_client(...)` ŠæŠ¾Š“Ń…Š¾Š“ŃŃ‚ в оГно Šø то же место — Šø вы можете Š½Š°ŠæŠøŃŠ°Ń‚ŃŒ свой. +**Транспорт** — ŃŃ‚Š¾ Š»ŃŽŠ±Š¾Š¹ асинхронный контекстный менеГжер, который отГаёт ŠæŠ°Ń€Ńƒ потоков сообщений `(read, write)`: Ń„Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾ — протокол `Transport` ŠøŠ· `mcp.client`. `Client` Ń€Š°Š·Ń€ŠµŃˆŠ°ŠµŃ‚ свой Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ по Ń‚ŠøŠæŃƒ: `str` ŠæŃ€ŠµŠ²Ń€Š°Ń‰Š°ŠµŃ‚ŃŃ в `streamable_http_client(url)`, `StdioServerParameters` — в `stdio_client(params)`, Š¾Š±ŃŠŠµŠŗŃ‚ сервера ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Š²Š½ŃƒŃ‚Ń€Šø процесса, а всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ Š¾Ń‚ŠŗŃ€Ń‹Š²Š°ŠµŃ‚ŃŃ Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ как транспорт. Š‘Š»Š°Š³Š¾Š“Š°Ń€Ń послеГнему ŠæŃ€Š°Š²ŠøŠ»Ńƒ `stdio_client(...)`, `streamable_http_client(...)` Šø `sse_client(...)` Š²ŃŃ‚Š°ŃŽŃ‚ в оГно Šø то же место — Šø ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ же можно Š½Š°ŠæŠøŃŠ°Ń‚ŃŒ свой собственный. ## Š˜Ń‚Š¾Š³Šø {#recap} -* `Client(mcp)` (Š¾Š±ŃŠŠµŠŗŃ‚ сервера) ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ в ŠæŠ°Š¼ŃŃ‚Šø. Š˜ŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ Š“Š»Ń тестов Šø Š“Š»Ń Š²ŃŃ‚Ń€Š°ŠøŠ²Š°Š½ŠøŃ. * `Client("http://.../mcp")` (URL) ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ по Streamable HTTP, Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ Š“Š»Ń ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Š°. * Заголовки, Š°ŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ, прокси Šø Ń‚Š°Š¹Š¼Š°ŃƒŃ‚Ń‹ Š·Š°Š“Š°ŃŽŃ‚ŃŃ на `httpx2.AsyncClient`, который ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ в `streamable_http_client(url, http_client=...)`. Именованного Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚а `headers=` нет. +* РеГиректы Š²Ń‹ŠæŠ¾Š»Š½ŃŃŽŃ‚ŃŃ Ń‚Š¾Š»ŃŒŠŗŠ¾ в преГелах источника самого URL (`307`/`308` с Гобавлением косой черты) ŠæŠ»ŃŽŃ `http`→`https` на том же хосте. Всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ Š·Š°Š²ŠµŃ€ŃˆŠ°ŠµŃ‚ŃŃ ошибкой `Redirect to … not followed`; ŃƒŠŗŠ°Š¶ŠøŃ‚Šµ в ŠŗŠ¾Š½Ń„ŠøŠ³ŃƒŃ€Š°Ń†ŠøŠø конечный URL. * stdio — ŃŃ‚Š¾ `Client(StdioServerParameters(...))`. ŠžŠ±Š¾Ń€Š°Ń‡ŠøŠ²Š°Š¹Ń‚Šµ его в `stdio_client(...)` сами, Ń‚Š¾Š»ŃŒŠŗŠ¾ чтобы ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²ŠøŃ‚ŃŒ stderr Гочернего процесса. * ŠŸŠ¾Š“ŠæŃ€Š¾Ń†ŠµŃŃ ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ Š¾ŠŗŃ€ŃƒŠ¶ŠµŠ½ŠøŠµ ŠøŠ· Ń€Š°Š·Ń€ŠµŃˆŃ‘Š½Š½Š¾Š³Š¾ списка, а не ваше; `env=` Š“Š¾Š±Š°Š²Š»ŃŠµŃ‚ Šŗ нему. +* `Client(mcp)` (Š¾Š±ŃŠŠµŠŗŃ‚ сервера) ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ в ŠæŠ°Š¼ŃŃ‚Šø. Š˜ŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ его в тестах или чтобы Š²ŃŃ‚Ń€Š¾ŠøŃ‚ŃŒ сервер в приложение, которое его созГало. * Транспорт — ŃŃ‚Š¾ всё, с чем можно Š½Š°ŠæŠøŃŠ°Ń‚ŃŒ `async with x as (read, write)`. `Client` переГаёт всё, что не Š¾Š±ŃŠŠµŠŗŃ‚ сервера, не URL Šø не `StdioServerParameters`, ŠæŃ€ŃŠ¼Š¾ в ŃŃ‚Š¾Ń‚ протокол. * ДозГание `Client` выбирает транспорт. `async with` его открывает. diff --git a/i18n/ru/pages/deprecated.md b/i18n/ru/pages/deprecated.md index ba399dd3..b014bc3f 100644 --- a/i18n/ru/pages/deprecated.md +++ b/i18n/ru/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Š£ŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ возможности {#deprecated-features} -Š”ŠæŠµŃ†ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ 2026-07-28 вывоГит ŠøŠ· Š¾Š±Ń€Š°Ń‰ŠµŠ½ŠøŃ ŠæŃŃ‚ŃŒ возможностей. SDK по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ Ń€ŠµŠ°Š»ŠøŠ·ŃƒŠµŃ‚ ŠŗŠ°Š¶Š“ŃƒŃŽ ŠøŠ· них, Šø ŠŗŠ°Š¶Š“Š°Ń Ń‚ŠµŠæŠµŃ€ŃŒ выГаёт **ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ об ŃƒŃŃ‚Š°Ń€ŠµŠ²Š°Š½ŠøŠø**. ŠžŠ“ŠøŠ½ Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¹ метоГ SDK Š¾Š±ŃŠŃŠ²Š»ŠµŠ½ ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠ¼ по собственным причинам Šø описан [в конце страницы](#deprecated-sdk-helpers). +Š”ŠæŠµŃ†ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ 2026-07-28 вывоГит ŠøŠ· Š¾Š±Ń€Š°Ń‰ŠµŠ½ŠøŃ ŠæŃŃ‚ŃŒ возможностей. SDK по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ Ń€ŠµŠ°Š»ŠøŠ·ŃƒŠµŃ‚ ŠŗŠ°Š¶Š“ŃƒŃŽ ŠøŠ· них, Šø ŠŗŠ°Š¶Š“Š°Ń Ń‚ŠµŠæŠµŃ€ŃŒ выГаёт **ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ об ŃƒŃŃ‚Š°Ń€ŠµŠ²Š°Š½ŠøŠø**. ŠŠµŃŠŗŠ¾Š»ŃŒŠŗŠ¾ ŃƒŃŃ‚Š°Ń€ŠµŠ²Š°Š½ŠøŠ¹ на ŃƒŃ€Š¾Š²Š½Šµ самого SDK ŃŃ‚Š¾ŃŃ‚ Š¾ŃŠ¾Š±Š½ŃŠŗŠ¾Š¼ Šø перечислены [в конце страницы](#deprecated-sdk-helpers). Š’ таблице ниже перечислены все ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ возможности, причина, по которой ŠŗŠ°Š¶Š“Š°Ń ŃƒŃ…Š¾Š“ŠøŃ‚, Šø замена, на ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ стоит Š¾ŠæŠøŃ€Š°Ń‚ŃŒŃŃ. @@ -137,13 +137,15 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ŠžŠ“Š½Š° строка в ŠŗŠ¾Š½Ń„ŠøŠ³ŃƒŃ€Š°Ń†ŠøŠø pytest — Šø ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠ¹ вызов уже не сможет незаметно Š²ŠµŃ€Š½ŃƒŃ‚ŃŒŃŃ в ŠŗŠ¾Š“Š¾Š²ŃƒŃŽ базу, не провалив тест. -## Š£ŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Šµ метоГы SDK {#deprecated-sdk-helpers} +## Š£ŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠµ Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Šµ среГства SDK {#deprecated-sdk-helpers} -Это не ŠøŠ·Š¼ŠµŠ½ŠµŠ½ŠøŃ спецификации, а лишь Š²Š½ŃƒŃ‚Ń€ŠµŠ½Š½ŠøŠµ Гетали SDK, у которых ŠæŠ¾ŃŠ²ŠøŠ»Š°ŃŃŒ Š»ŃƒŃ‡ŃˆŠ°Ń замена. ŠžŠ½Šø Š²Ń‹Š“Š°ŃŽŃ‚ то же ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ `MCPDeprecationWarning` Šø Š±ŃƒŠ“ŃƒŃ‚ ŃƒŠ“Š°Š»ŠµŠ½Ń‹ в версии 3.0. +Это не ŠøŠ·Š¼ŠµŠ½ŠµŠ½ŠøŃ спецификации, а лишь способы ŠøŃŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Š½ŠøŃ SDK, у которых ŠæŠ¾ŃŠ²ŠøŠ»Š°ŃŃŒ Š»ŃƒŃ‡ŃˆŠ°Ń замена. ŠžŠ½Šø Š²Ń‹Š“Š°ŃŽŃ‚ то же ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ `MCPDeprecationWarning`, а в версии 3.0 ŠæŃ€ŠµŠ¶Š½ŃŃ форма Š±ŃƒŠ“ет уГалена. | Устарело | Что Š“ŠµŠ»Š°Ń‚ŃŒ вместо ŃŃ‚Š¾Š³Š¾ | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, а затем `FuncMetadata.call_fn()`. Его вызывал Ń‚Š¾Š»ŃŒŠŗŠ¾ коГ, Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠøŠ¹ с `FuncMetadata` Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ (скажем, собственный поГкласс `Tool`). | +| `AuthSettings(resource_server_url=...)` без `validate_token_resource=` | ЗаГайте его: при `True` сервер Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ bearer-токены, которые ваш верификатор не признаёт Š²Ń‹ŠæŃƒŃ‰ŠµŠ½Š½Ń‹Š¼Šø Š“Š»Ń `resource_server_url`; `False` означает, что верификатор сам ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ Š°ŃƒŠ“ŠøŃ‚Š¾Ń€ŠøŃŽ токена (см. **[ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ](run/authorization.md#a-token-verifier)**). ŠŠµŠ·Š°Š“Š°Š½Š½Š¾Šµ значение веГёт ŃŠµŠ±Ń как `False`; в версии 3.0 `True` станет значением по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ Š²ŃŃŠŗŠøŠ¹ раз, когГа заГан `resource_server_url`. | +| `ClientCredentialsOAuthProvider(...)` или `PrivateKeyJWTOAuthProvider(...)` без `issuer=` | ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ `issuer=` с указанием сервера авторизации, который выГал ŃƒŃ‡Ń‘Ń‚Š½Ń‹Šµ Ганные (см. **[ŠŠ°ŠæŠøŃŠ°Š½ŠøŠµ OAuth-клиентов](client/oauth-clients.md#machine-to-machine)**). Без него MCP-сервер сам Ń€ŠµŃˆŠ°ŠµŃ‚, какой сервер авторизации ŠøŃ… ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚; в версии 3.0 ŃŃ‚Š¾Ń‚ именованный Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ станет Š¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¼. | ## Š˜Ń‚Š¾Š³Šø {#recap} @@ -152,7 +154,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Устаревание носит Ń€ŠµŠŗŠ¾Š¼ŠµŠ½Š“Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¹ характер: в переГаваемых Ганных ничего не Š¼ŠµŠ½ŃŠµŃ‚ся, всё проГолжает Ń€Š°Š±Š¾Ń‚Š°Ń‚ŃŒ в ŃŠµŃŃŠøŃŃ… Го 2026 гоГа, Šø ŠæŠ¾ŃŠ²Š»ŃŠµŃ‚ŃŃ заметное ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ `MCPDeprecationWarning` (ŃŃ‚Š¾ `UserWarning`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ оно Š²ŠŗŠ»ŃŽŃ‡ŠµŠ½Š¾ по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ). * Š”ŃŠ¼ŠæŠ»ŠøŃ€Š¾Š²Š°Š½ŠøŃŽ Šø корневым каталогам Š“Š¾ŠæŠ¾Š»Š½ŠøŃ‚ŠµŠ»ŃŒŠ½Š¾ нужен обратный канал, которого в сессии 2026-07-28 нет. ŠŠ° современном ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø они Š²Ń‹Š“Š°ŃŽŃ‚ ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ, а затем Š²Ń‹Š±Ń€Š°ŃŃ‹Š²Š°ŃŽŃ‚ ŠøŃŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` Š·Š°Š³Š»ŃƒŃˆŠ°ŠµŃ‚ Š²ŃŃŽ ŠŗŠ°Ń‚ŠµŠ³Š¾Ń€ŠøŃŽ; `"error::mcp.MCPDeprecationWarning"` в pytest превращает её в провал теста. -* ŠžŠ“ŠøŠ½ Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½Ń‹Š¹ метоГ SDK, `FuncMetadata.call_fn_with_arg_validation()`, Š¾Š±ŃŠŃŠ²Š»ŠµŠ½ ŃƒŃŃ‚Š°Ń€ŠµŠ²ŃˆŠøŠ¼ Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š¾ Šø Š±ŃƒŠ“ŠµŃ‚ ŃƒŠ“Š°Š»Ń‘Š½ в версии 3.0. +* [Š£ŃŃ‚Š°Ń€ŠµŠ²Š°Š½ŠøŃ на ŃƒŃ€Š¾Š²Š½Šµ SDK](#deprecated-sdk-helpers) ŠæŠ¾Š“Ń‡ŠøŠ½ŃŃŽŃ‚ŃŃ Ń‚Š¾Š¼Ńƒ же ŠæŃ€Š°Š²ŠøŠ»Ńƒ: сейчас они ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“Š°ŃŽŃ‚, а в версии 3.0 ŠæŃ€ŠµŠ¶Š½ŃŃ форма Š±ŃƒŠ“ет уГалена. * ŠŠ¾Š²Ń‹Š¹ коГ не ŃŠ»ŠµŠ“ŃƒŠµŃ‚ ŃŃ‚Ń€Š¾ŠøŃ‚ŃŒ ни на оГной ŠøŠ· ŃŃ‚ŠøŃ… возможностей. Все Š¾ŃŃ‚Š°Š»ŃŒŠ½Ń‹Šµ страницы ŃŃ‚Š¾Š¹ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŠø Š¾ŠæŠøŃŃ‹Š²Š°ŃŽŃ‚ Š°ŠŗŃ‚ŃƒŠ°Š»ŃŒŠ½Ń‹Š¹ API. diff --git a/i18n/ru/pages/get-started/first-steps.md b/i18n/ru/pages/get-started/first-steps.md index 0a2d2790..f1ef017b 100644 --- a/i18n/ru/pages/get-started/first-steps.md +++ b/i18n/ru/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # ŠŸŠµŃ€Š²Ń‹Šµ шаги {#first-steps} @@ -17,7 +17,7 @@ translation: * **ŠšŠ»ŠøŠµŠ½Ń‚** живёт Š²Š½ŃƒŃ‚Ń€Šø хоста Šø говорит на MCP. Єост Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ по оГному ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ на кажГый сервер, Šŗ ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½. * **Дервер** — ŃŃ‚Š¾ то, что вы строите с ŠæŠ¾Š¼Š¾Ń‰ŃŒŃŽ ŃŃ‚Š¾Š³Š¾ SDK. ŠžŠ½ ŠæŃ€ŠµŠ“Š¾ŃŃ‚Š°Š²Š»ŃŠµŃ‚ клиентам разные вещи. Š” Š¼Š¾Š“ŠµŠ»ŃŒŃŽ Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ он никогГа не Š¾Š±Ń‰Š°ŠµŃ‚ся. -Š’Ń‹ ŠæŠøŃˆŠµŃ‚Šµ сервер. Єосты — ŃŃ‚Š¾ Ń‡ŃƒŠ¶Š¾Š¹ ŠæŃ€Š¾Š“ŃƒŠŗŃ‚. SDK также Гаёт класс `Client`. ŠžŠ½ ŠæŃ€ŠøŠ³Š¾Š“ŠøŃ‚ŃŃ Š“Š»Ń Ń‚ŠµŃŃ‚ŠøŃ€Š¾Š²Š°Š½ŠøŃ серверов Šø ŠæŠ¾ŃŠ²ŠøŃ‚ŃŃ ниже на ŃŃ‚Š¾Š¹ странице. +Š’Ń‹ ŠæŠøŃˆŠµŃ‚Šµ сервер. Єосты — ŃŃ‚Š¾ Ń‡ŃƒŠ¶Š¾Š¹ ŠæŃ€Š¾Š“ŃƒŠŗŃ‚. SDK также Гаёт класс `Client` — тот же самый, которым хост ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŠ»ŃŃ бы Šŗ ŃŠµŃ€Š²ŠµŃ€Ńƒ по URL или Š·Š°ŠæŃƒŃŃ‚ŠøŠ» бы его как поГпроцесс. ŠžŠ½ ŠæŠ¾ŃŠ²ŠøŃ‚ŃŃ ниже на ŃŃ‚Š¾Š¹ странице, Šø им же вы Š±ŃƒŠ“ŠµŃ‚Šµ Ń‚ŠµŃŃ‚ŠøŃ€Š¾Š²Š°Ń‚ŃŒ свои серверы. ## Три примитива {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector Š·Š°ŠæŃƒŃŃ‚ŠøŠ» ваш сервер через **stdio** — оГин КогГа клиент ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ, сервер Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ свои **возможности**: на какие семейства запросов он Š±ŃƒŠ“ет Š¾Ń‚Š²ŠµŃ‡Š°Ń‚ŃŒ. По ŃŃ‚Š¾Š¼Ńƒ Š¾Š±ŃŠŃŠ²Š»ŠµŠ½ŠøŃŽ клиент Ń€ŠµŃˆŠ°ŠµŃ‚, о чём вообще имеет смысл ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ. Š’Ń‹ его не писали; `MCPServer` Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ его за вас. -ŠŸŠ¾ŃŠ¼Š¾Ń‚Ń€ŠøŃ‚Šµ сами. Класс `Client` ŠøŠ· SDK принимает Š¾Š±ŃŠŠµŠŗŃ‚ сервера Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ Šø ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ нему **в ŠæŠ°Š¼ŃŃ‚Šø** (без поГпроцесса, без порта): +ŠŸŠ¾ŃŠ¼Š¾Ń‚Ń€ŠøŃ‚Šµ сами. ŠžŃŃ‚Š°Š²ŃŒŃ‚Šµ `server.py` Ń€Š°Š±Š¾Ń‚Š°Ń‚ŃŒ по HTTP в оГном терминале: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +а ŠøŠ· Š“Ń€ŃƒŠ³Š¾Š³Š¾ Š½Š°ŠæŃ€Š°Š²ŃŒŃ‚Šµ на него клиент: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,9 +116,9 @@ asyncio.run(main()) ŠžŠ±Ń€Š°Ń‚ŠøŃ‚Šµ внимание на то, чего зГесь нет. `completions` (автоГополнение Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ов Š“Š»Ń шаблонов Ń€ŠµŃŃƒŃ€ŃŠ¾Š² Šø промптов) Ń‚Ń€ŠµŠ±ŃƒŠµŃ‚ обработчика, который ŠæŠøŃˆŠµŃ‚Šµ вы; у ŃŃ‚Š¾Š³Š¾ сервера его нет, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŒ Š¾Ń‚ŃŃƒŃ‚ŃŃ‚Š²ŃƒŠµŃ‚, Šø корректный клиент о ней не спросит. Таково правило Š“Š»Ń всего Š½ŠµŠ¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š¾Š³Š¾: Š·Š°Ń€ŠµŠ³ŠøŃŃ‚Ń€ŠøŃ€ŃƒŠ¹Ń‚Šµ нужное — Šø Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŒ ŠæŠ¾ŃŠ²ŠøŃ‚ŃŃ; страница **[АвтоГополнение](../servers/completions.md)** ŃŃ‚Š¾ Š“ŠµŠ¼Š¾Š½ŃŃ‚Ń€ŠøŃ€ŃƒŠµŃ‚. !!! info - `Client(mcp)` — тот самый клиент в ŠæŠ°Š¼ŃŃ‚Šø, которым Ń‚ŠµŃŃ‚ŠøŃ€ŃƒŠµŃ‚ŃŃ кажГый пример в ŃŃ‚Š¾Š¹ - Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†ŠøŠø, Šø именно так вы Š±ŃƒŠ“ŠµŃ‚Šµ Ń‚ŠµŃŃ‚ŠøŃ€Š¾Š²Š°Ń‚ŃŒ свои. Š•Š¼Ńƒ отвеГена Ń†ŠµŠ»Š°Ń страница: - **[Тестирование](testing.md)**. + Этот `client.py` — полноценный MCP-клиент, Šø ему ŠæŠ¾ŃŠ²ŃŃ‰ŠµŠ½Š° страница **[ŠšŠ»ŠøŠµŠ½Ń‚](../client/index.md)**. + Š’ тесте терминал Šø порт не Š½ŃƒŠ¶Š½Ń‹: переГайте `Client` сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера — + `Client(mcp)`. Š­Ń‚Š¾Š¼Ńƒ тоже отвеГена Ń†ŠµŠ»Š°Ń страница: **[Тестирование](testing.md)**. ## Чего вы не писали {#what-you-did-not-write} @@ -129,7 +127,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int` — ŃŃ‚Š¾ *Šø ŠµŃŃ‚ŃŒ* схема Š“Š»Ń `add`. * ŠžŠ±Ń€Š°Š±Š¾Ń‚Ń‡ŠøŠŗ запросов. `tools/list`, `resources/read`, `prompts/get` — всё ŃŃ‚Š¾ Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ŃŃ за вас. * ŠžŠ±ŃŠŃŠ²Š»ŠµŠ½ŠøŠµ возможностей. `MCPServer` составил его за вас. -* ŠŠø строчки протокола. Догласование версии, обрамление JSON-RPC, обмен Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŠ¼Šø — всё ŃŃ‚Š¾ ŠæŃ€Š¾ŠøŠ·Š¾ŃˆŠ»Š¾ Š²Š½ŃƒŃ‚Ń€Šø `mcp dev` Šø `Client(mcp)`, Šø вы ŃŃ‚Š¾Š³Š¾ не виГели. +* ŠŠø строчки протокола. Догласование версии, обрамление JSON-RPC, обмен Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŠ¼Šø — всё ŃŃ‚Š¾ ŠæŃ€Š¾ŠøŠ·Š¾ŃˆŠ»Š¾ Š²Š½ŃƒŃ‚Ń€Šø `mcp dev` Šø `client.py`, Šø вы ŃŃ‚Š¾Š³Š¾ не виГели. Š’ ŃŃ‚Š¾Š¼ ŃŠ¾Š¾Ń‚Š½Š¾ŃˆŠµŠ½ŠøŠø — весь смысл SDK. @@ -140,6 +138,6 @@ asyncio.run(main()) * По оГному Š“ŠµŠŗŠ¾Ń€Š°Ń‚Š¾Ń€Ńƒ на примитив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Š˜Š¼Ń, описание Šø схема Š±ŠµŃ€ŃƒŃ‚ся ŠøŠ· Ń„ŃƒŠ½ŠŗŃ†ŠøŠø. * URI с `{param}` созГаёт **шаблон** Ń€ŠµŃŃƒŃ€ŃŠ°, который Š¾Ń‚Š¾Š±Ń€Š°Š¶Š°ŠµŃ‚ŃŃ Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š¾ от конкретных Ń€ŠµŃŃƒŃ€ŃŠ¾Š². * **Возможности** сервера Š¾Š±ŃŠŃŠ²Š»ŃŃŽŃ‚ся за вас, а клиент ŃŠæŃ€Š°ŃˆŠøŠ²Š°ŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ о том, что сервер Š¾Š±ŃŠŃŠ²ŠøŠ». -* `Client(mcp)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ Š¾Š±ŃŠŠµŠŗŃ‚Ńƒ сервера в ŠæŠ°Š¼ŃŃ‚Šø: ваш тестовый стенГ с первого Š“Š½Ń. +* `Client("http://localhost:8000/mcp")` разговаривает с Š·Š°ŠæŃƒŃ‰ŠµŠ½Š½Ń‹Š¼ сервером. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ ему вместо ŃŃ‚Š¾Š³Š¾ сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера, `Client(mcp)`, — Šø ŃŃ‚Š¾ ваш тестовый стенГ с первого Š“Š½Ń. Š”Š°Š»ŃŒŃˆŠµ — **[ŠŸŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ Šŗ Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ Ń…Š¾ŃŃ‚Ńƒ](real-host.md)**: ŃŃ‚Š¾Ń‚ же сервер Š²Š½ŃƒŃ‚Ń€Šø Claude Desktop или IDE, по-Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ. Затем **[Тестирование](testing.md)**: оГна страница, оГин клиент в ŠæŠ°Š¼ŃŃ‚Šø — Šø больше не ŠæŃ€ŠøŠ“Ń‘Ń‚ŃŃ Š³Š°Š“Š°Ń‚ŃŒ, работает ли оно. После ŃŃ‚Š¾Š³Š¾ кажГому ŠæŃ€ŠøŠ¼ŠøŃ‚ŠøŠ²Ńƒ отвеГена ŃŠ²Š¾Ń страница, Š½Š°Ń‡ŠøŠ½Š°Ń с того, которым ŃƒŠæŃ€Š°Š²Š»ŃŠµŃ‚ моГель: **[Š˜Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń‹](../servers/tools.md)**. diff --git a/i18n/ru/pages/get-started/testing.md b/i18n/ru/pages/get-started/testing.md index 1f4d623e..24a0a0ad 100644 --- a/i18n/ru/pages/get-started/testing.md +++ b/i18n/ru/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Тестирование {#testing} -Š’ Python SDK ŠµŃŃ‚ŃŒ класс `Client` со **встроенным in-memory транспортом**: переГайте ему Š¾Š±ŃŠŠµŠŗŃ‚ сервера, Šø он ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŃ Šŗ нему Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ. +Класс `Client` ŠøŠ· SDK — тот самый, что ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ URL или Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ поГпроцесс, — ŃƒŠ¼ŠµŠµŃ‚ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°Ń‚ŃŒŃŃ Šø **в ŠæŠ°Š¼ŃŃ‚Šø**: переГайте ему Š¾Š±ŃŠŠµŠŗŃ‚ сервера, Šø он Š±ŃƒŠ“ет Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ с ним Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ. -ŠŠø поГпроцесса. ŠŠø порта. Вообще никакого транспорта. Та же ŠøŠ“ŠµŃ, что Šø `TestClient` в FastAPI. +ŠŠø поГпроцесса. ŠŠø порта. ŠŠøŃ‡ŠµŠ³Š¾ не ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ по сети. Та же ŠøŠ“ŠµŃ, что Šø `TestClient` в FastAPI. ## Базовое использование {#basic-usage} @@ -97,7 +97,7 @@ async def test_call_add_tool(client: Client): Š’ тестах Š¾ŃŃ‚Š°Š²Š»ŃŠ¹Ń‚Šµ его Š²ŠŗŠ»ŃŽŃ‡Ń‘нным. Š’ ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½-коГе он не имеет смысла. -## Š’Š½ŃƒŃ‚Ń€Šø процесса по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ {#in-process-by-default} +## Без ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠø Šŗ ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃŽ протокола по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ {#era-neutral-by-default} !!! note `Client(mcp)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Š²Š½ŃƒŃ‚Ń€Šø процесса Šø по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ **не ŠæŃ€ŠøŠ²ŃŠ·Š°Š½ Šŗ ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃŽ diff --git a/i18n/ru/pages/handlers/multi-round-trip.md b/i18n/ru/pages/handlers/multi-round-trip.md index 2c0ba395..172e2c33 100644 --- a/i18n/ru/pages/handlers/multi-round-trip.md +++ b/i18n/ru/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # ŠœŠ½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń‹Šµ запросы (multi-round-trip) {#multi-round-trip-requests} @@ -164,7 +164,7 @@ TTL, ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ° Šŗ ŠæŃ€ŠøŠ½Ń†ŠøŠæŠ°Š»Ńƒ Šø ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ° Šŗ запро ## Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ версии 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ в версии протокола **2026-07-28**. `Client(server)` в ŠæŠ°Š¼ŃŃ‚Šø ŃŠ¾Š³Š»Š°ŃŃƒŠµŃ‚ её за вас; по сети её Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ `mode="auto"`. После ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ `client.protocol_version` сообщает, что именно ŠæŠ¾Š»ŃƒŃ‡ŠøŠ»Š¾ŃŃŒ. +`InputRequiredResult` ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ в версии протокола **2026-07-28**. Режим `mode="auto"`, который `Client` ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ, Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ её на Š»ŃŽŠ±Š¾Š¼ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø. После ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ `client.protocol_version` сообщает, что именно ŠæŠ¾Š»ŃƒŃ‡ŠøŠ»Š¾ŃŃŒ. !!! warning Š’ сессии Го 2026 гоГа `InputRequiredResult` некуГа ŠæŠ¾Š»Š¾Š¶ŠøŃ‚ŃŒ. Верните его ŠøŠ· обработчика на diff --git a/i18n/ru/pages/handlers/progress.md b/i18n/ru/pages/handlers/progress.md index 77fcb71f..4bea42be 100644 --- a/i18n/ru/pages/handlers/progress.md +++ b/i18n/ru/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # ЄоГ Š²Ń‹ŠæŠ¾Š»Š½ŠµŠ½ŠøŃ {#progress} @@ -29,19 +29,17 @@ translation: ŠšŠ»ŠøŠµŠ½Ń‚ ŠæŠ¾Š“ŠæŠøŃŃ‹Š²Š°ŠµŃ‚ŃŃ **на кажГый вызов Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š¾**, ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń `progress_callback=` в `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,29 +54,32 @@ anyio.run(main) ŠšŠ¾Š»Š±ŃŠŗ — ŃŃ‚Š¾ `async`-Ń„ŃƒŠ½ŠŗŃ†ŠøŃ, ŠæŃ€ŠøŠ½ŠøŠ¼Š°ŃŽŃ‰Š°Ń ровно то, что сообщил сервер: `progress`, `total`, `message`. !!! info - `Client(mcp)` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ Šŗ Š¾Š±ŃŠŠµŠŗŃ‚Ńƒ сервера, в ŠæŠ°Š¼ŃŃ‚Šø, — ŃŃ‚Š¾ тот же клиент, на котором - построена страница **[Тестирование](../get-started/testing.md)**. ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€ `progress_callback` оГин - Šø тот же, какой бы транспорт ни использовал `Client`; а вот *временны́е характеристики*, которые вы - сейчас ŃƒŠ²ŠøŠ“ŠøŃ‚Šµ, Š¾Ń‚Š½Š¾ŃŃŃ‚ŃŃ Šŗ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃŽ в ŠæŠ°Š¼ŃŃ‚Šø. ŠžŠ½Š¾ Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ ŠŗŠ¾Š»Š±ŃŠŗ ŠæŃ€ŃŠ¼Š¾ на месте, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ - кажГый отчёт прихоГит Го того, как `call_tool` вернёт ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ. По Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ - ŃƒŠ²ŠµŠ“Š¾Š¼Š»ŠµŠ½ŠøŃ ŃŠ¾Ń€ŠµŠ²Š½ŃƒŃŽŃ‚ŃŃ с Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š¾Š¼, Šø меГленный ŠŗŠ¾Š»Š±ŃŠŗ может всё ещё Ń€Š°Š±Š¾Ń‚Š°Ń‚ŃŒ после того, как - `call_tool` уже Š²ŠµŃ€Š½ŃƒŠ» ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ. + ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€ `progress_callback` оГин Šø тот же, что бы вы ни переГали в `Client`: URL, как зГесь, + `StdioServerParameters` или Š¾Š±ŃŠŠµŠŗŃ‚ сервера в тесте. ŠŠ¾ по Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ ŃƒŃ‡ŠøŃ‚Ń‹Š²Š°Š¹Ń‚Šµ + временны́е характеристики. КажГое увеГомление Š“Š¾ŃŃ‚Š°Š²Š»ŃŠµŃ‚ŃŃ само по себе, Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Š¾ от ответа, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ + меГленный ŠŗŠ¾Š»Š±ŃŠŗ может всё ещё Ń€Š°Š±Š¾Ń‚Š°Ń‚ŃŒ после того, как `call_tool` уже Š²ŠµŃ€Š½ŃƒŠ» ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ. Только + Š²Š½ŃƒŃ‚Ń€ŠøŠæŃ€Š¾Ń†ŠµŃŃŠ½Š¾Šµ тестовое ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ ŠŗŠ¾Š»Š±ŃŠŗ ŠæŃ€ŃŠ¼Š¾ на месте Šø Š³Š°Ń€Š°Š½Ń‚ŠøŃ€ŃƒŠµŃ‚, что кажГый + отчёт приГёт Ń€Š°Š½ŃŒŃˆŠµ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š°. ### ŠŸŠ¾ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ сами {#try-it} -ŠŸŠ¾Š»Š¾Š¶ŠøŃ‚Šµ `client.py` Ń€ŃŠ“Š¾Š¼ с `server.py` Šø Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ: +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `server.py` по HTTP, затем ŠøŠ· второго терминала Š·Š°ŠæŃƒŃŃ‚ите клиент: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -ŠšŠ°Š¶Š“Ń‹Š¹ `await ctx.report_progress(...)` на сервере ŠæŃ€ŠµŠ²Ń€Š°Ń‚ŠøŠ»ŃŃ в оГин вызов `show` на клиенте, в том же ŠæŠ¾Ń€ŃŠ“ŠŗŠµ, Šø обе строки Š½Š°ŠæŠµŃ‡Š°Ń‚Š°Š»ŠøŃŃŒ **Го** того, как `call_tool` Š²ŠµŃ€Š½ŃƒŠ» ŃƒŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ. ЄоГ Š²Ń‹ŠæŠ¾Š»Š½ŠµŠ½ŠøŃ не ŃƒŠæŠ°ŠŗŠ¾Š²Ń‹Š²Š°ŠµŃ‚ŃŃ в Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚: он ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ потоком, пока ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ ещё работает. +ŠšŠ°Š¶Š“Ń‹Š¹ `await ctx.report_progress(...)` на сервере ŠæŃ€ŠµŠ²Ń€Š°Ń‚ŠøŠ»ŃŃ в оГин вызов `show` на клиенте, в том же ŠæŠ¾Ń€ŃŠ“ŠŗŠµ. ЄоГ Š²Ń‹ŠæŠ¾Š»Š½ŠµŠ½ŠøŃ не ŃƒŠæŠ°ŠŗŠ¾Š²Ń‹Š²Š°ŠµŃ‚ŃŃ в Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. ŠžŠ½ ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ потоком, пока ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ ещё работает. !!! warning `progress_callback` Š¾Ń‚Š½Š¾ŃŠøŃ‚ŃŃ Šŗ **Š²Ń‹Š·Š¾Š²Ńƒ**, а не Šŗ `Client`. ŠŃ€Š³ŃƒŠ¼ŠµŠ½Ń‚Š° ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š° Š“Š»Ń него нет, diff --git a/i18n/ru/pages/protocol-versions.md b/i18n/ru/pages/protocol-versions.md index 195d3026..719373e0 100644 --- a/i18n/ru/pages/protocol-versions.md +++ b/i18n/ru/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Версии протокола {#protocol-versions} @@ -11,9 +11,17 @@ translation: Š—Š°Š±Š¾Ń‚ŠøŃ‚ŃŒŃŃ об ŃŃ‚Š¾Š¼ почти никогГа не ŠæŃ€ŠøŃ…Š¾Š“ŠøŃ‚ŃŃ: `Client` Š“Š¾Š³Š¾Š²Š°Ń€ŠøŠ²Š°ŠµŃ‚ŃŃ за вас. Эта страница — об оГном Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚е ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, который ŃŃ‚ŠøŠ¼ ŃƒŠæŃ€Š°Š²Š»ŃŠµŃ‚, `mode=`, Šø о трёх ŃŠ»ŃƒŃ‡Š°ŃŃ…, когГа его Š¼ŠµŠ½ŃŃŽŃ‚. +ŠšŠ°Š¶Š“Ń‹Š¹ фрагмент на ŃŃ‚Š¾Š¹ странице — ŃŃ‚Š¾ `client.py`, который Š¾Š±Ń‰Š°ŠµŃ‚ся с сервером Bookshop `server.py` со страницы **[ŠšŠ»ŠøŠµŠ½Ń‚](client/index.md)**. Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ ŃŃ‚Š¾Ń‚ сервер в оГном терминале: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Затем Š·Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚е кажГый фрагмент во втором терминале команГой `python client.py`. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ translation: Вот Šø Š²ŃŃ механика. ŠžŠ“ŠøŠ½ `Client`, сервер Š»ŃŽŠ±Š¾Š³Š¾ ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ, никаких ветвлений в коГе. !!! info - `MCPServer` отвечает на `server/discover` на Š»ŃŽŠ±Š¾Š¼ транспорте — в ŠæŠ°Š¼ŃŃ‚Šø, stdio, Streamable - HTTP, — ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ с собственным сервером `auto` всегГа прихоГит Šŗ `2026-07-28`. ŠžŃ‚ŠŗŠ°Ń‚ - срабатывает Ń‚Š¾Š»ŃŒŠŗŠ¾ с Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¼ сервером Го 2026 гоГа — ровно тогГа, когГа он Šø нужен. + `MCPServer` отвечает на `server/discover` на Š»ŃŽŠ±Š¾Š¼ транспорте — Streamable HTTP, stdio Šø + Š²Š½ŃƒŃ‚Ń€ŠøŠæŃ€Š¾Ń†ŠµŃŃŠ½Š¾Š¼ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø, которое ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŃŽŃ‚ ваши тесты, — ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ с собственным сервером + `auto` всегГа прихоГит Šŗ `2026-07-28`. ŠžŃ‚ŠŗŠ°Ń‚ срабатывает Ń‚Š¾Š»ŃŒŠŗŠ¾ с Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¼ сервером Го + 2026 гоГа — ровно тогГа, когГа он Šø нужен. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ translation: `mode` принимает Šø ŃŃ‚Ń€Š¾ŠŗŃƒ современной версии протокола. Š”ŠµŠ³Š¾Š“Š½Ń ŃŃ‚Š¾ множество ровно `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 Так что сохраните его. После ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ в режиме `auto` в `client.session.discover_result` лежит ровно тот `DiscoverResult`, который прислал сервер: его `supported_versions`, `capabilities`, `instructions` Šø иГентификационные Ганные, которые сервер записал в `_meta` Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š°. Š’ ŃŠ»ŠµŠ“ŃƒŃŽŃ‰ŠøŠ¹ раз переГайте его обратно как `prior_discover=`: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/ru/pages/run/asgi.md b/i18n/ru/pages/run/asgi.md index 8fd397b4..bacb4f75 100644 --- a/i18n/ru/pages/run/asgi.md +++ b/i18n/ru/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # Добавление в ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŃŽŃ‰ŠµŠµ приложение {#add-to-an-existing-app} @@ -102,7 +102,7 @@ uvicorn server:app --8<-- "docs_src/asgi/tutorial004.py" ``` -Š¢ŠµŠæŠµŃ€ŃŒ клиенты ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŃŽŃ‚ŃŃ Šŗ `/notes`, а не Šŗ `/notes/mcp`. +Š¢ŠµŠæŠµŃ€ŃŒ клиенты ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŃŽŃ‚ŃŃ Šŗ `/notes/`, а не Šŗ `/notes/mcp`. ## CORS Š“Š»Ń Š±Ń€Š°ŃƒŠ·ŠµŃ€Š½Ń‹Ń… клиентов {#cors-for-browser-clients} @@ -137,7 +137,7 @@ uvicorn server:app ## Š˜Ń‚Š¾Š³Šø {#recap} * `mcp.streamable_http_app()` возвращает приложение Starlette с оГним Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ом, `/mcp`. Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚ŃŒ его может Š»ŃŽŠ±Š¾Š¹ ASGI-сервер. -* По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ приложение отвечает Ń‚Š¾Š»ŃŒŠŗŠ¾ на запросы, аГресованные localhost, а за Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¼ именем хоста Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ всё коГом `421`, пока вы не переГаГите список Ń€Š°Š·Ń€ŠµŃˆŃ‘Š½Š½Ń‹Ń… хостов в `transport_security=`. За ŃŃ‚Š¾ Šø за Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Š¹ ŠæŃƒŃ‚ŃŒ Šŗ ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Ńƒ отвечает страница **[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](deploy.md)**. +* По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ приложение отвечает Ń‚Š¾Š»ŃŒŠŗŠ¾ на запросы, аГресованные localhost, а за Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¼ именем хоста Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ всё коГом `421`, пока вы не переГаГите в `transport_security=` список Ń€Š°Š·Ń€ŠµŃˆŃ‘Š½Š½Ń‹Ń… хостов. За ŃŃ‚Š¾ Šø за Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Š¹ ŠæŃƒŃ‚ŃŒ Šŗ ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Ńƒ отвечает страница **[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](deploy.md)**. * `Mount` (или `Host`) помещает его Š²Š½ŃƒŃ‚Ń€ŃŒ более ŠŗŃ€ŃƒŠæŠ½Š¾Š³Š¾ ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ Starlette или FastAPI. * **ŠœŠ¾Š½Ń‚ŠøŃ€Š¾Š²Š°Š½ŠøŠµ Š¾Ń‚ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ встроенный жизненный цикл.** Жизненный цикл хост-ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ Голжен войти в `mcp.session_manager.run()`, иначе первый запрос Š·Š°Š²ŠµŃ€ŃˆŠøŃ‚ŃŃ ошибкой. * ŠŠµŃŠŗŠ¾Š»ŃŒŠŗŠ¾ серверов в оГном приложении — ŃŃ‚Š¾ несколько монтирований Šø оГин жизненный цикл, который вхоГит в кажГый менеГжер сессий. @@ -145,4 +145,4 @@ uvicorn server:app * Š‘Ń€Š°ŃƒŠ·ŠµŃ€Š½Ń‹Š¼ клиентам нужен CORS: `allow_headers` Š“Š»Ń заголовков запроса `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` Š“Š»Ń ответа. * `@mcp.custom_route()` Š“Š¾Š±Š°Š²Š»ŃŠµŃ‚ обычные HTTP-точки без Š°ŃƒŃ‚ентификации Ń€ŃŠ“Š¾Š¼ с `/mcp`. -КогГа сервер Š“Š¾ŃŃ‚ŃƒŠæŠµŠ½ по Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ URL, **[ŠšŠ»ŠøŠµŠ½Ń‚](../client/index.md)** ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ нему по ŃŃ‚Š¾Š¼Ńƒ URL вместо Š¾Š±ŃŠŠµŠŗŃ‚а сервера. +КогГа сервер Š“Š¾ŃŃ‚ŃƒŠæŠµŠ½ по Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ¼Ńƒ URL, **[ŠšŠ»ŠøŠµŠ½Ń‚](../client/index.md)** ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ нему по ŃŃ‚Š¾Š¼Ńƒ URL. diff --git a/i18n/ru/pages/run/authorization.md b/i18n/ru/pages/run/authorization.md index c4059c1a..68dd7b77 100644 --- a/i18n/ru/pages/run/authorization.md +++ b/i18n/ru/pages/run/authorization.md @@ -1,19 +1,19 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ {#authorization} -Через Streamable HTTP MCP-сервер — ŃŃ‚Š¾ обычный веб-сервис, Šø Š·Š°Ń‰ŠøŃ‰Š°ŠµŃ‚ŃŃ он так же, как Š»ŃŽŠ±Š¾Š¹ веб-сервис: с ŠæŠ¾Š¼Š¾Ń‰ŃŒŃŽ bearer-токенов OAuth 2.1. +ŠŸŃ€Šø работе через Streamable HTTP ваш MCP-сервер — обычный веб-сервис, Šø Š·Š°Ń‰ŠøŃ‰Š°ŃŽŃ‚ его так же, как Š»ŃŽŠ±Š¾Š¹ веб-сервис: bearer-токенами OAuth 2.1. -Š’ терминах OAuth ваш сервер — ŃŃ‚Š¾ **сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š²**. ŠžŠ½ никого не Š°ŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŃ†ŠøŃ€ŃƒŠµŃ‚ Šø не выГаёт токенов. ŠžŠ½ Гелает ровно оГно: смотрит на заголовок `Authorization` кажГого запроса Šø Ń€ŠµŃˆŠ°ŠµŃ‚, Š³Š¾Š“ŠøŃ‚ŃŃ ли токен в нём. +Š’ терминах OAuth ваш сервер — ŃŃ‚Š¾ **сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š²**. ŠžŠ½ никогГа не Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ вхоГ ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŠµŠ¹ Šø никогГа не выГаёт токены. ŠžŠ½ Гелает оГно: смотрит на заголовок `Authorization` в кажГом запросе Šø Ń€ŠµŃˆŠ°ŠµŃ‚, Š³Š¾Š“ŠøŃ‚ŃŃ ли токен в нём. -Эта страница — о серверной стороне. ŠšŠ»ŠøŠµŠ½Ń‚, который Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ ваш сервер авторизации Šø ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ токен, описан на странице **[OAuth-клиенты](../client/oauth-clients.md)**. +Эта страница — о серверной стороне. ŠšŠ»ŠøŠµŠ½Ń‚, который нахоГит ваш сервер авторизации Šø ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ токен, описан на странице **[OAuth-клиенты](../client/oauth-clients.md)**. ## Три стороны {#the-three-parties} -* **Дервер авторизации** Š°ŃƒŃ‚ŠµŠ½Ń‚ŠøŃ„ŠøŃ†ŠøŃ€ŃƒŠµŃ‚ ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŠµŠ¹ Šø выГаёт токены Š“Š¾ŃŃ‚ŃƒŠæŠ°. Его вы не ŠæŠøŃˆŠµŃ‚Šµ. Это ваш провайГер иГентификации (Auth0, Keycloak, Entra или ваш собственный). +* **Дервер авторизации** Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ вхоГ ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŠµŠ¹ Šø выГаёт токены Š“Š¾ŃŃ‚ŃƒŠæŠ°. Его вы не ŠæŠøŃˆŠµŃ‚Šµ. Это ваш провайГер иГентификации (Auth0, Keycloak, Entra или собственный). * **Дервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š²** — ŃŃ‚Š¾ ваш MCP-сервер. ŠžŠ½ ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ токен в кажГом запросе. * **ŠšŠ»ŠøŠµŠ½Ń‚** Š²Ń‹ŃŃŠ½ŃŠµŃ‚, какому ŃŠµŃ€Š²ŠµŃ€Ńƒ авторизации вы Š“Š¾Š²ŠµŃ€ŃŠµŃ‚Šµ, ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ у него токен Šø присылает его вам в виГе `Authorization: Bearer `. @@ -21,29 +21,33 @@ translation: ## Верификатор токенов {#a-token-verifier} -SDK ничего не преГполагает о том, как Š²Ń‹Š³Š»ŃŠ“ŠøŃ‚ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Š¹ токен. Это Š¾ŠæŃ€ŠµŠ“ŠµŠ»ŃŠµŃ‚Šµ вы, Ń€ŠµŠ°Š»ŠøŠ·ŃƒŃ **`TokenVerifier`**: +Š£ SDK нет Š¼Š½ŠµŠ½ŠøŃ о том, как Š²Ń‹Š³Š»ŃŠ“ŠøŃ‚ Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Š¹ токен. Это сообщаете вы, Ń€ŠµŠ°Š»ŠøŠ·ŃƒŃ **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` -* `TokenVerifier` — ŃŃ‚Š¾ протокол с оГним асинхронным метоГом. `verify_token` ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ сырой токен ŠøŠ· заголовка `Authorization` Šø возвращает **`AccessToken`**, если токен Гействителен, или `None`, если нет. Š‘Š¾Š»ŃŒŃˆŠµ Ń€ŠµŠ°Š»ŠøŠ·Š¾Š²Ń‹Š²Š°Ń‚ŃŒ нечего. -* Этот верификатор ищет токен в таблице. ŠŠ°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ поГпись JWT или Š¾Š±Ń€Š°Ń‰Š°ŠµŃ‚ся Šŗ ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Ńƒ интроспекции токенов на сервере авторизации. Этот коГ — ваш; SDK его Ń‚Š¾Š»ŃŒŠŗŠ¾ вызывает. +* `TokenVerifier` — протокол с оГним асинхронным метоГом. `verify_token` ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ сырой токен ŠøŠ· заголовка `Authorization` Šø возвращает **`AccessToken`**, если токен Гействителен, Šø `None`, если нет. Š‘Š¾Š»ŃŒŃˆŠµ Ń€ŠµŠ°Š»ŠøŠ·Š¾Š²Ń‹Š²Š°Ń‚ŃŒ нечего. +* Этот верификатор ищет токен в таблице; в кажГой записи указан Ń€ŠµŃŃƒŃ€Ń, Š“Š»Ń которого токен выГан. ŠŠ°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ поГпись JWT или вызывает ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚ интроспекции токенов сервера авторизации Šø сообщает, Š“Š»Ń кого выГан токен (его `aud`), в `AccessToken.resource`. Этот коГ ŠæŠøŃˆŠµŃ‚Šµ вы; SDK его Ń‚Š¾Š»ŃŒŠŗŠ¾ вызывает. * `token_verifier=` Šø `auth=` всегГа ŠøŠ“ŃƒŃ‚ в паре. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ оГин без Š“Ń€ŃƒŠ³Š¾Š³Š¾ — Šø `MCPServer(...)` выбросит `ValueError` ещё Го того, как Š¾Š±ŃŠ»ŃƒŠ¶ŠøŃ‚ Ń…Š¾Ń‚ŃŒ оГин запрос. -`AuthSettings` — ŃŃ‚Š¾ ŠæŃƒŠ±Š»ŠøŃ‡Š½Š¾Šµ лицо вашего сервера Ń€ŠµŃŃƒŃ€ŃŠ¾Š²: +`AuthSettings` — ŠæŃƒŠ±Š»ŠøŃ‡Š½Š¾Šµ лицо вашего сервера Ń€ŠµŃŃƒŃ€ŃŠ¾Š²: * `issuer_url`: сервер авторизации, который выГаёт ваши токены. -* `resource_server_url`: ŠæŃƒŠ±Š»ŠøŃ‡Š½Ń‹Š¹ URL ŃŃ‚Š¾Š³Š¾ MCP-ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Š°. ŠžŠ½ ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚, *Š“Š»Ń какого* Ń€ŠµŃŃƒŃ€ŃŠ° преГназначен токен, Šø по нему же Ń€Š°Š·Š¼ŠµŃ‰Š°ŠµŃ‚ŃŃ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŃ. +* `resource_server_url`: ŠæŃƒŠ±Š»ŠøŃ‡Š½Ń‹Š¹ URL ŃŃ‚Š¾Š³Š¾ MCP-ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Š°. ŠžŠ½ ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚, Š“Š»Ń *какого* Ń€ŠµŃŃƒŃ€ŃŠ° преГназначен токен, Šø по нему же Ń€Š°ŃŠæŠ¾Š»Š°Š³Š°ŠµŃ‚ŃŃ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŃ. * `required_scopes`: кажГый токен Голжен ŃŠ¾Š“ŠµŃ€Š¶Š°Ń‚ŃŒ ŠøŃ… все. +* `validate_token_resource`: Š¾Ń‚ŠŗŠ»Š¾Š½ŃŃ‚ŃŒ Š»ŃŽŠ±Š¾Š¹ токен, у которого `AccessToken.resource` не равен `resource_server_url`. Если Š¾ŃŃ‚Š°Š²ŠøŃ‚ŃŒ его незаГанным при заГанном `resource_server_url`, Š²Ń‹Š“Š°Ń‘Ń‚ŃŃ ŠæŃ€ŠµŠ“ŃƒŠæŃ€ŠµŠ¶Š“ŠµŠ½ŠøŠµ (`MCPDeprecationWarning`), а повеГение такое же, как при `False`; в версии 3.0 значением по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ Š“Š»Ń серверов Ń€ŠµŃŃƒŃ€ŃŠ¾Š² станет `True`. + * Š’ŠŗŠ»ŃŽŃ‡ŠøŃ‚Šµ его, если ваш сервер авторизации ŠæŃ€ŠøŠ²ŃŠ·Ń‹Š²Š°ŠµŃ‚ токены Šŗ ŠæŠ°Ń€Š°Š¼ŠµŃ‚Ń€Ńƒ `resource`, который запросил клиент, — MCP-клиенты ŠæŠµŃ€ŠµŠ“Š°ŃŽŃ‚ его всегГа. ДлеГите, чтобы `resource_server_url` в точности совпаГал с URL, Šŗ ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŃŽŃ‚ŃŃ клиенты. + * ŠžŃŃ‚Š°Š²ŃŒŃ‚Šµ Š²Ń‹ŠŗŠ»ŃŽŃ‡ŠµŠ½Š½Ń‹Š¼, если сервер авторизации ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ собственные иГентификаторы Š°ŃƒŠ“ŠøŃ‚Š¾Ń€ŠøŠø (иГентификатор API в Auth0, иГентификатор ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ в Entra), Šø вместо ŃŃ‚Š¾Š³Š¾ ŠæŃ€Š¾Š²ŠµŃ€ŃŠ¹Ń‚Šµ `aud` в верификаторе, Š²Š¾Š·Š²Ń€Š°Ń‰Š°Ń `None` Š“Š»Ń токена, преГназначенного не ŃŃ‚Š¾Š¼Ńƒ ŃŠµŃ€Š²ŠµŃ€Ńƒ. + * Если `aud` — список, поместите в `resource` тот ŃŠ»ŠµŠ¼ŠµŠ½Ń‚, который равен `resource_server_url`. !!! tip - Š’ `examples/servers/simple-auth/` в репозитории SDK ŠµŃŃ‚ŃŒ `IntrospectionTokenVerifier`, который Š¾Š±Ń€Š°Ń‰Š°ŠµŃ‚ся - Šŗ ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Ńƒ [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ³Š¾ сервера авторизации. Именно так ŃƒŃŃ‚Ń€Š¾ŠµŠ½Š¾ Š±Š¾Š»ŃŒŃˆŠøŠ½ŃŃ‚Š²Š¾ верификаторов в ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½Šµ. + Š’ репозитории SDK, в `examples/servers/simple-auth/`, ŠµŃŃ‚ŃŒ `IntrospectionTokenVerifier`, который Š¾Š±Ń€Š°Ń‰Š°ŠµŃ‚ся + Šŗ ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Ńƒ [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) Š½Š°ŃŃ‚Š¾ŃŃ‰ŠµŠ³Š¾ сервера авторизации. Так ŃƒŃŃ‚Ń€Š¾ŠµŠ½Š¾ Š±Š¾Š»ŃŒŃˆŠøŠ½ŃŃ‚Š²Š¾ верификаторов в Ń€ŠµŠ°Š»ŃŒŠ½Ń‹Ń… Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Š½ŠøŃŃ…. -## Что вы ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚Šµ через HTTP {#what-you-get-over-http} +## Что ŠæŠ¾ŃŠ²Š»ŃŠµŃ‚ŃŃ по HTTP {#what-you-get-over-http} -ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ живёт в HTTP-заголовках, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ на HTTP-транспортах. Š—Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ её на том транспорте, который развёртываете: `mcp.run(transport="streamable-http")` поГнимает её на `http://127.0.0.1:8000/mcp`, а Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ — на странице **[Š—Š°ŠæŃƒŃŠŗ сервера](index.md)**. Š¢ŠµŠæŠµŃ€ŃŒ у ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ Гва Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚а: +ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ живёт в HTTP-заголовках, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŃŃƒŃ‰ŠµŃŃ‚Š²ŃƒŠµŃ‚ Ń‚Š¾Š»ŃŒŠŗŠ¾ на HTTP-транспортах. Š—Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ её на том, который развёртываете: `mcp.run(transport="streamable-http")` поГнимает сервер на `http://127.0.0.1:8000/mcp`, а Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ — на странице **[Š—Š°ŠæŃƒŃŠŗ сервера](index.md)**. Š¢ŠµŠæŠµŃ€ŃŒ у ŠæŃ€ŠøŠ»Š¾Š¶ŠµŠ½ŠøŃ Гва Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚а: ```text /mcp @@ -65,11 +69,11 @@ SDK ничего не преГполагает о том, как Š²Ń‹Š³Š»ŃŠ“Šø } ``` -Именно по ŃŃ‚Š¾Š¼Ńƒ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Ńƒ клиент, который никогГа не ŃŠ»Ń‹ŃˆŠ°Š» о вашем сервере, нахоГит Šŗ нему Š“Š¾Ń€Š¾Š³Ńƒ: читает `authorization_servers` Šø иГёт Ń‚ŃƒŠ“Š° за токеном. ŠŠøŃ‡ŠµŠ³Š¾ ŠøŠ· ŃŃ‚Š¾Š³Š¾ вы не писали. +По ŃŃ‚Š¾Š¼Ńƒ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Ńƒ клиент, никогГа не ŃŠ»Ń‹ŃˆŠ°Š²ŃˆŠøŠ¹ о вашем сервере, нахоГит Š“Š¾Ń€Š¾Š³Ńƒ Š²Š½ŃƒŃ‚Ń€ŃŒ: читает `authorization_servers` Šø иГёт Ń‚ŃƒŠ“Š° за токеном. ŠŠø строчки ŠøŠ· него вы не писали. !!! check - ŠžŠ±Ń€Š°Ń‚ŠøŃ‚ŠµŃŃŒ Šŗ `/mcp` без токена (или с таким, Š“Š»Ń которого верификатор Š²ŠµŃ€Š½ŃƒŠ» `None`) — Šø запрос - Š¾ŃŃ‚Š°Š½Š¾Š²ŃŃ‚ на вхоГе: + Вызовите `/mcp` без токена (или с токеном, Š“Š»Ń которого верификатор Š²ŠµŃ€Š½ŃƒŠ» `None`) — Šø запрос + Š¾ŃŃ‚Š°Š½Š¾Š²ŃŃ‚ на пороге: ```text HTTP/1.1 401 Unauthorized @@ -78,26 +82,26 @@ SDK ничего не преГполагает о том, как Š²Ń‹Š³Š»ŃŠ“Šø {"error": "invalid_token", "error_description": "Authentication required"} ``` - ŠŠøŃ‡ŠµŠ³Š¾ не Ń€Š°Š·Š±ŠøŃ€Š°Š»Š¾ŃŃŒ, ни оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ не Š·Š°ŠæŃƒŃŠŗŠ°Š»ŃŃ. А ŃƒŠŗŠ°Š·Š°Ń‚ŠµŠ»ŃŒ `resource_metadata` в `WWW-Authenticate` — - ŃŃ‚Š¾ то, что Гелает Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ автоматическим: 401 -> Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ метаГанных -> сервер авторизации -> токен -> повтор. + ŠŠøŃ‡ŠµŠ³Š¾ не было разобрано, Šø ни оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ не Š²Ń‹ŠæŠ¾Š»Š½ŠøŠ»ŃŃ. А ŃƒŠŗŠ°Š·Š°Ń‚ŠµŠ»ŃŒ `resource_metadata` в `WWW-Authenticate` — + именно то, что Гелает Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ автоматическим: 401 -> Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ метаГанных -> сервер авторизации -> токен -> повтор запроса. !!! warning ŠŠøŃ‡Ń‚Š¾ ŠøŠ· ŃŃ‚Š¾Š³Š¾ не защищает `stdio`. Š£ канала нет заголовка `Authorization`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ Šŗ `token_verifier` там никогГа - не Š¾Š±Ń€Š°Ń‰Š°ŃŽŃ‚ся. Граница безопасности `stdio`-сервера — ŃŃ‚Š¾ процесс, который его Š·Š°ŠæŃƒŃŃ‚ŠøŠ». То же - Š¾Ń‚Š½Š¾ŃŠøŃ‚ŃŃ Šŗ `Client(mcp)` в ŠæŠ°Š¼ŃŃ‚Šø, который ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ся в тестах: он ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Š½Š°ŠæŃ€ŃŠ¼ŃƒŃŽ Šŗ Š¾Š±ŃŠŠµŠŗŃ‚Ńƒ сервера + не Š¾Š±Ń€Š°Ń‰Š°ŃŽŃ‚ся. Граница безопасности `stdio`-сервера — процесс, который его Š·Š°ŠæŃƒŃŃ‚ŠøŠ». То же + Š¾Ń‚Š½Š¾ŃŠøŃ‚ŃŃ Šŗ `Client(mcp)` в ŠæŠ°Š¼ŃŃ‚Šø, который ŠøŃŠæŠ¾Š»ŃŒŠ·ŃƒŠµŃ‚ся в тестах: он ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ ŠæŃ€ŃŠ¼Š¾ Šŗ Š¾Š±ŃŠŠµŠŗŃ‚Ńƒ сервера Šø Š¼ŠøŠ½ŃƒŠµŃ‚ HTTP-ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ вместе с авторизацией. -## Š›ŠøŃ‡Š½Š¾ŃŃ‚ŃŒ Š²Ń‹Š·Ń‹Š²Š°ŃŽŃ‰ŠµŠ³Š¾ {#the-callers-identity} +## Š›ŠøŃ‡Š½Š¾ŃŃ‚ŃŒ Š²Ń‹Š·Ń‹Š²Š°ŃŽŃ‰ŠµŠ¹ стороны {#the-callers-identity} Š’Š½ŃƒŃ‚Ń€Šø Š»ŃŽŠ±Š¾Š³Š¾ обработчика **`get_access_token()`** — ŃŃ‚Š¾ `AccessToken`, который ваш верификатор Š²ŠµŃ€Š½ŃƒŠ» Š“Š»Ń Ń‚ŠµŠŗŃƒŃ‰ŠµŠ³Š¾ запроса: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` -* Это работает в ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°Ń…, Ń€ŠµŃŃƒŃ€ŃŠ°Ń… Šø промптах, Šø ничего ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŃŒ не нужно: middleware авторизации ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ его в контекстной переменной Š“Š»Ń кажГого запроса. -* Š’Š¾Š·Š²Ń€Š°Ń‰Š°ŠµŃ‚ŃŃ **тот самый Š¾Š±ŃŠŠµŠŗŃ‚, который собрал ваш верификатор**: `client_id`, `scopes`, `subject`, `expires_at` Šø Š»ŃŽŠ±Ń‹Šµ Š“Š¾ŠæŠ¾Š»Š½ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Šµ `claims`, которые вы прикрепили. Это Šø ŠµŃŃ‚ŃŒ точка Š“Š»Ń правил на ŃƒŃ€Š¾Š²Š½Šµ Š¾Ń‚Š“ŠµŠ»ŃŒŠ½Ń‹Ń… ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š¾Š²: прочитайте scopes Šø откажите. -* Вне Š°ŃƒŃ‚ентифицированного HTTP-запроса Ń„ŃƒŠ½ŠŗŃ†ŠøŃ возвращает `None`. Š’ ŠæŠ°Š¼ŃŃ‚Šø Šø через `stdio` ŃŃ‚Š¾ всегГа `None`. +* Работает в ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°Ń…, Ń€ŠµŃŃƒŃ€ŃŠ°Ń… Šø промптах, Šø ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŃŒ ничего не нужно: middleware авторизации ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ его в контекстной переменной Š“Š»Ń кажГого запроса. +* Š’Š¾Š·Š²Ń€Š°Ń‰Š°ŠµŃ‚ŃŃ **тот самый Š¾Š±ŃŠŠµŠŗŃ‚, который собрал ваш верификатор**: `client_id`, `scopes`, `subject`, `expires_at` Šø Š»ŃŽŠ±Ń‹Šµ Š“Š¾ŠæŠ¾Š»Š½ŠøŃ‚ŠµŠ»ŃŒŠ½Ń‹Šµ `claims`, которые вы прикрепили. Это Šø ŠµŃŃ‚ŃŒ точка Š“Š»Ń правил на ŃƒŃ€Š¾Š²Š½Šµ ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°: прочитайте области Š“ŠµŠ¹ŃŃ‚Š²ŠøŃ Šø откажите. +* Вне Š°ŃƒŃ‚ентифицированного HTTP-запроса Š²Š¾Š·Š²Ń€Š°Ń‰Š°ŠµŃ‚ся `None`. Š’ ŠæŠ°Š¼ŃŃ‚Šø Šø по `stdio` ŃŃ‚Š¾ всегГа `None`. Вызовите `whoami` с `Authorization: Bearer alice-token` — Šø моГель прочитает: @@ -105,26 +109,26 @@ SDK ничего не преГполагает о том, как Š²Ń‹Š³Š»ŃŠ“Šø alice (scopes: notes:read) ``` -## Половина, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ SDK не Гелает {#the-half-the-sdk-doesnt-do} +## Половина, которой в SDK нет {#the-half-the-sdk-doesnt-do} -SDK Гаёт вам половину сервера Ń€ŠµŃŃƒŃ€ŃŠ¾Š²: ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ, Š¾Š±ŃŠŃŠ²ŠøŃ‚ŃŒ, Š¾Ń‚ŠŗŠ°Š·Š°Ń‚ŃŒ. ŠžŠ½ не Гаёт ŃŃ‚Ń€Š°Š½ŠøŃ†Ńƒ вхоГа, ŃŠŗŃ€Š°Š½ ŃŠ¾Š³Š»Š°ŃŠøŃ или токен. +SDK Гаёт вам половину сервера Ń€ŠµŃŃƒŃ€ŃŠ¾Š²: ŠæŃ€Š¾Š²ŠµŃ€ŠøŃ‚ŃŒ, Š¾Š±ŃŠŃŠ²ŠøŃ‚ŃŒ, Š¾Ń‚ŠŗŠ°Š·Š°Ń‚ŃŒ. ŠžŠ½ не Гаёт ни страницы вхоГа, ни ŃŠŗŃ€Š°Š½Š° ŃŠ¾Š³Š»Š°ŃŠøŃ, ни токена. -Чтобы ŃƒŠ²ŠøŠ“ŠµŃ‚ŃŒ все три стороны в Гействии, Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `examples/servers/simple-auth/` ŠøŠ· Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€ŠøŃ SDK (небольшой сервер авторизации Šø сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š², настроенный ровно так, как на ŃŃ‚Š¾Š¹ странице), а затем Š½Š°ŠæŃ€Š°Š²ŃŒŃ‚е на него `examples/clients/simple-auth-client/` — Šø пройГите весь ŠæŃƒŃ‚ŃŒ от Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŃ Го токена. +Чтобы ŃƒŠ²ŠøŠ“ŠµŃ‚ŃŒ в Гвижении все три стороны, Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `examples/servers/simple-auth/` ŠøŠ· Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€ŠøŃ SDK (небольшой сервер авторизации Šø сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š², настроенный ровно как на ŃŃ‚Š¾Š¹ странице), а затем Š½Š°ŠæŃ€Š°Š²ŃŒŃ‚е на него `examples/clients/simple-auth-client/` — ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚ŃŃ полный цикл Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŃ Šø ŠæŠ¾Š»ŃƒŃ‡ŠµŠ½ŠøŃ токена. !!! info - Š•ŃŃ‚ŃŒ второй Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, `auth_server_provider=`, который встраивает полноценный сервер - авторизации Š²Š½ŃƒŃ‚Ń€ŃŒ MCP-сервера. ŠžŠ½ ŠæŠ¾ŃŠ²ŠøŠ»ŃŃ Ń€Š°Š½ŃŒŃˆŠµ Ń€Š°Š·Š“ŠµŠ»ŠµŠ½ŠøŃ AS/RS, Š²Š¾ŠŗŃ€ŃƒŠ³ которого построена ŃŠæŠµŃ†ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ - авторизации MCP. Š’ новых серверах Šŗ нему ŠæŃ€ŠøŠ±ŠµŠ³Š°Ń‚ŃŒ не ŃŠ»ŠµŠ“ŃƒŠµŃ‚. + Š•ŃŃ‚ŃŒ Šø второй Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, `auth_server_provider=`, который встраивает полноценный сервер + авторизации Š²Š½ŃƒŃ‚Ń€ŃŒ MCP-сервера. ŠžŠ½ ŠæŠ¾ŃŠ²ŠøŠ»ŃŃ Ń€Š°Š½ŃŒŃˆŠµ Ń€Š°Š·Š“ŠµŠ»ŠµŠ½ŠøŃ на AS Šø RS, Š²Š¾ŠŗŃ€ŃƒŠ³ которого построена + ŃŠæŠµŃ†ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ авторизации MCP. Š’ новых серверах Š¾Š±Ń€Š°Ń‰Š°Ń‚ŃŒŃŃ Šŗ нему не ŃŠ»ŠµŠ“ŃƒŠµŃ‚. -Дервер авторизации также может ŠæŃ€ŠøŠ½ŃŃ‚ŃŒ поГписанное ŃƒŃ‚Š²ŠµŃ€Š¶Š“ŠµŠ½ŠøŠµ от корпоративного провайГера иГентификации вместо того, чтобы ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ прохоГил через ŃŠŗŃ€Š°Š½ ŃŠ¾Š³Š»Š°ŃŠøŃ, Šø SDK поГГерживает обе стороны ŃŃ‚Š¾Š³Š¾ обмена. Дам грант Šø клиент, который его ŠæŃ€ŠµŠ“ŃŠŃŠ²Š»ŃŠµŃ‚, описаны на странице **[УтвержГение личности](../client/identity-assertion.md)**. +Дервер авторизации может также ŠæŃ€ŠøŠ½ŃŃ‚ŃŒ поГписанное ŃƒŃ‚Š²ŠµŃ€Š¶Š“ŠµŠ½ŠøŠµ корпоративного провайГера иГентификации вместо того, чтобы ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»ŃŒ прохоГил ŃŠŗŃ€Š°Š½ ŃŠ¾Š³Š»Š°ŃŠøŃ, — Šø SDK поГГерживает обе стороны ŃŃ‚Š¾Š³Š¾ обмена. Дам грант Šø клиент, который его ŠæŃ€ŠµŠ“ŃŠŃŠ²Š»ŃŠµŃ‚, описаны на странице **[УтвержГение иГентичности](../client/identity-assertion.md)**. ## Š˜Ń‚Š¾Š³Šø {#recap} -* Через Streamable HTTP ваш сервер — ŃŃ‚Š¾ **сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š²** OAuth 2.1: он ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ токены, но никогГа ŠøŃ… не выГаёт. -* `TokenVerifier` — Š²ŃŃ ŠæŠ¾Š²ŠµŃ€Ń…Š½Š¾ŃŃ‚ŃŒ интеграции: оГин асинхронный метоГ, токен на вхоГе, `AccessToken | None` на выхоГе. +* По Streamable HTTP ваш сервер — **сервер Ń€ŠµŃŃƒŃ€ŃŠ¾Š²** OAuth 2.1: он ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ токены Šø никогГа ŠøŃ… не выГаёт. +* `TokenVerifier` — Š²ŃŃ ŠæŠ¾Š²ŠµŃ€Ń…Š½Š¾ŃŃ‚ŃŒ интеграции: оГин асинхронный метоГ, на вхоГе токен, на выхоГе `AccessToken | None`. * `token_verifier=` Šø `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` всегГа ŠøŠ“ŃƒŃ‚ в паре. -* SDK ŠæŃƒŠ±Š»ŠøŠŗŃƒŠµŃ‚ Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по Š°Š“Ń€ŠµŃŃƒ `/.well-known/oauth-protected-resource/...` Šø отвечает на Š½ŠµŠ°ŃƒŃ‚ентифицированные запросы коГом 401, заголовок `WWW-Authenticate` которого ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ на них. Это Šø ŠµŃŃ‚ŃŒ Š²ŃŃ ŠøŃŃ‚Š¾Ń€ŠøŃ Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŃ. +* SDK ŠæŃƒŠ±Š»ŠøŠŗŃƒŠµŃ‚ Protected Resource Metadata по [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) по Š°Š“Ń€ŠµŃŃƒ `/.well-known/oauth-protected-resource/...` Šø отвечает на Š½ŠµŠ°ŃƒŃ‚ентифицированные запросы коГом 401, заголовок `WWW-Authenticate` которого ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ на ŃŃ‚Š¾Ń‚ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚. Š’ ŃŃ‚Š¾Š¼ Šø состоит всё Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ. * `get_access_token()` в Š»ŃŽŠ±Š¾Š¼ обработчике — ŃŃ‚Š¾ тот, кто вызывает. -* ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ — забота HTTP. `stdio` Šø клиент в ŠæŠ°Š¼ŃŃ‚Šø её никогГа не Š²ŠøŠ“ŃŃ‚. +* ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†ŠøŃ — Гело HTTP. `stdio` Šø тестовый клиент в ŠæŠ°Š¼ŃŃ‚Šø её никогГа не Š²ŠøŠ“ŃŃ‚. -ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ°Ń половина (Š¾Š±Š½Š°Ń€ŃƒŠ¶ŠµŠ½ŠøŠµ вашего сервера авторизации Šø ŠæŠ¾Š»ŃƒŃ‡ŠµŠ½ŠøŠµ токена за вас) — на странице **[OAuth-клиенты](../client/oauth-clients.md)**. А клиент, который *ŃƒŃ‚Š²ŠµŃ€Š¶Š“Š°ŠµŃ‚* Š»ŠøŃ‡Š½Š¾ŃŃ‚ŃŒ вместо того, чтобы Š·Š°ŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ её у ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń, — на странице **[УтвержГение личности](../client/identity-assertion.md)**. +ŠšŠ»ŠøŠµŠ½Ń‚ŃŠŗŠ°Ń половина (найти ваш сервер авторизации Šø ŠæŠ¾Š»ŃƒŃ‡ŠøŃ‚ŃŒ токен за вас) — на странице **[OAuth-клиенты](../client/oauth-clients.md)**. А клиент, который *ŃƒŃ‚Š²ŠµŃ€Š¶Š“Š°ŠµŃ‚* ŠøŠ“ŠµŠ½Ń‚ŠøŃ‡Š½Š¾ŃŃ‚ŃŒ, вместо того чтобы ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ её у ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń, — на странице **[УтвержГение иГентичности](../client/identity-assertion.md)**. diff --git a/i18n/ru/pages/run/deploy.md b/i18n/ru/pages/run/deploy.md index 1671d923..1ddeb70d 100644 --- a/i18n/ru/pages/run/deploy.md +++ b/i18n/ru/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ {#deploy-scale} @@ -47,6 +47,22 @@ translation: Š”Š²ŠµŠ¶ŠµŃ€Š°Š·Š²Ń‘Ń€Š½ŃƒŃ‚Ń‹Š¹ сервер, который Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚ все ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ, — ŃŃ‚Š¾ список Ń€Š°Š·Ń€ŠµŃˆŃ‘Š½Š½Ń‹Ń… Host, пока не Гоказано обратное. **[Устранение неполаГок](../troubleshooting.md)** тоже Š½Š°Ń‡ŠøŠ½Š°ŠµŃ‚ся Š¾Ń‚ŃŃŽŠ“Š°. +## За прокси, Ń‚ŠµŃ€Š¼ŠøŠ½ŠøŃ€ŃƒŃŽŃ‰ŠøŠ¼ TLS {#behind-a-tls-terminating-proxy} + +Если TLS Š·Š°Š²ŠµŃ€ŃˆŠ°ŠµŃ‚ŃŃ на прокси (ingress, балансировщик Š½Š°Š³Ń€ŃƒŠ·ŠŗŠø, Caddy, nginx), а uvicorn за ним отГаёт обычный HTTP, велите uvicorn Š“Š¾Š²ŠµŃ€ŃŃ‚ŃŒ заголовкам `X-Forwarded-*` от прокси: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Без ŃŃ‚Š¾Š³Š¾ приложение считает, что его Š¾Ń‚Š“Š°ŃŽŃ‚ по `http://`, Šø Š»ŃŽŠ±Š¾Šµ перенаправление, которое оно выГаёт (обычно ŃŃ‚Š¾ `/mcp` → `/mcp/`), ŃƒŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ на `http://…`. Python-клиент Š¾Ń‚ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ ŠæŠµŃ€ŠµŃ…Š¾Š“ŠøŃ‚ŃŒ с HTTPS-ŃŠ½Š“ŠæŠ¾ŠøŠ½Ń‚Š° на обычный HTTP Šø ŠæŃ€ŃŠ¼Š¾ об ŃŃ‚Š¾Š¼ говорит: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Временное Ń€ŠµŃˆŠµŠ½ŠøŠµ на стороне клиента — ŃƒŠŗŠ°Š·Š°Ń‚ŃŒ точный URL, который Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ сервер (`https://mcp.example.com/mcp/`, со ŃŠ»ŃŃˆŠµŠ¼ на конце), чтобы ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½ŠøŃ не было вовсе. ŠŠ°ŃŃ‚Š¾ŃŃ‰ŠµŠµ Ń€ŠµŃˆŠµŠ½ŠøŠµ — флаг Š²Ń‹ŃˆŠµ. `FORWARDED_ALLOW_IPS` — то же самое в виГе переменной Š¾ŠŗŃ€ŃƒŠ¶ŠµŠ½ŠøŃ; `*` Š“Š¾Š²ŠµŃ€ŃŠµŃ‚ кажГому узлу по ŠæŃƒŃ‚Šø, что ŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾, Ń‚Š¾Š»ŃŒŠŗŠ¾ если Го uvicorn не может Š“Š¾Š±Ń€Š°Ń‚ŃŒŃŃ никто, кроме прокси. + ## Рабочие процессы Šø кому нужна ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ° {#workers-and-who-has-to-be-sticky} Как Ń‚Š¾Š»ŃŒŠŗŠ¾ Гоменное ŠøŠ¼Ń отвечает, ŠæŠ¾ŃŃ‚Š°Š²ŃŒŃ‚Šµ за ним больше оГного рабочего процесса. Š’ SDK Š“Š»Ń ŃŃ‚Š¾Š³Š¾ нет никакой Ń€ŃƒŃ‡ŠŗŠø; приложение Starlette Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€ŃƒŠµŃ‚ŃŃ так же, как Š»ŃŽŠ±Š¾Šµ ASGI-приложение: Š¾Š±ŃŠŠµŠŗŃ‚ ŠæŠµŃ€ŠµŠ“Š°Ń‘Ń‚ŃŃ Ń‚Š¾Š¼Ńƒ, кто ŃƒŠ¼ŠµŠµŃ‚ ŠæŠ¾Ń€Š¾Š¶Š“Š°Ń‚ŃŒ процессы: @@ -171,6 +187,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## Š˜Ń‚Š¾Š³Šø {#recap} * По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ приложение отвечает Ń‚Š¾Š»ŃŒŠŗŠ¾ на запросы, аГресованные localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` — ŃŃ‚Š¾ ворота в ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½: пока вы его не переГаГите, кажГый запрос за Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¼ Гоменным именем ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ `421`, а причина ŠµŃŃ‚ŃŒ Ń‚Š¾Š»ŃŒŠŗŠ¾ в логе сервера. +* За прокси, Ń‚ŠµŃ€Š¼ŠøŠ½ŠøŃ€ŃƒŃŽŃ‰ŠøŠ¼ TLS, Š·Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ uvicorn с `--proxy-headers --forwarded-allow-ips=...`, иначе его ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½ŠøŃ ŃƒŠŗŠ°Š·Ń‹Š²Š°ŃŽŃ‚ на `http://`, Šø клиент ŠøŃ… Š¾Ń‚ŠŗŠ»Š¾Š½ŃŠµŃ‚. * ŠŠ° 2026-07-28 нет сессии, Šø Š±Š°Š»Š°Š½ŃŠøŃ€Š¾Š²Ń‰ŠøŠŗŃƒ не Šŗ Ń‡ŠµŠ¼Ńƒ ŠæŃ€ŠøŠ²ŃŠ·Ń‹Š²Š°Ń‚ŃŒŃŃ. `stateless_http=True` — Ń€ŃƒŃ‡ŠŗŠ° Ń‚Š¾Š»ŃŒŠŗŠ¾ Š“Š»Ń старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что современный запрос Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŠøŃ€ŃƒŠµŃ‚ŃŃ Šø ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ ответ Ń€Š°Š½ŃŒŃˆŠµ, чем ŃŃ‚Š¾Ń‚ флаг вообще Ń‡ŠøŃ‚Š°ŠµŃ‚ŃŃ. * ŠšŠ»ŃŽŃ‡ `requestState` по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ — `os.urandom(32)`, Š²Ń‹ŠæŃƒŃŠŗŠ°ŠµŠ¼Ń‹Š¹ в кажГом процессе. ŠœŠ½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń‹Š¹ повтор, попавший Šŗ Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ Ń€Š°Š±Š¾Ń‡ŠµŠ¼Ńƒ ŠæŃ€Š¾Ń†ŠµŃŃŃƒ, паГает с `-32602` *Ā«Invalid or expired requestStateĀ»*. * Решение — `RequestStateSecurity(keys=[...])` **Šø** оГно Šø то же ŠøŠ¼Ń сервера на кажГом ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Šµ. Š˜Š¼Ń — ŃŃ‚Š¾ audience claim токена по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ. ŠžŠ“Š½Šø ŠŗŠ»ŃŽŃ‡Šø, оГно ŠøŠ¼Ń. diff --git a/i18n/ru/pages/run/index.md b/i18n/ru/pages/run/index.md index 879daf72..a4ad8692 100644 --- a/i18n/ru/pages/run/index.md +++ b/i18n/ru/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Š—Š°ŠæŃƒŃŠŗ сервера {#running-your-server} @@ -75,6 +75,11 @@ Inspector Гелает ровно то же, что Šø Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹ хос * `max_request_body_size`: Š¼Š°ŠŗŃŠøŠ¼Š°Š»ŃŒŠ½Ń‹Š¹ принимаемый размер тела запроса в байтах. По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ 4 ŠœŠøŠ‘; более ŠŗŃ€ŃƒŠæŠ½Ń‹Šµ запросы ŠæŠ¾Š»ŃƒŃ‡Š°ŃŽŃ‚ HTTP 413 ещё Го разбора Šø ŃŠ¾Š·Š“Š°Š½ŠøŃ сессии. Увеличивайте его, Ń‚Š¾Š»ŃŒŠŗŠ¾ если легитимные MCP-ŃŠ¾Š¾Š±Ń‰ŠµŠ½ŠøŃ ŠæŃ€ŠµŠ²Ń‹ŃˆŠ°ŃŽŃ‚ ŃŃ‚Š¾Ń‚ размер. +* `session_idle_timeout`: сколько секунГ ŃŠµŃŃŠøŃ старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ может ŠæŃ€Š¾ŃŃ‚Š°ŠøŠ²Š°Ń‚ŃŒ без еГиного Š²Ń‹ŠæŠ¾Š»Š½ŃŃŽŃ‰ŠµŠ³Š¾ŃŃ запроса, прежГе чем + сервер её закроет. По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ 1800. `None` Š¾Ń‚ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ Ń‚Š°Š¹Š¼Š°ŃƒŃ‚. Дм. + [Š’Ń€ŠµŠ¼Ń жизни сессий Šø лимиты](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: сколько сессий старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ оГин процесс ŃƒŠ“ŠµŃ€Š¶ŠøŠ²Š°ŠµŃ‚ оГновременно. По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ 10 000. `None` + снимает ограничение. ŠžŠæŠøŃŠ°Š½ в том же разГеле. * `event_store`, `retry_interval`, `transport_security`: Š²Š¾Š·Š¾Š±Š½Š¾Š²Š»ŃŠµŠ¼Š¾ŃŃ‚ŃŒ Šø защита от DNS-rebinding. ŠžŠ½Šø Š¼Š¾Š³ŃƒŃ‚ ŠæŠ¾Š“Š¾Š¶Š“Š°Ń‚ŃŒ, пока вы не Ń€Š°Š·Š²ŠµŃ€Š½Ń‘Ń‚ŠµŃŃŒ гГе-то кроме localhost; `transport_security` разобран на странице **[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](deploy.md)**. !!! warning diff --git a/i18n/ru/pages/run/legacy-clients.md b/i18n/ru/pages/run/legacy-clients.md index 254ec014..bd6708c2 100644 --- a/i18n/ru/pages/run/legacy-clients.md +++ b/i18n/ru/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ клиентов старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŠøŃ€ŃƒŠµŃ‚ кажГый запрос по его заго ## ŠžŠ“ŠøŠ½ обработчик, оба ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ {#one-handler-both-eras} -Вот ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ нужно кое-что ŃŠæŃ€Š¾ŃŠøŃ‚ŃŒ у ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń, Šø клиенты обоих поколений, которые его Š²Ń‹Š·Ń‹Š²Š°ŃŽŃ‚: +Вот ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ нужно кое-что ŃŠæŃ€Š¾ŃŠøŃ‚ŃŒ у ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` Š˜Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ńƒ `reserve` нужно оГно, чего моГель не сообщила: сколько ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Š¾Š². `Annotated[..., Resolve(ask_quantity)]` — так ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ ŃŃ‚Š¾ Š¾Š±ŃŠŃŠ²Š»ŃŠµŃ‚ (поГробнее — на странице **[Зависимости](../handlers/dependencies.md)**). ŠŠøŃ‡Ń‚Š¾ в `reserve` не называет Š²ŠµŃ€ŃŠøŃŽ, не ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ Š²Š¾Š·Š¼Š¾Š¶Š½Š¾ŃŃ‚ŃŒ Šø не Š²ŠµŃ‚Š²ŠøŃ‚ŃŃ. -ŠžŠ±Š° клиента открыты **оГновременно**, на оГном Šø том же Š¾Š±ŃŠŠµŠŗŃ‚е `mcp`. `mode="legacy"` Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ Ń€ŃƒŠŗŠ¾ŠæŠ¾Š¶Š°Ń‚ŠøŠµ `initialize` — ровно такое ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ открывает клиент Го 2026 гоГа. Второй клиент берёт значение по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ Šø Š¾ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ на `2026-07-28`. +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ его по HTTP — Šø вот клиенты обоих поколений, которые его Š²Ń‹Š·Ń‹Š²Š°ŃŽŃ‚: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ŠžŠ±Š° клиента открыты **оГновременно**, Šŗ оГному Šø Ń‚Š¾Š¼Ńƒ же Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠµŠ¼Ńƒ ŃŠµŃ€Š²ŠµŃ€Ńƒ. `mode="legacy"` Š²Ń‹ŠæŠ¾Š»Š½ŃŠµŃ‚ Ń€ŃƒŠŗŠ¾ŠæŠ¾Š¶Š°Ń‚ŠøŠµ `initialize` — ровно такое ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ открывает клиент Го 2026 гоГа. Второй клиент берёт значение по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ Šø Š¾ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ на `2026-07-28`. Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ `python client.py` во втором терминале: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,40 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŠøŃ€ŃƒŠµŃ‚ кажГый запрос по его заго отправка ŠæŃ€Š¾ŠæŃƒŃ‰ŠµŠ½Š½Ń‹Ń… SSE-событий ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ, который ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šŗ *той же* сессии), а не хранилище сессий. Š”ŠµŃŃŠøŃŽ Š“Š¾ŃŃ‚ŃƒŠæŠ½Š¾Š¹ ŠøŠ· Š“Ń€ŃƒŠ³Š¾Š³Š¾ процесса он не Гелает никогГа. +## Š’Ń€ŠµŠ¼Ń жизни Šø лимиты сессий {#session-lifetime-and-limits} + +Š”ŠµŃŃŠøŃ старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ не живёт вечно, Šø оГин процесс не Гержит ŠøŃ… неограниченное количество. +За ŃŃ‚Š¾ Š¾Ń‚Š²ŠµŃ‡Š°ŃŽŃ‚ Гве настройки. ŠžŠ±Šµ — именованные Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń‹ `run()`, `streamable_http_app()` +Šø `Server.streamable_http_app()`. Š£ современных ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠ¹ (`2026-07-28`) Šø при `stateless_http=True` +сессий нет, так что ни оГна ŠøŠ· настроек Šŗ ним не Š¾Ń‚Š½Š¾ŃŠøŃ‚ŃŃ. + +| ŠŠ°ŃŃ‚Ń€Š¾Š¹ŠŗŠ° | По ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ | Что Гелает | Что виГит клиент | Как Š¾Ń‚ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŒ | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 мин) | Закрывает ŃŠµŃŃŠøŃŽ, в которой ŃŃ‚Š¾Š»ŃŒŠŗŠ¾ времени ничего не было в работе. | `404 Session not found`. ŠŸŃ€ŠøŠ“Ń‘Ń‚ŃŃ заново Š²Ń‹ŠæŠ¾Š»Š½ŠøŃ‚ŃŒ `initialize`. | `None` | +| `max_sessions` | `10_000` | ŠžŃ‚ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ Š¾Ń‚ŠŗŃ€Ń‹Š²Š°Ń‚ŃŒ сессии сверх ŃŃ‚Š¾Š³Š¾ числа. Š”ŃƒŃ‰ŠµŃŃ‚Š²ŃƒŃŽŃ‰ŠøŠµ сессии не трогает Šø ничего не Š²Ń‹Ń‚ŠµŃŠ½ŃŠµŃ‚. | `503 Too many open sessions` с коГом JSON-RPC `-32603`. | `None` | + +Что ŃŃ‡ŠøŃ‚Š°ŠµŃ‚ŃŃ «в работе»: + +* ŠžŃ‚ŠŗŃ€Ń‹Ń‚Ń‹Š¹ `GET`-поток. ŠšŠ»ŠøŠµŠ½Ń‚Ń‹ SDK Гержат такой поток открытым, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ ŃŠµŃŃŠøŃ ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Š¾Š³Š¾ + клиента никогГа не истекает. +* Запрос, на который ещё Š³Š¾Ń‚Š¾Š²ŠøŃ‚ŃŃ ответ. Вызов ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°, Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠøŠ¹ Гольше тайм-Š°ŃƒŃ‚Š°, + не ŠæŃ€ŠµŃ€Ń‹Š²Š°ŠµŃ‚ся, а обратный отсчёт Š½Š°Ń‡ŠøŠ½Š°ŠµŃ‚ŃŃ Ń‚Š¾Š»ŃŒŠŗŠ¾ после его Š·Š°Š²ŠµŃ€ŃˆŠµŠ½ŠøŃ. +* Š‘Š¾Š»ŃŒŃˆŠµ ничего. МежГу запросами часы ŠøŠ“ŃƒŃ‚. Š›ŃŽŠ±Š¾Š¹ запрос в сессии Š·Š°ŠæŃƒŃŠŗŠ°ŠµŃ‚ ŠøŃ… заново, + Š²ŠŗŠ»ŃŽŃ‡Š°Ń `ping`. Š˜ŃŃ‚Ń‘ŠŗŃˆŃƒŃŽ ŃŠµŃŃŠøŃŽ уже ничто не оживит. + +ŠšŠ»ŠøŠµŠ½Ń‚, Š·Š°Š²ŠµŃ€ŃˆŠ°ŃŽŃ‰ŠøŠ¹ ŃŠµŃŃŠøŃŽ запросом `DELETE`, освобожГает её ŃŃ€Š°Š·Ńƒ. То же происхоГит +с клиентом, чей Š¾Ń‚ŠŗŃ€Ń‹Š²Š°ŃŽŃ‰ŠøŠ¹ запрос был отклонён. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +ŠžŠ±Š° ŃŠ¾Š±Ń‹Ń‚ŠøŃ ŠæŠ¾ŠæŠ°Š“Š°ŃŽŃ‚ в лог сервера. Š˜ŃŃ‚ŠµŃ‡ŠµŠ½ŠøŠµ — `Session idle timeout` на ŃƒŃ€Š¾Š²Š½Šµ `INFO`. +ŠžŃ‚ŠŗŠ°Š· в открытии — `Refusing to open a new session: sessions are already open` на ŃƒŃ€Š¾Š²Š½Šµ `WARNING`. + +Лимиты Š“ŠµŠ¹ŃŃ‚Š²ŃƒŃŽŃ‚ на процесс. ŠŸŃ€Šø четырёх рабочих процессах потолок — четыре раза по `max_sessions`, +Šø кажГый рабочий процесс сам отсчитывает Š²Ń€ŠµŠ¼Ń жизни своих сессий. + ## ЕГинственный ŠæŠµŃ€ŠµŠŗŠ»ŃŽŃ‡Š°Ń‚ŠµŠ»ŃŒ: `stateless_http` {#the-one-knob-stateless_http} Если ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ° — цена, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ вы ŠæŠ»Š°Ń‚ŠøŃ‚ŃŒ не готовы, ŠøŠ·Š¼ŠµŠ½ŠøŃ‚ŃŒ можно ровно оГно. @@ -87,7 +131,7 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŠøŃ€ŃƒŠµŃ‚ кажГый запрос по его заго !!! check ДГелайте завеГомо Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾. `reserve` — тот самый ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, который Ń‚Š¾Š»ŃŒŠŗŠ¾ что обслужил - оба клиента. Разверните его с `stateless_http=True`, ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚Šµ те же Гва клиента по HTTP Šø + оба клиента. Разверните его с `stateless_http=True`, ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚Šµ те же Гва клиента Šø вызовите его ŠøŠ· кажГого. Довременный клиент по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ ŠæŠ¾Š»ŃƒŃ‡Š°ŠµŃ‚ `Reserved 2 of 'Dune'.` Š”Š¾Š²Ń€ŠµŠ¼ŠµŠ½Š½Š°Ń ветка не изменилась. diff --git a/i18n/ru/pages/troubleshooting.md b/i18n/ru/pages/troubleshooting.md index 189c9d0a..b364dc4a 100644 --- a/i18n/ru/pages/troubleshooting.md +++ b/i18n/ru/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Устранение неполаГок {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Эти записи Š¾Š±Ń€Š°Ń‰Š°ŃŽŃ‚ся Šŗ нему по Š°Š“Ń€ŠµŃŃƒ `http://localhost:8000/mcp`, так что Š¾ŃŃ‚Š°Š²ŃŒŃ‚Šµ его Š·Š°ŠæŃƒŃ‰ŠµŠ½Š½Ń‹Š¼ по HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + ŠžŃˆŠøŠ±ŠŗŠø, которые Ń†ŠøŃ‚ŠøŃ€ŃƒŠµŃ‚ ŃŃ‚Š° страница, Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠµ: собственный набор тестов SDK воспроизвоГит ŠŗŠ°Š¶Š“ŃƒŃŽ ŠøŠ· них. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -251,7 +257,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Дервер не ŃƒŠ·Š½Š°Ń‘Ń‚ `Mcp-Session-Id`, который отправил клиент, — почти всегГа ŠæŠ¾Ń‚Š¾Š¼Ńƒ, что сервер **ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ»ŃŃ** (или вас направили на Š“Ń€ŃƒŠ³Š¾Š¹ ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€). Дессии Š¶ŠøŠ²ŃƒŃ‚ в ŠæŠ°Š¼ŃŃ‚Šø оГного ŃŃ‚Š¾Š³Š¾ процесса. +Дервер не ŃƒŠ·Š½Š°Ń‘Ń‚ `Mcp-Session-Id`, который отправил клиент. Либо сервер **ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ»ŃŃ** (или вас направили на Š“Ń€ŃƒŠ³Š¾Š¹ ŃŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€), либо ŃŠµŃŃŠøŃ **истекла**, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что в течение `session_idle_timeout` — по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ 30 Š¼ŠøŠ½ŃƒŃ‚ — не Š²Ń‹ŠæŠ¾Š»Š½ŃŠ»Š¾ŃŃŒ ни оГного запроса. Дм. разГел [Š’Ń€ŠµŠ¼Ń жизни сессии Šø Š¾Š³Ń€Š°Š½ŠøŃ‡ŠµŠ½ŠøŃ](run/legacy-clients.md#session-lifetime-and-limits). Дессии Š¶ŠøŠ²ŃƒŃ‚ в ŠæŠ°Š¼ŃŃ‚Šø оГного ŃŃ‚Š¾Š³Š¾ процесса. Š˜ŃŠŗŠ°Ń‚ŃŒ ошибку в сервере незачем. HTTP-ответ — `404`, тело которого — *Š½Š°ŃŃ‚Š¾ŃŃ‰ŠøŠ¹* JSON-RPC, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ, в отличие от `421` Š²Ń‹ŃˆŠµ, `Client` на Python показывает его Гословно: @@ -261,9 +267,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Š˜ŃŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ — ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŃŒŃŃ: выйти ŠøŠ· блока `async with Client(...)` Šø войти в новый, который ŃŠ¾Š³Š»Š°ŃŃƒŠµŃ‚ ŃŠ²ŠµŠ¶ŃƒŃŽ ŃŠµŃŃŠøŃŽ. Š”Š»Ń Š“Š¾Š»Š³Š¾Š¶ŠøŠ²ŃƒŃ‰ŠµŠ³Š¾ клиента ŃŃ‚Š¾ означает ŠæŠµŃ€ŠµŃ…Š²Š°Ń‚Ń‹Š²Š°Ń‚ŃŒ `MCPError` Š²Š¾ŠŗŃ€ŃƒŠ³ вызовов Šø ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°Ń‚ŃŒŃŃ по ŃŃ‚Š¾Š¼Ńƒ ŃŠ¾Š¾Š±Ń‰ŠµŠ½ŠøŃŽ, а не ŠæŠ¾Š²Ń‚Š¾Ń€ŃŃ‚ŃŒ попытки Š²Š½ŃƒŃ‚ри мёртвой сессии. -Если ŃŃ‚Š¾ происхоГит *без* ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŠŗŠ°, значит, у вас больше оГного воркера без Š·Š°ŠŗŃ€ŠµŠæŠ»ŠµŠ½ŠøŃ сессий за ними: кажГый воркер Гержит ŃŠ¾Š±ŃŃ‚Š²ŠµŠ½Š½ŃƒŃŽ Ń‚Š°Š±Š»ŠøŃ†Ńƒ сессий, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ запрос, направленный не на тот воркер, Š¾ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ зГесь. Эта ŠøŃŃ‚Š¾Ń€ŠøŃ Šø Гва её Ń€ŠµŃˆŠµŠ½ŠøŃ (Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·Š°Ń†ŠøŃ с ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ¾Š¹ сессий или `stateless_http=True`) — на страницах **[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](run/deploy.md)** Šø **[ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ клиентов старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ](run/legacy-clients.md)**. +Если ŃŃ‚Š¾ происхоГит *без* ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŠŗŠ° Šø клиент не молчал так Голго, значит, у вас больше оГного воркера без Š·Š°ŠŗŃ€ŠµŠæŠ»ŠµŠ½ŠøŃ сессий за ними: кажГый воркер Гержит ŃŠ¾Š±ŃŃ‚Š²ŠµŠ½Š½ŃƒŃŽ Ń‚Š°Š±Š»ŠøŃ†Ńƒ сессий, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ запрос, направленный не на тот воркер, Š¾ŠŗŠ°Š·Ń‹Š²Š°ŠµŃ‚ŃŃ зГесь. Эта ŠøŃŃ‚Š¾Ń€ŠøŃ Šø Гва её Ń€ŠµŃˆŠµŠ½ŠøŃ (Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·Š°Ń†ŠøŃ с ŠæŃ€ŠøŠ²ŃŠ·ŠŗŠ¾Š¹ сессий или `stateless_http=True`) — на страницах **[Развёртывание Šø Š¼Š°ŃŃˆŃ‚Š°Š±ŠøŃ€Š¾Š²Š°Š½ŠøŠµ](run/deploy.md)** Šø **[ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ клиентов старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ](run/legacy-clients.md)**. -Š”Š»Ń оператора сервера ŃŠ¾Š¾Ń‚Š²ŠµŃ‚ŃŃ‚Š²ŃƒŃŽŃ‰Š°Ń строка лога — `Rejected request with unknown or expired session ID: `. ŠžŠ½Š° ŠæŠøŃˆŠµŃ‚ŃŃ на ŃƒŃ€Š¾Š²Š½Šµ `INFO`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ при обычном пороге `WARNING` её не виГно. Š’ŠøŠ“ŠµŃ‚ŃŒ её пачками ŃŃ€Š°Š·Ńƒ после Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Š½ŠøŃ — Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾: все ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Šµ клиенты ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŃŽŃ‚ŃŃ. +Š”Š»Ń оператора сервера ŃŠ¾Š¾Ń‚Š²ŠµŃ‚ŃŃ‚Š²ŃƒŃŽŃ‰Š°Ń строка лога — `Rejected request with unknown or expired session ID: `. ŠžŠ½Š° ŠæŠøŃˆŠµŃ‚ŃŃ на ŃƒŃ€Š¾Š²Š½Šµ `INFO`, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ при обычном пороге `WARNING` её не виГно. Š’ŠøŠ“ŠµŃ‚ŃŒ её пачками ŃŃ€Š°Š·Ńƒ после Ń€Š°Š·Š²Ń‘Ń€Ń‚Ń‹Š²Š°Š½ŠøŃ — Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾: все ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Šµ клиенты ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŃŽŃ‚ŃŃ. Если же ŃŠµŃŃŠøŃ истекла, ŃŃ‚Š¾Š¹ строке ŠæŃ€ŠµŠ“ŃˆŠµŃŃ‚Š²ŃƒŠµŃ‚ `Session idle timeout`, тоже на ŃƒŃ€Š¾Š²Š½Šµ `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -275,7 +281,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Дервер хочет что-то ŃŠæŃ€Š¾ŃŠøŃ‚ŃŒ у ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń, а ŃŃ‚Š¾Ń‚ клиент никогГа не говорил, что его можно ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ. -Резолвер ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŠø (elicitation) отказывает заранее, если ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Š¹ клиент не Š¾Š±ŃŠŃŠ²ŠøŠ» ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŃŽ через формы, Šø `e.error.data` называет ровно то, чего не хватает: +Этот Bistro переГ бронированием заГаёт вопрос через резолвер: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Š—Š°ŠæŃƒŃŃ‚ŠøŃ‚Šµ его вместо сервера Weather Šø вызовите `book_table` ŠøŠ· клиента, который не переГал `elicitation_callback`. Резолвер отказывает заранее, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Ń‘Š½Š½Ń‹Š¹ клиент так Šø не Š¾Š±ŃŠŃŠ²ŠøŠ» ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŃŽ (elicitation) через формы, а `e.error.data` называет ровно то, чего не хватает: ```json { @@ -289,7 +301,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -314,14 +326,14 @@ async def main() -> None: ŠžŠ±Ń€Š°Š±Š¾Ń‚Ń‡ŠøŠŗ ŠæŠ¾ŠæŃ‹Ń‚Š°Š»ŃŃ Š¾Š±Ń€Š°Ń‚ŠøŃ‚ŃŒŃŃ Šŗ ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ посреГи запроса на ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠø, гГе у вызова нет канала, способного Гонести запрос от сервера. Š’ такое положение вызов ŃŃ‚Š°Š²ŃŃ‚ три ŠŗŠ¾Š½Ń„ŠøŠ³ŃƒŃ€Š°Ń†ŠøŠø сервера. -**ŠŸŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ `2026-07-28`: Š»ŃŽŠ±Š¾Š¹ транспорт, всегГа.** Š’ современном протоколе вообще нет запросов, ŠøŠ½ŠøŃ†ŠøŠøŃ€ŃƒŠµŠ¼Ń‹Ń… сервером, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ сервер отказывает ещё Го того, как что-либо отправлено. `ctx.elicit()` Š²Š½ŃƒŃ‚Ń€Šø ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° — классический способ с ŃŃ‚ŠøŠ¼ ŃŃ‚Š¾Š»ŠŗŠ½ŃƒŃ‚ŃŒŃŃ (в самом первом тесте в ŠæŠ°Š¼ŃŃ‚Šø, поскольку `Client(server)` согласовывает `2026-07-28`, не ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń), Šø переГача `elicitation_callback=` ничего не Š¼ŠµŠ½ŃŠµŃ‚: никакой запрос Го клиента не ГохоГит, так что Š¾Ń‚Š²ŠµŃ‡Š°Ń‚ŃŒ ему не на что: +**ŠŸŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ `2026-07-28`: Š»ŃŽŠ±Š¾Š¹ транспорт, всегГа.** Š’ современном протоколе вообще нет запросов, ŠøŠ½ŠøŃ†ŠøŠøŃ€ŃƒŠµŠ¼Ń‹Ń… сервером, ŠæŠ¾ŃŃ‚Š¾Š¼Ńƒ сервер отказывает ещё Го того, как что-либо отправлено. `ctx.elicit()` Š²Š½ŃƒŃ‚Ń€Šø ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° — классический способ с ŃŃ‚ŠøŠ¼ ŃŃ‚Š¾Š»ŠŗŠ½ŃƒŃ‚ŃŒŃŃ, обычно в самом первом **[тесте](get-started/testing.md)** ŃŃ‚Š¾Š³Š¾ ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° в ŠæŠ°Š¼ŃŃ‚Šø, веГь `Client(mcp)` согласовывает `2026-07-28`, Гаже если его об ŃŃ‚Š¾Š¼ не просили. ŠŸŠµŃ€ŠµŠ“Š°Ń‡Š° `elicitation_callback=` ничего не Š¼ŠµŠ½ŃŠµŃ‚: никакой запрос Го клиента не ГохоГит, так что Š¾Ń‚Š²ŠµŃ‡Š°Ń‚ŃŒ ему не на что: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -363,7 +375,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -416,7 +428,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` в логе сервера — еГинственный признак того, что Гва оГноимённых ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° ŃŃ…Š»Š¾ŠæŠ½ŃƒŠ»ŠøŃŃŒ в оГин. * ŠžŠ“ŠøŠ½ 421, три Š½Š°ŠæŠøŃŠ°Š½ŠøŃ: `Server returned an error response` (`Client` на Python), `421 Misdirected Request` / `Invalid Host header` (всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ), `Invalid Host header: ` (лог сервера). Š˜ŃŠæŃ€Š°Š²Š»ŠµŠ½ŠøŠµ: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> смонтированное приложение, жизненный цикл хоста которого так Šø не Š²Š¾ŃˆŃ‘Š» в `mcp.session_manager.run()`. -* `Session not found` -> сервер ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ»ŃŃ; ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŠµŃŃŒ. +* `Session not found` -> сервер ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ»ŃŃ или ŃŠµŃŃŠøŃ истекла (`session_idle_timeout`); ŠæŠµŃ€ŠµŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠøŃ‚ŠµŃŃŒ. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` нужен канал от сервера Šŗ ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ: у ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ `2026-07-28` его не бывает никогГа, `stateless_http=True` отнимает его у ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠ¹ старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ, а `json_response=True` отнимает канал, ŠæŃ€ŠøŠ²ŃŠ·Š°Š½Š½Ń‹Š¹ Šŗ Š·Š°ŠæŃ€Š¾ŃŃƒ. Š˜ŃŠæŠ¾Š»ŃŒŠ·ŃƒŠ¹Ń‚Šµ резолвер (ŠŗŠ»ŠøŠµŠ½Ń‚Ńƒ старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ Šŗ Ń‚Š¾Š¼Ńƒ же нужен сервер, который ŃŠ¾Ń…Ń€Š°Š½ŃŠµŃ‚ канал). ДосеГнее `Method not found` — ŃŃ‚Š¾ запрос метоГа, которого нет в ревизии протокола Š“Ń€ŃƒŠ³Š¾Š¹ стороны. * `Client did not declare the form elicitation capability ...` Šø `Elicitation not supported` -> у клиента не хватает `elicitation_callback=`. * `Invalid or expired requestState` никогГа не говорит по сети, ŠæŠ¾Ń‡ŠµŠ¼Ńƒ. Лог сервера говорит; `unknown key` означает, что `RequestStateSecurity(keys=[...])` нужно ŃŠ“ŠµŠ»Š°Ń‚ŃŒ общим Š“Š»Ń всех воркеров. diff --git a/i18n/ru/pages/whats-new.md b/i18n/ru/pages/whats-new.md index 2e9f324a..3382e2d0 100644 --- a/i18n/ru/pages/whats-new.md +++ b/i18n/ru/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Что нового в v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 выГавала три вложенных ŃŠ»Š¾Ń: контекстный менеГжер транспорта, Š¾Ń‚Š“Š°ŃŽŃ‰ŠøŠ¹ сырые потоки, Š¾Š±Ń‘Ń€Š½ŃƒŃ‚Ń‹Š¹ Š²Š¾ŠŗŃ€ŃƒŠ³ них `ClientSession` Šø вызываемый Š²Ń€ŃƒŃ‡Š½ŃƒŃŽ `await session.initialize()`. Š’ v2 Š¾Š±ŃŠŠµŠŗŃ‚ оГин: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` принимает Š¾Š±ŃŠŠµŠŗŃ‚ сервера (в ŠæŠ°Š¼ŃŃ‚Šø, без транспорта — ŃŃ‚Š¾ сценарий Š“Š»Ń тестов), URL (Streamable HTTP), `StdioServerParameters` (поГпроцесс stdio) или Š»ŃŽŠ±Š¾Š¹ Š“Ń€ŃƒŠ³Š¾Š¹ контекстный менеГжер транспорта, например `sse_client(...)`. ВхоГ в `async with` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šø ŃŠ¾Š³Š»Š°ŃŃƒŠµŃ‚ Š²ŠµŃ€ŃŠøŃŽ протокола, на каком бы поколении ни говорил сервер; после ŃŃ‚Š¾Š³Š¾ `client.server_capabilities` Šø `client.protocol_version` просто Š“Š¾ŃŃ‚ŃƒŠæŠ½Ń‹, как Šø `client.server_info`, когГа сервер ŃŠµŠ±Ń ŠøŠ“ŠµŠ½Ń‚ŠøŃ„ŠøŃ†ŠøŃ€ŃƒŠµŃ‚ (Ń‚ŠµŠæŠµŃ€ŃŒ ŃŃ‚Š¾ `Implementation | None`, поскольку в поколении 2026 ŠøŠ“ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ Š½ŠµŠ¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š°). ŠšŠ¾Š»Š±ŃŠŗŠø ŃŃŠ¼ŠæŠ»ŠøŃ€Š¾Š²Š°Š½ŠøŃ Šø ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŠø, зарегистрированные в v1, по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‚ (ŠøŃ… тела затрагивает то же переименование Š°Ń‚Ń€ŠøŠ±ŃƒŃ‚Š¾Š² в snake_case, что Šø всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ на ŃŃ‚Š¾Š¹ странице), Ń‚ŠµŠæŠµŃ€ŃŒ они ещё Šø Š¾Ń‚Š²ŠµŃ‡Š°ŃŽŃ‚ на запросы Š²Š½ŃƒŃ‚Ń€Šø Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š¾Š² в стиле 2026 (см. ниже) Šø Š²Ń‹ŠæŠ¾Š»Š½ŃŃŽŃ‚ŃŃ ŠæŠ°Ń€Š°Š»Š»ŠµŠ»ŃŒŠ½Š¾, а не по оГному. `ClientSession` по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ лежит в основе Š“Š»Ń тех, кому нужна Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š°Ń ŠæŠ¾Š²ŠµŃ€Ń…Š½Š¾ŃŃ‚ŃŒ, Šø `client.session` её отГаёт; она тоже изменилась (работает на новом Гвижке-Гиспетчере, Šø некоторые её собственные ŃŠøŠ³Š½Š°Ń‚ŃƒŃ€Ń‹ ŠæŠ¾Š¼ŠµŠ½ŃŠ»ŠøŃŃŒ), так что прежГе чем ŃŠæŃƒŃŠŗŠ°Ń‚ŃŒŃŃ на ŃŃ‚Š¾Ń‚ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ, прочитайте **[Š ŃƒŠŗŠ¾Š²Š¾Š“ŃŃ‚Š²Š¾ по миграции](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**. +`Client` принимает URL (Streamable HTTP), `StdioServerParameters` (поГпроцесс stdio), Š»ŃŽŠ±Š¾Š¹ Š“Ń€ŃƒŠ³Š¾Š¹ контекстный менеГжер транспорта, например `sse_client(...)`, или — в тестах — сам Š¾Š±ŃŠŠµŠŗŃ‚ сервера (в ŠæŠ°Š¼ŃŃ‚Šø, без транспорта). ВхоГ в `async with` ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡Š°ŠµŃ‚ŃŃ Šø ŃŠ¾Š³Š»Š°ŃŃƒŠµŃ‚ Š²ŠµŃ€ŃŠøŃŽ протокола, на каком бы поколении ни говорил сервер; после ŃŃ‚Š¾Š³Š¾ `client.server_capabilities` Šø `client.protocol_version` просто Š“Š¾ŃŃ‚ŃƒŠæŠ½Ń‹, как Šø `client.server_info`, когГа сервер ŃŠµŠ±Ń ŠøŠ“ŠµŠ½Ń‚ŠøŃ„ŠøŃ†ŠøŃ€ŃƒŠµŃ‚ (Ń‚ŠµŠæŠµŃ€ŃŒ ŃŃ‚Š¾ `Implementation | None`, поскольку в поколении 2026 ŠøŠ“ŠµŠ½Ń‚ŠøŃ„ŠøŠŗŠ°Ń†ŠøŃ Š½ŠµŠ¾Š±ŃŠ·Š°Ń‚ŠµŠ»ŃŒŠ½Š°). ŠšŠ¾Š»Š±ŃŠŗŠø ŃŃŠ¼ŠæŠ»ŠøŃ€Š¾Š²Š°Š½ŠøŃ Šø ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŠø, зарегистрированные в v1, по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‚ (ŠøŃ… тела затрагивает то же переименование Š°Ń‚Ń€ŠøŠ±ŃƒŃ‚Š¾Š² в snake_case, что Šø всё Š¾ŃŃ‚Š°Š»ŃŒŠ½Š¾Šµ на ŃŃ‚Š¾Š¹ странице), Ń‚ŠµŠæŠµŃ€ŃŒ они ещё Šø Š¾Ń‚Š²ŠµŃ‡Š°ŃŽŃ‚ на запросы Š²Š½ŃƒŃ‚Ń€Šø Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š¾Š² в стиле 2026 (см. ниже) Šø Š²Ń‹ŠæŠ¾Š»Š½ŃŃŽŃ‚ŃŃ ŠæŠ°Ń€Š°Š»Š»ŠµŠ»ŃŒŠ½Š¾, а не по оГному. `ClientSession` по-ŠæŃ€ŠµŠ¶Š½ŠµŠ¼Ńƒ лежит в основе Š“Š»Ń тех, кому нужна Š½ŠøŠ·ŠŗŠ¾ŃƒŃ€Š¾Š²Š½ŠµŠ²Š°Ń ŠæŠ¾Š²ŠµŃ€Ń…Š½Š¾ŃŃ‚ŃŒ, Šø `client.session` её отГаёт; она тоже изменилась (работает на новом Гвижке-Гиспетчере, Šø некоторые её собственные ŃŠøŠ³Š½Š°Ń‚ŃƒŃ€Ń‹ ŠæŠ¾Š¼ŠµŠ½ŃŠ»ŠøŃŃŒ), так что прежГе чем ŃŠæŃƒŃŠŗŠ°Ń‚ŃŒŃŃ на ŃŃ‚Š¾Ń‚ ŃƒŃ€Š¾Š²ŠµŠ½ŃŒ, прочитайте **[Š ŃƒŠŗŠ¾Š²Š¾Š“ŃŃ‚Š²Š¾ по миграции](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**. Дтраница **[ŠžŠ±ŃŠŠµŠŗŃ‚ Client](client/index.md)** знакомит с ним, **[Транспорты клиента](client/transports.md)** описывает четыре формы ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŃ, **[ŠšŠ¾Š»Š±ŃŠŗŠø клиента](client/callbacks.md)** — сами ŠŗŠ¾Š»Š±ŃŠŗŠø, а **[Тестирование](get-started/testing.md)** показывает шаблон работы в ŠæŠ°Š¼ŃŃ‚Šø, который Š·Š°Š¼ŠµŠ½ŃŠµŃ‚ Š²ŃŠæŠ¾Š¼Š¾Š³Š°Ń‚ŠµŠ»ŃŒŠ½ŃƒŃŽ Ń„ŃƒŠ½ŠŗŃ†ŠøŃŽ `create_connected_server_and_client_session()` ŠøŠ· v1. @@ -171,11 +171,15 @@ v2 Ń€ŠµŠ°Š»ŠøŠ·ŃƒŠµŃ‚ Ń€ŠµŠ²ŠøŠ·ŠøŃŽ 2026-07-28 Šø Š¾Š±ŃŠ»ŃƒŠ¶ŠøŠ²Š°ŠµŃ‚ **об Замена разворачивает вызов. Š˜Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŠŗŠ¾Ń‚Š¾Ń€Š¾Š¼Ńƒ что-то нужно от ŠæŠ¾Š»ŃŒŠ·Š¾Š²Š°Ń‚ŠµŠ»Ń, *возвращает* вопрос (`InputRequiredResult`), клиент отвечает на него теми же ŠŗŠ¾Š»Š±ŃŠŗŠ°Š¼Šø, что были всегГа, Šø вызов ŠæŠ¾Š²Ń‚Š¾Ń€ŃŠµŃ‚ŃŃ с приложенными ответами. `Client` веГёт ŃŃ‚Š¾Ń‚ цикл за вас. ŠŠ° сервере вы реГко собираете Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ сами, ŠæŠ¾Ń‚Š¾Š¼Ńƒ что ŃŃ‚Š¾ Гелает **[Š·Š°Š²ŠøŃŠøŠ¼Š¾ŃŃ‚ŃŒ](handlers/dependencies.md)**: Š°Š½Š½Š¾Ń‚ŠøŃ€ŃƒŠ¹Ń‚Šµ параметр `Resolve(ask_quantity)`, гГе `ask_quantity` — Š¾Š±Ń‹Ń‡Š½Š°Ń Ń„ŃƒŠ½ŠŗŃ†ŠøŃ, ŠŗŠ¾Ń‚Š¾Ń€ŃƒŃŽ вы ŠæŠøŃˆŠµŃ‚Šµ, Šø SDK спросит тем механизмом, который поГГерживает ŠæŠ¾Š“ŠŗŠ»ŃŽŃ‡ŠµŠ½ŠøŠµ: живым запросом ŃŠ»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŠø на сессии старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ или Š¼Š½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“овым запросом на 2026. ŠžŠ“Š½Š¾ тело ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°, оба ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Š’ ŃŃ‚Š¾Š¼ файле Š²ŃŃ ŠøŠ“ŠµŃ собрана в оГном месте: оГин сервер, оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ на `Resolve`, а также клиент старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ Šø современный клиент, оба ŠæŠ¾Š»ŃƒŃ‡Š°ŃŽŃ‰ŠøŠµ свой ответ, — всё в ŠæŠ°Š¼ŃŃ‚Šø. **[ŠœŠ½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń‹Šµ запросы](handlers/multi-round-trip.md)** Š¾Š±ŃŠŃŃŠ½ŃŠµŃ‚ механизм (Š²ŠŗŠ»ŃŽŃ‡Š°Ń `request_state`, который SDK запечатывает Šø ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ за вас); **[Š­Š»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŃ](handlers/elicitation.md)** описывает, как ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Š’ ŃŃ‚ŠøŃ… Š“Š²ŃƒŃ… файлах Š²ŃŃ ŠøŠ“ŠµŃ целиком: оГин сервер, оГин ŠøŠ½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ на `Resolve`, а также клиент старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ Šø современный клиент, оба ŠæŠ¾Š»ŃƒŃ‡Š°ŃŽŃ‰ŠøŠµ свой ответ от оГного Šø того же Ń€Š°Š±Š¾Ń‚Š°ŃŽŃ‰ŠµŠ³Š¾ сервера (**[ŠžŠ±ŃŠ»ŃƒŠ¶ŠøŠ²Š°Š½ŠøŠµ клиентов старого ŠæŠ¾ŠŗŠ¾Š»ŠµŠ½ŠøŃ](run/legacy-clients.md)** разбирает ŠøŃ… по шагам). **[ŠœŠ½Š¾Š³Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń‹Šµ запросы](handlers/multi-round-trip.md)** Š¾Š±ŃŠŃŃŠ½ŃŠµŃ‚ механизм (Š²ŠŗŠ»ŃŽŃ‡Š°Ń `request_state`, который SDK запечатывает Šø ŠæŃ€Š¾Š²ŠµŃ€ŃŠµŃ‚ за вас); **[Š­Š»ŠøŃ†ŠøŃ‚Š°Ń†ŠøŃ](handlers/elicitation.md)** описывает, как ŃŠæŃ€Š°ŃˆŠøŠ²Š°Ń‚ŃŒ. !!! warning "Это еГинственное место, гГе перенесённый сервер v1 Š¼ŠµŠ½ŃŠµŃ‚ повеГение" ŠŸŠµŃ€Š²Ń‹Š¼Šø на ŃŃ‚Š¾ Š½Š°Ń‚Ń‹ŠŗŠ°ŃŽŃ‚ŃŃ ваши собственные тесты: `Client(mcp)` по ŃƒŠ¼Š¾Š»Ń‡Š°Š½ŠøŃŽ ŃŠ¾Š³Š»Š°ŃŃƒŠµŃ‚ 2026-07-28 с вашим diff --git a/i18n/tr/pages/advanced/apps.md b/i18n/tr/pages/advanced/apps.md index 4f2582c9..dbe33806 100644 --- a/i18n/tr/pages/advanced/apps.md +++ b/i18n/tr/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK bunu yerleşik `Apps` uzantısı (`io.modelcontextprotocol/ui`) olarak sunar ## Yüzü olan bir saat {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ Her istemci uygulamaları Ƨizmez. Şartname bunun sizin iƧin ne anlama geldiğ Model `content`'i okur; iframe insanlar iƧindir. UI destekli bir host yine de metin sonucunu modele iletir, yalnızca metin destekleyen bir istemci ise *sadece* onu alır. Yani kanonik desen tek araƧ, iki yanıttır. `get_time`'a bir daha bakın: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` yalnızca istemci `io.modelcontextprotocol/ui` uzantısını beyan ettiğinde **ve** `mimeTypes` ayarlarında `text/html;profile=mcp-app`'i listelediğinde `True` olur. Alan zorunludur, bu yüzden onu atlayan bir istemci sayılmaz. Aynı dosyadaki `main()` tam olarak bunu beyan eder: anlaşmanın istemci tarafı, ve zengin yanıt geri gelir. +`client_supports_apps(ctx)` yalnızca istemci `io.modelcontextprotocol/ui` uzantısını beyan ettiğinde **ve** `mimeTypes` ayarlarında `text/html;profile=mcp-app`'i listelediğinde `True` olur. Alan zorunludur, bu yüzden onu atlayan bir istemci sayılmaz. Anlaşmanın istemci tarafı şöyle: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +`server.py` dosyasını HTTP üzerinden sunun, ardından istemciyi ikinci bir terminalden Ƨalıştırın: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +Zengin yanıt geri geldi. `Client` Ƨağrısından `extensions=[APPS_SUPPORT]` argümanını Ƨıkarın; aynı program bunun yerine `The time is 2026-06-26T12:00:00Z.` yazdırır. Yalnızca metin destekleyen bir istemcinin gƶrüp gƶreceği de budur. !!! warning Tek iƧerik olarak asla `"[Rendered UI]"` gibi bir yer tutucu dƶndürmeyin. Yedek metin işe yaramazsa araƧ, yalnızca metin destekleyen her istemci iƧin ve modelin kendisi iƧin işe yaramaz. O cümleyi yazın. diff --git a/i18n/tr/pages/advanced/extensions.md b/i18n/tr/pages/advanced/extensions.md index 0403a29f..a144b017 100644 --- a/i18n/tr/pages/advanced/extensions.md +++ b/i18n/tr/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Uzantılar {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string İşe yarar en küçük uzantı, bir araƧ ve bir ayarlar eşlemesidir: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()`, `capabilities.extensions["com.example/stamps"]` konumunda duyurulan değerdir. Uzantıyı ayarsız duyurmak iƧin `{}` (varsayılan) dƶndürün. * Uzantı sunucuyu hiƧbir zaman almaz. Katkıları veri olarak beyan eder; bunları `MCPServer` tüketir. Değiştirilecek bir `self.server` yoktur. -Kanıtı da `main()`: doğrudan `mcp`'ye bağlanan bellek iƧi bir istemci: +HTTP üzerinden sunun; kanıt da bir istemcidir: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Bu sayfadaki her `server.py` bu komutla sunulur; her `client.py` de ikinci bir terminalden `python client.py` ile onun yanında Ƨalışır. + ### Kendi metotlarınızı sunma {#serving-your-own-methods} Bir uzantı **yeni istek metotları** kaydedebilir: spesifikasyonunkilerin yanında sunulan kendi fiilleri: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ Metotlar **yalnızca ekleme niteliğindedir**. SDK bunu Ƨalışma zamanında de ### İstemci tarafı {#the-client-side} -Aynı dosyadaki `main()`, istemci tarafının tamamıdır; iki yarısıyla birlikte: +İstemci başlı başına bir programdır ve istemci tarafının iki yarısını da taşır: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` uzantıyı beyan eder. Beyanlar `ClientCapabilities.extensions` hĆ¢line gelir: 2026-07-28 bağlantısında eşleme istek başına `_meta` zarfında taşınır, bƶylece sunucu onu **her** istekte gƶrür; eski nesil bir bağlantıda `initialize` el sıkışmasıyla taşınır. Sunucu kodu hangisi olduğuyla ilgilenmez: `require_client_extension(ctx, ...)` ve `ctx.session.check_client_capability(...)` her iki yolda da doğru kaynağı okur. * Satıcıya ƶzgü metotlar bir katman aşağıya, `client.session.send_request(...)` düzeyine iner; `Client` yalnızca spesifikasyon fiilleri iƧin birinci sınıf metotlar kazanır. `send_request` herhangi bir `Request` alt sınıfını kabul eder, bu yüzden satıcıya ƶzgü istek olduğu gibi geƧer. +* `SearchRequest` ve taşıdığı iki model uzantının iletim sƶzleşmesidir; bu yüzden istemci onları kendisi iƧin beyan eder. Yayımlanmış bir uzantı bunları her iki tarafın da iƧe aktardığı bir pakette sunardı. ### `tools/call` isteğini yakalama {#intercepting-toolscall} @@ -109,12 +116,18 @@ Kanca `tools/call` isteğini sarmalar, başka hiƧbir şeyi değil. Her iletiyi ## Bir istemci uzantısı kullanma {#using-a-client-extension} -**İstemci uzantısı**, aynı sƶzleşmenin tüketen taraftan gƶrünüşüdür: tek bir tanımlayıcının arkasında toplanmış bir istemci tarafı davranış paketi. Ɩrnekleri `Client(extensions=[...])` ile geƧirin ve araƧları normal şekilde Ƨağırın: +**İstemci uzantısı**, aynı sƶzleşmenin tüketen taraftan gƶrünüşüdür: tek bir tanımlayıcının arkasında toplanmış bir istemci tarafı davranış paketi. Buradaki sunucu `buy` Ƨağrısını malın kendisi yerine kullanılacak bir makbuzla yanıtlar; bunu da yalnızca uzantıyı beyan etmiş bir istemci iƧin yapar: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +İstemcide ƶrnekleri `Client(extensions=[...])` ile geƧirin ve araƧları normal şekilde Ƨağırın: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)`, diğer her Ƨağrı gibi düz bir `CallToolResult` dƶndürür. Uzantının değiştirdiği şu: sunucu artık `buy` Ƨağrısını nihai bir sonuƧ yerine `receipt` adlı bir **sonuƧ biƧimiyle** yanıtlayabilir ve `Receipts`, `call_tool` dƶnmeden ƶnce onu tamamlar (burada makbuzu bir takip Ƨağrısıyla kullanarak). Ƈağrı yerinde hiƧbir şey değişmez. Uzantıyı Ƨıkarırsanız bunların hiƧbiri olmaz: sunucunun kontrol noktası onu beyan etmemiş bir istemciyi reddeder (hata -32021) ve kontrolü atlayan bir sunucudan gelen sahiplenilmiş bir biƧim, spesifikasyonun tanınmayan bir `resultType` iƧin gerektirdiği gibi doğrulamadan geƧemez. Bağlantının her iki ucunda da varsayılan olarak kapalı. @@ -124,15 +137,15 @@ Uzantıyı Ƨıkarırsanız bunların hiƧbiri olmaz: sunucunun kontrol noktası ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## İstemci uzantısı yazma {#writing-a-client-extension} `ClientExtension`'dan alt sınıf türetin ve yalnızca ihtiyaƧ duyduklarınızı geƧersiz kılın. Her birinin varsayılanı olan üç katkı türü var: `settings()`, `claims()` ve `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * Tanımlayıcı, sunucununkiyle aynı dilbilgisini izler ve sınıf tanımlandığında doğrulanır. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Uzantı fiilleri {#extension-verbs} -Bir uzantının kendi istek metotları istemci tarafında kayıt gerektirmez. Satıcıya ƶzgü bir istek türü `mcp.types.Request`'ten türer ve [Kendi metotlarınızı sunma](#serving-your-own-methods) bƶlümündeki gibi `client.session.send_request` üzerinden gider. Tek bir ekleme var: bir params anahtarının `Mcp-Name` başlığında taşınması gerektiğinde (tasks gibi uzantı spesifikasyonları kendi fiilleri iƧin bunu şart koşar) istek türü `name_param` beyan eder: +Bir uzantının kendi istek metotları istemci tarafında kayıt gerektirmez. Satıcıya ƶzgü bir istek türü `mcp.types.Request`'ten türer ve [Kendi metotlarınızı sunma](#serving-your-own-methods) bƶlümündeki gibi `client.session.send_request` üzerinden gider. Uzantısı, adlandırılmış bir iş hakkında tek bir fiil sunan bir sunucu düşünün: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +İstemcide tek bir ekleme var: bir params anahtarının `Mcp-Name` başlığında taşınması gerektiğinde (tasks gibi uzantı spesifikasyonları kendi fiilleri iƧin bunu şart koşar) istek türü `name_param` beyan eder: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Oturum, `params["jobId"]` değerini her gƶnderim yolunda `Mcp-Name` başlığına yansıtır; eksik bir değer ise gerekli bir başlığı sessizce atlamak yerine aƧıkƧa hata verir. ## Bir uzantının yapamayacakları {#what-an-extension-cannot-do} diff --git a/i18n/tr/pages/advanced/low-level-server.md b/i18n/tr/pages/advanced/low-level-server.md index 42a7a211..ba9cd121 100644 --- a/i18n/tr/pages/advanced/low-level-server.md +++ b/i18n/tr/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # Düşük seviyeli Server {#the-low-level-server} @@ -36,18 +36,22 @@ Bu, **[AraƧlar](../servers/tools.md)** sayfasında dokuz satır `@mcp.tool()` i ### Deneyin {#try-it} -Bunun iƧin Inspector yok: `mcp dev` ve `mcp run` yalnızca `MCPServer` kabul eder. Bellek iƧi `Client` bunu umursamaz; düşük seviyeli bir `Server`'ı tıpkı bir `MCPServer`'ı aldığı gibi alır: +`mcp dev` ve `mcp run` yalnızca `MCPServer` kabul eder, bu yüzden bunu kendiniz sunarsınız. `server.py` dosyasının son satırı ondan sıradan bir ASGI uygulaması oluşturur, uvicorn da onu Ƨalıştırır: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Inspector'ı ya da herhangi bir istemciyi `http://localhost:8000/mcp` adresine yƶnlendirin: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` değeri `None`. Yüksek seviyeli sunucu `-> str` dƶnüş türünü sizin yerinize `{"result": ...}` iƧine sarmalar; burada sizin oluşturmadığınızı kimse oluşturmaz. * `list_tools`, **sizin** yazdığınız şemayı karakteri karakterine dƶndürür. Yüksek seviyeli sürümde her ƶzellikte `"title": "Query"`, kƶkte de `"title": "search_booksArguments"` vardı: Pydantic'in bıraktığı izler. Burada ise ağa giden bir şey varsa onu oraya siz koymuşsunuzdur. +Testte uvicorn'u ve portu atlarsınız: `Client(server)`, düşük seviyeli bir `Server`'ı süreƧ iƧinde tıpkı bir `MCPServer`'ı aldığı gibi alır; **[Test etme](../get-started/testing.md)** sayfasının anlattığı kalıp da budur. + ## Sizin yerinize hiƧbir şey denetlenmez {#nothing-is-checked-for-you} `MCPServer`, Ƨağrıyı kendi ürettiği şemaya gƶre doğrulayarak hatalı bir argümanı fonksiyonunuz daha Ƨalışmadan reddeder (**[AraƧlar](../servers/tools.md)**). @@ -215,4 +221,4 @@ Bunların her biri, artık kavramlarını bildiğiniz birer fikir; her birinin k * `add_request_handler(method, params_type, handler)` her metodu sunar. `initialize` ayrılmıştır. * Bir `Server`'ın duyurduğu yetenekler, hangi işleyicileri kaydettiğinizden türetilir. -`Client(server)` iki sunucuya da aynı davrandı, çünkü ikisi aynı protokolün *ta kendisi*; bütün mesele de bu. Bir alt katman ise bir sınıf bile değil: **[Middleware](middleware.md)**. +İstemci iki sunucuya da aynı davrandı, çünkü ikisi aynı protokolün *ta kendisi*; bütün mesele de bu. Bir alt katman ise bir sınıf bile değil: **[Middleware](middleware.md)**. diff --git a/i18n/tr/pages/advanced/pagination.md b/i18n/tr/pages/advanced/pagination.md index e686ca1b..68454ac2 100644 --- a/i18n/tr/pages/advanced/pagination.md +++ b/i18n/tr/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # Sayfalama {#pagination} @@ -31,9 +31,13 @@ Sayfalama, kaynak listesi aslında bir veritabanı olan sunucu iƧindir: tek yan ### Deneyin {#try-it} -`Client(server)`, düşük seviyeli bir `Server`'a bellek iƧinde, bir `MCPServer`'a bağlandığı gibi bağlanır. +`mcp run` yalnızca bir `MCPServer` kabul eder; bu yüzden bunu kendiniz sunarsınız. `server.py` dosyasının son satırı `Server`'dan sıradan bir ASGI uygulaması oluşturur, uvicorn da onu Ƨalıştırır: -`list_resources()`'ı argümansız Ƨağırın. `book-1`'den `book-10`'a kadar on kaynak alırsınız ve `next_cursor`, `"10"` dizgesidir. +```console +uvicorn server:app --port 8000 +``` + +Herhangi bir istemciyi (**[İstemci](../client/index.md)** ya da Inspector) `http://localhost:8000/mcp` adresine yƶnlendirin ve `list_resources()`'ı argümansız Ƨağırın. `book-1`'den `book-10`'a kadar on kaynak alırsınız ve `next_cursor`, `"10"` dizgesidir. Bunu `list_resources(cursor="10")` ile geri verin; ilk kaynak `book-11`, yeni `next_cursor` ise `"20"` olur. @@ -43,7 +47,7 @@ Onuncu sayfa, `next_cursor` değeri `None` olarak dƶner. Bitti. `Client` üzerindeki her `list_*` metodu (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) bir `cursor=` anahtar kelimesi alır. Sayfalanmış bir listeyi sonuna kadar okumak tek bir `while True`'dur: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ Onuncu sayfa, `next_cursor` değeri `None` olarak dƶner. Bitti. * `next_cursor`'a bakmadan **ƶnce** listeyi genişletin: son sayfada da kaynaklar vardır. * Ƈıkış koşulu `next_cursor is None`'dır. Bunun dışındaki her şey, dokunulmadan doğrudan `cursor=`'a geri gider. -`main()`'ini Ƨalıştırın; `100 resources` yazdırır: on tane onluk sayfa, on sayfa olduğundan hiƧ haberi olmayan bir dƶngü tarafından birleştirilmiş. +uvicorn hĆ¢lĆ¢ `server.py` dosyasını sunarken ikinci bir terminalde `python client.py` komutunu Ƨalıştırın. `100 resources` yazdırır: on tane onluk sayfa; bunları, on sayfa olduğundan hiƧ haberi olmayan bir dƶngü birleştirir. Bu, **[İstemci](../client/index.md)** sayfasının her `list_*` fiili iƧin gƶsterdiği dƶngünün aynısıdır ve sayfalamayan bir sunucuya karşı hiƧbir maliyeti yoktur: ilk yanıtta `next_cursor`, `None` olur ve dƶngü bir kez Ƨalışır. diff --git a/i18n/tr/pages/client/caching.md b/i18n/tr/pages/client/caching.md index 359fbe25..0e734d96 100644 --- a/i18n/tr/pages/client/caching.md +++ b/i18n/tr/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # Ɩnbellekleme ipuƧları {#caching-hints} @@ -30,7 +30,7 @@ Varsayılan olarak her sonuƧ `ttlMs: 0, cacheScope: "private"` der: anında bay Alt düzey `Server`'da işleyiciler sonuƧlarını elle oluşturur ve `ttl_ms` / `cache_scope` sonuƧ modellerindeki sıradan alanlardır. Bunları aƧıkƧa ayarlayan bir işleyici, alan alan, her zaman oluşturucu eşlemesine üstün gelir: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ Sayfalandırılmış listelerle ilgili bir uyarı: protokol bir listenin **her s 2026-07-28 oturumunda `Client` ipuƧlarına sizin yerinize uyar: varsayılan olarak aƧık, yerleşik bir yanıt ƶnbelleği vardır. `ttlMs` taşıyarak gelen bir sonuƧ saklanır ve o TTL iƧinde yapılan ƶzdeş bir Ƨağrı hiƧ tur atılmadan ƶnbellekten sunulur. *HiƧ* ipucu taşımayan bir sonuƧ ƶnbelleğe alınmaz: ipucu taşımayan sonuƧlar `CacheConfig.default_ttl_ms` değerini alır, bu da varsayılan olarak `0`'dır (anında bayat); dolayısıyla hiƧbir şey beyan etmeyen bir sunucu, her zaman gƶrdüğü Ƨağrı başına bir istek trafiğinin aynısını gƶrür. -```python title="client.py" hl_lines="33 35 38" +Bunu izlemek iƧin ƶnceki bƶlümdeki `server.py` dosyasını uvicorn ile sunun (son satırı ASGI uygulamasını oluşturur). İşleyici, gerƧekten her Ƨalıştığında bir satır yazdırır: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +İkinci bir terminalden `python client.py` komutunu Ƨalıştırın. İlk sonucun taşıdığı ipuƧlarını yazdırır: işleyicinin `ttlMs` değeri, eşlemenin `cacheScope`'unun yanında: + +```text +1000 public +``` + +HikĆ¢yenin geri kalanını sunucunun terminali anlatır: uvicorn'un istek log'ları arasında `tools/list served` üç kez gƶrünür. + Dƶrt Ƨağrı, üç getirme. İkinci Ƨağrı taze bir girdi buldu ve sunucuya hiƧ ulaşmadı; (enjekte edilen) saati TTL'nin ƶtesine ilerletmek üçüncünün yeniden getirmesine yol aƧtı; dƶrdüncü `cache_mode="refresh"` dedi. Bu anahtar sƶzcük argümanı ƶnbellekleme yapan beş fiilde bulunur (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (varsayılan) varsa taze bir girdiyi sunar, yoksa getirilen sonucu saklar. @@ -56,7 +70,7 @@ Dƶrt Ƨağrı, üç getirme. İkinci Ƨağrı taze bir girdi buldu ve sunucuya `"use"`'un üzerinde bir kural vardır: **`meta` taşıyan Ƨağrılar her zaman sunucuya ulaşır.** `meta` ayarlanmış bir istek (bir ilerleme token'ı, izleme alanları) ağ üzerinde gerƧek bir istek bekler; bu yüzden `cache_mode="use"` altında `"refresh"` gibi ele alınır: ƶnbellek okuması atlanır ve getirilen sonuƧ yine de ƶnbellekteki girdinin yerine geƧer. `"bypass"` ve aƧık bir `"refresh"` her zamanki gibi davranır. -Ɩnbelleklemeyi tamamen kapatmak iƧin `Client(server, cache=None)` ile oluşturun: her Ƨağrı yeniden bir tur olur ve `cache_mode` hĆ¢lĆ¢ kabul edilse de hiƧbir şey yapmaz. +Ɩnbelleklemeyi tamamen kapatmak iƧin `Client`'ı oluştururken `cache=None` geƧirin: her Ƨağrı yeniden bir tur olur ve `cache_mode` hĆ¢lĆ¢ kabul edilse de hiƧbir şey yapmaz. Kapsama da otomatik olarak uyulur: `"private"` girdiler ƶnbelleğin *bƶlümüne* (partition, aşağıda) gƶre anahtarlanır, `"public"` olanlar ise daha geniş paylaşıma katılmayı seƧebilir. Ayrıca **bildirimler TTL'yi yener**, ama yalnızca tam olarak adlandırdıkları girdiler iƧin: bir `list_changed` bildirimi eşleşen ƶnbellekteki listeyi Ƨıkarır, `resources/updated` ise tam olarak kendi URI'si altında saklanan ƶnbellekteki okumayı Ƨıkarır; ne kadar taze olurlarsa olsunlar. 2026-07-28 bağlantısında bu bildirimler `client.listen(...)` ile aƧtığınız bir `subscriptions/listen` akışı üzerinden gelir ve Ƨıkarma, izleyiciniz olayı gƶrmeden ƶnce tamamlanır; bunun sayfası **[Abonelikler](subscriptions.md)**. @@ -82,7 +96,7 @@ client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms= Bƶlüm ayrıca `Client`'ın ƶmrü boyunca sabittir. Bağlantının yetkilendirme bağlamı oturum ortasında değişirse (ƶrneğin farklı bir principal olarak yeniden kimlik doğrulama), ƶnbellek bunu takip etmez; yeni principal iƧin yeni bir `Client` oluşturun. -Ɩnbellek anahtarları ayrıca **sunucunun kimliğini** de taşır: bağlandığınız URL dizesi, varsa `user:pass@` kullanıcı bilgisi Ƨıkarılmış ve bunun dışında bayt bayt aynı hĆ¢liyle. Büyük/küçük harf katlama yok, sorgu yeniden sıralama yok, sondaki eğik Ƨizgi temizliği yok. Az normalleştirmek yalnızca paylaşımdan ƶdün verir, aşırı normalleştirmek ise iki kiracıyı birleştirebilir (`?tenant=a` ve `?tenant=b`); bu yüzden yüzeysel olarak farklı URL'ler girdi paylaşmaz, o kadar. URL olmadığında (süreƧ iƧi bir sunucu ya da bir `Transport` ƶrneği) istemci bunun yerine ƶrnek başına rastgele bir kimlik alır; sunucuya ad vermek iƧin `CacheConfig.target_id`'yi ayarlayın (ƶzel bir depoyla bu zorunludur ve oluşturma bunu sƶyler). Kimlik, anahtar malzemesine girmeden ƶnce sha256 ile ƶzetlenir; dolayısıyla sorgu dizesinde sır taşıyan bir URL depo anahtarlarında asla gƶrünmez. Ɩzet ƶncesi hĆ¢lini siz de loglamayın. +Ɩnbellek anahtarları ayrıca **sunucunun kimliğini** de taşır: bağlandığınız URL dizesi, varsa `user:pass@` kullanıcı bilgisi Ƨıkarılmış ve bunun dışında bayt bayt aynı hĆ¢liyle. Büyük/küçük harf katlama yok, sorgu yeniden sıralama yok, sondaki eğik Ƨizgi temizliği yok. Az normalleştirmek yalnızca paylaşımdan ƶdün verir, aşırı normalleştirmek ise iki kiracıyı birleştirebilir (`?tenant=a` ve `?tenant=b`); bu yüzden yüzeysel olarak farklı URL'ler girdi paylaşmaz, o kadar. URL olmadığında (süreƧ iƧi bir sunucu ya da bir `Transport` ƶrneği) istemci bunun yerine ƶrnek başına rastgele bir kimlik alır; sunucuya ad vermek iƧin `CacheConfig.target_id`'yi ayarlayın (ƶzel bir depoyla bu zorunludur ve oluşturma bunu sƶyler). Kimlik, anahtar malzemesine girmeden ƶnce sha256 ile ƶzetlenir; dolayısıyla sorgu dizesinde sır taşıyan bir URL depo anahtarlarında asla gƶrünmez. Ɩzet ƶncesi hĆ¢lini siz de log'a yazmayın. !!! warning "`share_public` sunucuya tüm filo genelinde güvenir" Varsayılan olarak `"public"` girdiler bile kendi bƶlümlerinde kalır. `share_public=True`, sunucunun `cacheScope: "public"` olarak işaretlediği girdileri depoyu kullanan **her** bƶlüme sunar; sunucunun sınıflandırmasına hepsi adına güvenir. Kiracıya ƶzgü veriye (hata ya da kƶtü niyet sonucu) `"public"` damgası vuran bir sunucu, o zaman bir kiracının yanıtını diğerlerine sızdırır. Bayrak bilerek yalnızca oluşturucu düzeyindedir: Ƨağrı başına `cache_mode` ƶnbelleklemeyi daraltabilir, ama Ƨağrı başına hiƧbir şey paylaşımı genişletemez. diff --git a/i18n/tr/pages/client/callbacks.md b/i18n/tr/pages/client/callbacks.md index 8cb234ac..36b86ce1 100644 --- a/i18n/tr/pages/client/callbacks.md +++ b/i18n/tr/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # İstemci callback'leri {#client-callbacks} @@ -63,7 +63,7 @@ Sizden tek bir `tools/call`, sunucudan geriye tek bir `elicitation/create`, onu `Client(...)` Ƨağrısındaki `mode="legacy"` gerƧekten iş yapıyor. Varsayılan olarak `Client(...)` modern protokol yolunu müzakere eder ve o yolda sunucudan istemciye gelen istekler iƧin bir geri kanal (back-channel) yoktur: `ctx.elicit`, callback'iniz daha Ƨalışmadan başarısız olur. Buna aktarım karar vermez; müzakere edilen - protokol karar verir, bellek iƧinde de bir URL üzerinden de aynı şekilde. İstemcinizin bƶyle bir isteği + protokol karar verir. İstemcinizin bƶyle bir isteği yanıtlaması gerektiğinde `mode="legacy"`'yi sabitleyin; bu sayfanın arkasındaki her test bunu yapar. Ayrıntıların tamamı **[Protokol sürümleri](../protocol-versions.md)** sayfasında. @@ -152,4 +152,4 @@ Bunları `Client(...)`'a tıpkı `elicitation_callback` gibi geƧirin. * `sampling_callback` ve `list_roots_callback` aynı şekilde Ƨalışır ama kullanım dışı ƶzelliklere hizmet eder; modern sunucular bunun yerine Ƨok turlu istekler (multi-round-trip) kullanır. * `logging_callback` ve `message_handler` bildirimleri alır. HiƧbir şey bildirmezler. -`Client(...)`'ın ilk argümanı bir aktarım nesnesidir. **[İstemci aktarımları](transports.md)** her türünü ele alır. +`Client(...)`'ın ilk argümanı aktarımı seƧer. **[İstemci aktarımları](transports.md)** her türünü ele alır. diff --git a/i18n/tr/pages/client/index.md b/i18n/tr/pages/client/index.md index 2cc12da8..47b945d7 100644 --- a/i18n/tr/pages/client/index.md +++ b/i18n/tr/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # İstemci {#the-client} @@ -11,13 +11,23 @@ Tek bir yaşam dƶngüsü olan tek bir nesnedir: oluşturun, `async with` bloğu ## İlk istemciniz {#your-first-client} -```python title="client.py" hl_lines="14-18" +Bir istemcinin konuşacağı bir sunucuya ihtiyacı vardır. Bu sayfadaki her ƶrneğin bağlandığı sunucu aşağıdaki Bookshop. Onu `server.py` olarak kaydedin ve HTTP üzerinden Ƨalışır durumda bırakın: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Üstteki sunucu yalnızca bağlanacak bir şeyiniz olsun diye orada. İstemci, vurgulanan beş satırdan ibaret. +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` Ƨağrısına **sunucu nesnesinin kendisi** verilir. Bu, bellek iƧi aktarımdır: alt süreƧ yok, port yok, HTTP yok. Bu sayfadaki her ƶrnek ve yazdığınız her test bƶyle bağlanır. +Bu, sunucuyu `http://localhost:8000/mcp` adresinde sunar. İstemci ayrı bir programdır. Onu `client.py` olarak kaydedin ve ikinci bir terminalde `python client.py` komutunu Ƨalıştırın: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` Ƨağrısına bir **URL** verilir; bu yüzden az ƶnce başlattığınız sunucuya Streamable HTTP üzerinden bağlanır. * `async with` **yaşam dƶngüsüdür**. Bloğa girdiğinizde bağlantı kurulur ve anlaşma yapılır; Ƨıktığınızda bağlantı kesilir. `connect()` / `close()` Ƨifti yoktur ve blok bittikten sonra bir `Client` yeniden kullanılamaz. * Bloğun iƧinde bağlantı bilgileri düz ƶzellikler olarak zaten hazırdır. @@ -25,10 +35,10 @@ Tek bir yaşam dƶngüsü olan tek bir nesnedir: oluşturun, `async with` bloğu `Client` tek bir konumsal argüman alır ve aktarımı onun türünden Ƨƶzümler: -* Bir `MCPServer` (veya düşük seviyeli `Server`) ƶrneği: **süreƧ iƧinde** bağlanır. -* Bir URL dizesi (`Client("http://localhost:8000/mcp")`): Streamable HTTP, yani üretim yolu. -* Bir `StdioServerParameters`: **alt süreƧ** olarak başlatılacak komut; onunla stdin ve stdout'u üzerinden konuşulur. +* Bir URL dizesi (`Client("http://localhost:8000/mcp")`): Streamable HTTP, dağıtımda kullandığınız aktarım. +* Bir `StdioServerParameters`: yerel bir **alt süreƧ** olarak başlatılacak komut; onunla stdin ve stdout'u üzerinden konuşulur. * Bir **aktarım**: `async with ... as (read, write)` ile kullanabileceğiniz herhangi bir şey; ƶrneğin kendi HTTP istemcinizi saran `streamable_http_client(url, http_client=...)`. +* Bir `MCPServer` (veya düşük seviyeli `Server`) ƶrneği: **süreƧ iƧinde** bağlanır; alt süreƧ yok, port yok. Bu seƧenek testler iƧindir ve **[Test etme](../get-started/testing.md)** sayfası onun üzerine kurulur. Bu sayfadaki geri kalan her şey dƶrdünde de aynıdır. Başlıklar, alt süreƧler, zaman aşımları ve `Transport` protokolünün kendi sayfası var: **[İstemci aktarımları](transports.md)**. @@ -49,11 +59,11 @@ HiƧ protokol sürümü seƧmediniz. Varsayılan olarak `Client` sunucuyu yoklar ## AraƧları listeleme {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` bir `ListToolsResult` dƶndürür; araƧlar `.tools` iƧindedir. Her biri, bir host'un modele vereceği eksiksiz tanımdır: +`list_tools()` bir `ListToolsResult` dƶndürür; araƧlar `.tools` iƧindedir. Her biri, bir host'un modele vereceği eksiksiz tanımdır. İşte ilki: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Bu şema, bir arayüzün argüman formu oluşturması iƧin gereken her şeydir; bir modelin geƧerli argümanlar üretmesi iƧin gereken her şey de odur. +İkinci araƧ olan `lookup_book`, `title=` olmadan kaydedildi; bu yüzden `tool.title` değeri `None`. + !!! tip `title` isteğe bağlıdır; bu yüzden araƧları bir insana gƶsteren arayüzün seƧim yapması gerekir: varsa `title`, yoksa `name`. `from mcp.shared.metadata_utils import get_display_name` tam olarak bunu yapar; @@ -86,7 +98,7 @@ Bu şema, bir arayüzün argüman formu oluşturması iƧin gereken her şeydir; `call_tool(name, arguments)` aracı Ƨalıştırır ve size bir `CallToolResult` geri verir. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ Tek dƶnüş değeri, okunacak üç şey. Her birinin tüketicisi farklı. Kaynak fiilleri Ƨift gelir: listelemenin iki yolu, okumanın tek yolu. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ Bir istemciye bir kaynağın ne zaman değiştiği de bildirilebilir. 2025 nesli ## Prompt'lar {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ Host bu mesajları doğrudan modele verir. Ɩzelliğin tamamı bu. Tamamlama işleyicisi olan bir sunucu, kullanıcı yazdıkƧa prompt ve kaynak şablonu argümanlarını otomatik tamamlayabilir. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ Yanıt `result.completion.values` iƧindedir. `"p"` yazın, sunucu `['poetry']` Her `list_*` yƶntemi bir `cursor=` anahtar sƶzcüğü alır ve her sonuƧ bir `next_cursor` taşır. `next_cursor` `None` olduğunda her şeyi almışsınız demektir. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Bu dƶngü her sunucuya karşı doğrudur. `MCPServer` her şeyi tek sayfada dƶndürür; bu yüzden `next_cursor` `None` olur ve dƶngü bir kez Ƨalışır. Ƈoğu kodun bunu hiƧ yazmamasının nedeni budur. GerƧekten sayfalayan sunucular ve imleƧlerin uyduğu kurallar **[Sayfalama](../advanced/pagination.md)** sayfasında. +`list_all_tools` her sunucuya karşı doğrudur. `MCPServer` her şeyi tek sayfada dƶndürür; bu yüzden `next_cursor` `None` olur ve dƶngü bir kez Ƨalışır. Ƈoğu kodun bunu hiƧ yazmamasının nedeni budur. GerƧekten sayfalayan sunucular ve imleƧlerin uyduğu kurallar **[Sayfalama](../advanced/pagination.md)** sayfasında. ## Testlerde {#in-tests} -SüreƧ ve port olmadan `Client(mcp)`, sunucunuz iƧin zaten bir test düzeneğidir. +Bu sayfadaki her `client.py`, `server.py` dosyasına HTTP üzerinden ulaştı. Bir testte ağı atlar ve `Client`'a sunucu nesnesinin kendisini verirsiniz: `from server import mcp`, ardından `Client(mcp)`. SüreƧ yok, port yok; yukarıdaki her yƶntem aynı şekilde Ƨalışır. -Bunun iƧin yapılmış tek bir kurucu bayrağı var: `Client(mcp, raise_exceptions=True)`. Yalnızca bellek iƧi bağlantılarda etkisi olur; onu aƧıklayan ve bütün kalıbı onun etrafında kuran sayfa ise **[Test etme](../get-started/testing.md)**. +Bunun iƧin yapılmış tek bir kurucu bayrağı var: `Client(mcp, raise_exceptions=True)`. Yalnızca süreƧ iƧi bağlantılarda etkisi olur; onu aƧıklayan ve bütün kalıbı onun etrafında kuran sayfa ise **[Test etme](../get-started/testing.md)**. ## Ɩzet {#recap} -* `Client(x)` bir sunucu nesnesine bellek iƧinden, bir URL dizesine Streamable HTTP üzerinden, geri kalan her şeye de bir aktarım aracılığıyla bağlanır. +* `Client(x)` bir URL dizesine Streamable HTTP üzerinden bağlanır, bir `StdioServerParameters` iƧin alt süreƧ başlatır, bir aktarıma doğrudan girer ve testlerde sunucu nesnesinin kendisini alır. * `async with` yaşam dƶngüsünün tamamıdır. İƧinde `server_capabilities` ve `protocol_version` zaten doludur; sunucu sağladığında `server_info` ve `instructions` da ƶyle. * `list_tools()` size her aracın `name`, `title`, `description` ve `input_schema` değerlerini verir. * `call_tool()` model iƧin `content`, kodunuz iƧin `structured_content` ve `is_error` dƶndürür. İstisna fırlatan bir araƧ istisna değil, sonuƧtur. diff --git a/i18n/tr/pages/client/oauth-clients.md b/i18n/tr/pages/client/oauth-clients.md index 640f85a3..8619f430 100644 --- a/i18n/tr/pages/client/oauth-clients.md +++ b/i18n/tr/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth istemcileri {#oauth-clients} @@ -82,20 +82,22 @@ GerƧek bir istemci `input()` Ƨağırmak yerine yƶnlendirme URI'si üzerinde k `Client` ilk kez bir istek gƶnderdiğinde sunucu `401` yanıtını verir. Sağlayıcı devralır: -1. **Keşif.** `WWW-Authenticate` başlığını okur, sunucunun Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource` adresinden alır, bu kaynağı hangi yetkilendirme sunucusunun koruduğunu öğrenir ve *o* sunucunun metadatasını alır. +1. **Keşif.** `WWW-Authenticate` başlığını okur, sunucunun Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource` adresinden alır, bu kaynağı hangi yetkilendirme sunucusunun koruduğunu öğrenir ve *o* sunucunun metadatasını alır. (Kaynak metadatası yayımlamayan daha eski bir sunucudan bunun yerine kendi kƶkeninde yetkilendirme sunucusu metadatası istenir.) Her iki durumda da metadata, `issuer` alanında hangi sunucu iƧin alındıysa o sunucuyu belirtmelidir; başka her şey reddedilir. 2. **Kayıt.** Depoda bir şey yok mu? `OAuthClientMetadata`'nızla sizi dinamik olarak kaydeder ve sonucu saklar. 3. **Yetkilendirme.** PKCE Ƨiftini ve bir `state` üretir, yetkilendirme URL'sini oluşturur, `redirect_handler`'ınızı await eder, ardından kod iƧin `callback_handler`'ınızı await eder. 4. **Değişim.** Kodu bir `OAuthToken` ile takas eder, onu saklar ve ƶzgün isteğinizi `Authorization: Bearer ...` ile yeniden gƶnderir. Bundan sonra sessizdir. Token'lar depodan gelir, süresi dolmuş bir erişim token'ı yenileme token'ıyla yenilenir ve ancak bunların hiƧbiri işe yaramadığında akışı yeniden Ƨalıştırır. +Bu isteklerin hepsi iƧin tek bir aktarım kuralı geƧerlidir: iƧinde Ƨalıştıkları MCP isteği gibi, bir yƶnlendirmeyi yalnızca aynı kƶkende kalıyor ve metodu koruyorsa izlerler (ƶrneğin sondaki eğik Ƨizgi iƧin bir 307/308); başka her yƶnlendirmeyi o URL yanıt vermiyormuş gibi ele alırlar. + Bunların hiƧbirini siz yazmadınız. Geriye iki anahtar sƶzcük argümanı kalır (`client_metadata_url` ve `validate_resource_url`) ve bu dosyanın ikisine de ihtiyacı yoktur. Bilmeye değer olanı `client_metadata_url`'dir; aşağıda kendi bƶlümü var. ### Deneyin {#try-it} -Bu belgelerdeki ƶrneklerin Ƨoğunu bellek iƧi bir `Client(server)` ile sınayabilirsiniz. Bunu değil: akışın bütün amacı bir HTTP `401`'idir ve bellek iƧi bir istemci ile sunucusu arasında HTTP yoktur. +Testlerinizin kullandığı bellek iƧi `Client(server)` burada işe yaramaz: akışın bütün amacı bir HTTP `401`'idir ve bellek iƧi bir istemci ile sunucusu arasında HTTP yoktur. -Depo canlı sürümü iƧerir. `examples/servers/simple-auth/` bağımsız bir yetkilendirme sunucusu ile korumalı bir MCP sunucusu Ƨalıştırır; `examples/clients/simple-auth-client/` ise bu sayfadaki istemcinin küçük bir CLI'a dƶnüşmüş hĆ¢lidir. README'sinde iki komut var: sunucuları başlatın, istemciyi onlara karşı Ƨalıştırın ve dƶrt adımın geƧişini izleyin. +Depo canlı sürümü iƧerir. `examples/servers/simple-auth/` bağımsız bir yetkilendirme sunucusu ile korumalı bir MCP sunucusu Ƨalıştırır; `examples/clients/simple-auth-client/` ise bu sayfadaki istemcinin küçük bir CLI aracına dƶnüşmüş hĆ¢lidir. README'sinde iki komut var: sunucuları başlatın, istemciyi onlara karşı Ƨalıştırın ve dƶrt adımın geƧişini izleyin. ## Client ID Metadata Documents {#client-id-metadata-documents} @@ -111,13 +113,14 @@ Bir gece gƶrevi, bir CI adımı, başka bir servis. Tarayıcı yok, "allow" dü `ClientCredentialsOAuthProvider` aynı `httpx2.Auth`'tur, insan hariƧ: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` Neler değişti: * `OAuthClientMetadata` yok, işleyiciler yok. `client_id` ve `client_secret` geƧirirsiniz; sağlayıcı bunların etrafında asgari bir `client_credentials` kaydı oluşturur ve dinamik kaydı tamamen atlar. +* `issuer`, bu kimlik bilgilerini veren yetkilendirme sunucusunu belirtir; onun `/.well-known/oauth-authorization-server` belgesinin dƶndürdüğü `issuer` değerini kullanın. Keşif yine yukarıdaki gibi Ƨalışır, ancak token istekleri yalnızca *o* yayıncıya ait metadatadan oluşturulur; MCP sunucusu başka bir yeri gƶsteriyorsa akış bunun yerine bir `OAuthFlowError` ile durur. Bu parametreyi atlamak kullanım dışı bırakıldı ve 3.0 sürümünde zorunlu hĆ¢le gelir (bkz. **[Kullanım dışı ƶzellikler](../deprecated.md#deprecated-sdk-helpers)**); o zamana kadar sağlayıcı uyarı verir ve keşif hangi yetkilendirme sunucusunu bulursa onu kullanır. * `scope`, boşlukla ayrılmış bir dizedir; OAuth'un iletilen verideki biƧimi budur. * Bundan sonraki her şey aynıdır: aynı `TokenStorage`, aynı `httpx2.AsyncClient(auth=...)`, aynı `streamable_http_client`. @@ -130,7 +133,7 @@ Varsayılan olarak sır, token isteğinde HTTP Basic kimlik doğrulaması olarak `mcp.client.auth.extensions.client_credentials` iƧinde bir sağlayıcı daha var: paylaşılan bir sır yerine JWT ile kimlik doğrulayan istemciler iƧin **`PrivateKeyJWTOAuthProvider`** (`private_key_jwt`; anahtar Ƨifti ve iş yükü kimliği türü). Aynı kalıbı izler: - bir tane oluşturun, `auth=`'a koyun. Aynı modül, onun assertion'ını oluşturan iki yardımcıyı da + bir tane oluşturun (aynı isteğe bağlı `issuer` parametresini alır), `auth=`'a koyun. Aynı modül, onun assertion'ını oluşturan iki yardımcıyı da sunar: `SignedJWTParameters` ve `static_assertion_provider`. İnsansız bir durum daha var: istemci, hangi MCP sunucularına erişebileceğine kullanıcının değil kimlik sağlayıcısının karar verdiği bir kuruluşa aittir. Bu, kendi güven modeli ve kendi sayfası olan farklı bir yetkilendirme türüdür: **[Kimlik beyanı](identity-assertion.md)**. diff --git a/i18n/tr/pages/client/session-groups.md b/i18n/tr/pages/client/session-groups.md index 25e965c4..0499620d 100644 --- a/i18n/tr/pages/client/session-groups.md +++ b/i18n/tr/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Oturum grupları {#session-groups} @@ -78,7 +78,7 @@ Elinizde zaten bağlı bir `ClientSession` varsa (`Client.session` bunlardan bir ## Ɩzet {#recap} * `ClientSessionGroup` birƧok sunucu bağlantısını tutar ve bunların araƧlarını, kaynaklarını ve prompt'larını birer `dict`'te birleştirir. -* Her sunucu iƧin `connect_to_server(params)`. Aktarım parametreleri alır; bir `Client`'ın aldığı sunucu nesnesini ya da URL'yi asla almaz. +* Her sunucu iƧin `connect_to_server(params)`. Aktarım parametreleri alır; bir `Client`'ın aldığı URL'yi ya da `Transport`'u asla almaz. * `group.call_tool(name, arguments)` Ƨağrıyı sizin yerinize sahibi olan sunucuya yƶnlendirir. * Adlar grubun tamamında benzersiz olmalıdır; `search` aracı olan iki sunucu kendi hĆ¢llerine bırakılırsa bir arada bulunamaz. * `component_name_hook=` kaydedilen her adı yeniden yazar. Sƶzlük anahtarı değişir, ağ üzerindeki ad değişmez. diff --git a/i18n/tr/pages/client/transports.md b/i18n/tr/pages/client/transports.md index 552ede30..bbf104cc 100644 --- a/i18n/tr/pages/client/transports.md +++ b/i18n/tr/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # İstemci aktarımları {#client-transports} @@ -11,30 +11,15 @@ Aktarımı hiƧbir zaman ayrıca yapılandırmazsınız. `Client` tek bir konums Her birinin *sunucu* tarafı (`mcp.run()`'ın ne yaptığı ve neyi dağıttığınız) **[Sunucunuzu Ƨalıştırma](../run/index.md)** sayfasında. -## Bellek iƧinde {#in-memory} - -Sunucu nesnesinin kendisini geƧirin: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -Alt süreƧ yok, port yok, ağ üzerinde tek bir bayt yok. İstemci ve sunucu aynı süreƧteki iki nesnedir; yine de Ƨağrı gerƧek protokol katmanından geƧer: `search_books`, HTTP üzerinden nasıl olacaksa tam olarak ƶyle listelenir, doğrulanır ve Ƨağrılır. - -Bu, onu aynı anda iki şey yapar: - -* **Bir test düzeneği.** Bu belgelerdeki her ƶrnek bu şekilde Ƨalıştırılır ve **[Test etme](../get-started/testing.md)** sayfası tüm deseni bunun üzerine kurar. -* **Bir gƶmme API'si.** Sunucuyu oluşturan bir uygulamanın, araƧlarını Ƨağırmak iƧin ağ üzerinden bir sıƧrama yapmasına gerek yoktur. - ## Streamable HTTP {#streamable-http} -Bir URL dizesi geƧirin; arkasına dağıtım yaptığınız aktarım olan **Streamable HTTP**'yi elde edersiniz: +Bir URL dizesi geƧirin; arkasına dağıtım yaptığınız ve ilk başvurmanız gereken aktarım olan **Streamable HTTP**'yi elde edersiniz: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -Üretim istemcisinin tamamı bu. `Client`, URL'yi sizin iƧin `streamable_http_client(...)` ile sarar; bunu da MCP'nin gerektirdiği şekilde yapılandırılmış bir `httpx2.AsyncClient` üzerine kurar: `follow_redirects=True`, connect/write/pool iƧin 30 saniyelik zaman aşımı ve sunucu bir yanıt akışını aƧık tutabileceği iƧin 300 saniyelik okuma zaman aşımı. +Üretim istemcisinin tamamı bu. `Client`, URL'yi sizin iƧin `streamable_http_client(...)` ile sarar; bunu da MCP'nin gerektirdiği şekilde yapılandırılmış bir `httpx2.AsyncClient` üzerine kurar: connect/write/pool iƧin 30 saniyelik zaman aşımı ve sunucu bir yanıt akışını aƧık tutabileceği iƧin 300 saniyelik okuma zaman aşımı. !!! check Oluşturduğunuz bir `Client` bağlı **değildir**. Oluşturma yalnızca aktarımı seƧer; @@ -50,7 +35,7 @@ Bir URL dizesi geƧirin; arkasına dağıtım yaptığınız aktarım olan **Str Bir `Authorization` başlığına, bir Ƨereze, bir vekil sunucuya, mTLS'e ya da farklı bir zaman aşımına ihtiyaƧ duyduğunuz anda `httpx2.AsyncClient`'ı kendiniz oluşturun ve `streamable_http_client`'a verin: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -79,9 +64,30 @@ ortam değişkenlerini ayarlayın ya da `httpx2.AsyncClient`'ınıza aƧıkƧa b !!! info `httpx2`, tanıdık `httpx` API'sini korur; yani `httpx`'i biliyorsanız kimlik doğrulama, vekil sunucular, olay kancaları, yeniden denemeler ve bağlantı sınırlarının burada nasıl yapılacağını zaten biliyorsunuz. SDK üzerine hiƧbir şey eklemez, - hiƧbir şeyi de eksiltmez. OAuth'un takıldığı yer de burası: + [yƶnlendirme işleme](#redirects) dışında hiƧbir şeyi de eksiltmez. OAuth'un takıldığı yer de burası: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Bu akışın tamamı **[OAuth istemcileri](oauth-clients.md)** sayfasında. +### Yƶnlendirmeler {#redirects} + +Aktarım, verdiğiniz URL'ye ve yalnızca o kƶkene (origin) bağlanır. + +* Aynı şema, ana bilgisayar ve portta kalan bir `307`/`308` yƶnlendirmesi izlenir; aynı ana bilgisayar üzerindeki `http://` → `https://` yƶnlendirmesi de ƶyle. Bu, alışıldık `/mcp` → `/mcp/` sondaki eğik Ƨizgi yƶnlendirmesini kapsar. +* Başka herhangi bir yere yapılan yƶnlendirme **izlenmez**. Ƈağrı şu hatayla başarısız olur: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + O URL kastettiğiniz sunucuysa yapılandırmanıza yazın. Değilse sunucu ya da ƶnündeki bir vekil sunucu yanlış yapılandırılmıştır. + +Bu, geƧirdiğiniz her `httpx2.AsyncClient` iƧin geƧerlidir: MCP isteklerinde `follow_redirects` ayarına hiƧbir yƶnde bakılmaz. SDK'nın OAuth sağlayıcıları da aynı kuralı kendi isteklerine uygular. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` iletisi, + sunucunun haberdar olmadığı, TLS sonlandıran bir vekil sunucunun arkasında durduğu ve `http://` yƶnlendirmeleri + verdiği anlamına gelir. Bu, sunucu tarafında düzeltilir (**[Dağıtım ve ƶlƧekleme](../run/deploy.md#behind-a-tls-terminating-proxy)**) + ya da iletinin ƶnerdiği tam `https://…/` URL'si kullanılarak. + ## stdio {#stdio} Bir **stdio** sunucusu bir alt süreƧtir. İstemci onu başlatır, stdin'ine JSON-RPC yazar ve stdout'undan JSON-RPC okur. Bir masaüstü host'un makinenizde bir sunucuyu Ƨalıştırma biƧimi budur: bir host, bu kod artı bir kullanıcı arayüzü*dür* ve **[GerƧek bir host'a bağlanma](../get-started/real-host.md)**, aynı ilişkinin host'un tarafından, bir yapılandırma dosyası olarak gƶrülen halidir. @@ -104,6 +110,18 @@ Alt sürecin stderr'i sizinkine gider. Başka bir yere gƶndermek iƧin aktarım Bir API anahtarına ihtiyaƧ duyan bir sunucu onu orada bulamaz. `env=` ile aƧıkƧa geƧirin; bu değişkenler izin listesinin üstüne birleştirilir. Yukarıda `BOOKSHOP_API_KEY`'in yaptığı budur. +## Bellek iƧinde {#in-memory} + +Bir testte dağıtılacak da başlatılacak da bir şey yoktur. Sunucu nesnesinin kendisini geƧirin: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +Alt süreƧ yok, port yok, ağ üzerinde tek bir bayt yok. İstemci ve sunucu aynı süreƧteki iki nesnedir; yine de Ƨağrı gerƧek protokol katmanından geƧer: `search_books`, HTTP üzerinden nasıl olacaksa tam olarak ƶyle listelenir, doğrulanır ve Ƨağrılır. **[Test etme](../get-started/testing.md)** sayfası tüm deseni bunun üzerine kurar. + +Aynı biƧim bir gƶmme API'si olarak da iş gƶrür: sunucuyu kendisi oluşturan bir uygulama, araƧlarını ağ üzerinden bir sıƧrama olmadan Ƨağırabilir. + ## SSE {#sse} `mcp.client.sse` iƧindeki `sse_client(url)`, Streamable HTTP'nin yerini aldığı HTTP aktarımıdır. HĆ¢lĆ¢ onu konuşan bir sunucuyla konuşmak iƧin aynı şekilde sarın, `Client(sse_client("http://localhost:8000/sse"))`, ve üzerine yeni hiƧbir şey kurmayın. @@ -112,15 +130,16 @@ Alt sürecin stderr'i sizinkine gider. Başka bir yere gƶndermek iƧin aktarım `Client` iƧin yukarıdakilerin hepsi aynı şeydir. -Bir **aktarım**, `(read, write)` mesaj akışı Ƨifti veren herhangi bir asenkron bağlam yƶneticisidir: resmi olarak `mcp.client` iƧindeki `Transport` protokolü. `Client`, argümanını türüne gƶre Ƨƶzümler: bir sunucu nesnesi süreƧ iƧinde bağlanır, bir `str` `streamable_http_client(url)` olur, bir `StdioServerParameters` `stdio_client(params)` olur ve geri kalan her şeye doğrudan bir aktarım olarak girilir. `stdio_client(...)`, `streamable_http_client(...)` ve `sse_client(...)`'in hepsinin aynı yuvaya oturmasının ve kendinizinkini yazabilmenizin nedeni bu son kuraldır. +Bir **aktarım**, `(read, write)` mesaj akışı Ƨifti veren herhangi bir asenkron bağlam yƶneticisidir: resmi olarak `mcp.client` iƧindeki `Transport` protokolü. `Client`, argümanını türüne gƶre Ƨƶzümler: bir `str` `streamable_http_client(url)` olur, bir `StdioServerParameters` `stdio_client(params)` olur, bir sunucu nesnesi süreƧ iƧinde bağlanır ve geri kalan her şeye doğrudan bir aktarım olarak girilir. `stdio_client(...)`, `streamable_http_client(...)` ve `sse_client(...)`'in hepsinin aynı yuvaya oturmasının ve kendinizinkini yazabilmenizin nedeni bu son kuraldır. ## Ɩzet {#recap} -* `Client(mcp)` (sunucu nesnesi) bellek iƧinde bağlanır. Testler ve gƶmme iƧin kullanın. * `Client("http://.../mcp")` (bir URL), üretim aktarımı olan Streamable HTTP üzerinden bağlanır. * Başlıklar, kimlik doğrulama, vekil sunucular ve zaman aşımları, `streamable_http_client(url, http_client=...)`'a geƧirdiğiniz bir `httpx2.AsyncClient` üzerinde yer alır. `headers=` anahtar sƶzcüğü yoktur. +* Yƶnlendirmeler yalnızca URL'nin kendi kƶkeni iƧinde (sondaki eğik Ƨizgi iƧin `307`/`308`) ve aynı ana bilgisayarda `http`→`https` iƧin izlenir. Geri kalan her şey `Redirect to … not followed` hatasıyla başarısız olur; nihai URL'yi yapılandırın. * stdio, `Client(StdioServerParameters(...))` demektir. Onu `stdio_client(...)` ile yalnızca alt sürecin stderr'ini başka yere yƶnlendirmek iƧin kendiniz sarın. * Alt süreƧ sizinkini değil, izin listesine gƶre oluşturulmuş bir ortam alır; `env=` buna ekleme yapar. +* `Client(mcp)` (sunucu nesnesi) bellek iƧinde bağlanır. Testlerde ya da bir sunucuyu onu oluşturan uygulamaya gƶmmek iƧin kullanın. * Bir aktarım, `async with x as (read, write)` yapabildiğiniz herhangi bir şeydir. `Client`, sunucu nesnesi, URL ya da `StdioServerParameters` olmayan her şeyi doğrudan bu protokole verir. * Bir `Client` oluşturmak aktarımı seƧer. Onu `async with` aƧar. diff --git a/i18n/tr/pages/deprecated.md b/i18n/tr/pages/deprecated.md index af116483..945cdba6 100644 --- a/i18n/tr/pages/deprecated.md +++ b/i18n/tr/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Kullanım dışı ƶzellikler {#deprecated-features} -2026-07-28 spesifikasyonu beş şeyi emekliye ayırıyor. SDK hĆ¢lĆ¢ hepsini uygular ve artık her biri bir **kullanım dışı bırakma uyarısı** taşır. Bir SDK yardımcısı ise kendi gerekƧesiyle kullanım dışı bırakıldı ve [sayfanın sonunda](#deprecated-sdk-helpers) listeleniyor. +2026-07-28 spesifikasyonu beş şeyi emekliye ayırıyor. SDK hĆ¢lĆ¢ hepsini uygular ve artık her biri bir **kullanım dışı bırakma uyarısı** taşır. SDK düzeyindeki birkaƧ kullanım dışı bırakma ise kendi gerekƧesine dayanır ve [sayfanın sonunda](#deprecated-sdk-helpers) listeleniyor. Aşağıdaki tablo kullanım dışı bırakılan her ƶzelliği, neden gittiğini ve yerine neyin üzerine inşa etmeniz gerektiğini gƶsterir. @@ -136,11 +136,13 @@ API'nin tamamı bu. Yƶntem başına bir anahtar yok, zaten istemezsiniz de: tek ## Kullanım dışı SDK yardımcıları {#deprecated-sdk-helpers} -Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi olan SDK iƧ ayrıntılarıdır. Aynı `MCPDeprecationWarning` ile uyarırlar ve 3.0'da kaldırılacaklar. +Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi olan SDK kullanım biƧimleridir. Aynı `MCPDeprecationWarning` ile uyarırlar ve 3.0 eski biƧimi kaldırır. | Kullanım dışı | Bunun yerine ne yaparsınız | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, ardından `FuncMetadata.call_fn()`. Bunu yalnızca `FuncMetadata`'yı doğrudan kullanan kod (ƶrneğin ƶzel bir `Tool` alt sınıfı) Ƨağırırdı. | +| `validate_token_resource=` olmadan `AuthSettings(resource_server_url=...)` | Onu ayarlayın: `True`, doğrulayıcınızın `resource_server_url` iƧin düzenlendiğini bildirmediği bearer token'ları sunucunun reddetmesini sağlar; `False`, token'ın audience değerini doğrulayıcınızın kendisinin denetlediğini sƶyler (bkz. **[Yetkilendirme](run/authorization.md#a-token-verifier)**). Ayarlanmadığında `False` gibi davranır; 3.0, `resource_server_url` ayarlı olduğunda `True`'yu varsayılan yapar. | +| `issuer=` olmadan `ClientCredentialsOAuthProvider(...)` veya `PrivateKeyJWTOAuthProvider(...)` | Kimlik bilgilerini düzenleyen yetkilendirme sunucusunu belirten `issuer=` geƧirin (bkz. **[OAuth istemcileri yazma](client/oauth-clients.md#machine-to-machine)**). Onsuz, kimlik bilgilerini hangi yetkilendirme sunucusunun alacağına MCP sunucusu karar verir; 3.0 bu anahtar kelimeyi zorunlu yapar. | ## Ɩzet {#recap} @@ -149,7 +151,7 @@ Bunlar spesifikasyon değişikliği değil, yalnızca daha iyi bir alternatifi o * Kullanım dışı bırakma tavsiye niteliğindedir: iletilen veride değişiklik yok, her şey 2026 ƶncesi oturumlarda Ƨalışmaya devam eder ve gƶrünür bir `MCPDeprecationWarning` alırsınız (bir `UserWarning`, dolayısıyla varsayılan olarak aƧık). * Ɩrnekleme ve kƶk dizinler ayrıca, 2026-07-28 oturumunda bulunmayan bir geri kanala ihtiyaƧ duyar. Modern bir bağlantıda ƶnce uyarır, sonra istisna fırlatırlar. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` tüm kategoriyi susturur; pytest'te `"error::mcp.MCPDeprecationWarning"` bunu bir test hatasına dƶnüştürür. -* Bir SDK yardımcısı, `FuncMetadata.call_fn_with_arg_validation()`, 3.0'da kaldırılmak üzere ayrıca kullanım dışı bırakıldı. +* [SDK düzeyindeki kullanım dışı bırakmalar](#deprecated-sdk-helpers) da aynı kurala uyar: şimdi uyarırlar, 3.0 ise eski biƧimi kaldırır. * Yeni kod bunların hiƧbiri üzerine kurulmamalıdır. Bu belgelerdeki diğer tüm sayfalar güncel API'yi anlatır. diff --git a/i18n/tr/pages/get-started/first-steps.md b/i18n/tr/pages/get-started/first-steps.md index e7e9ec74..f52e64fc 100644 --- a/i18n/tr/pages/get-started/first-steps.md +++ b/i18n/tr/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # İlk adımlar {#first-steps} @@ -17,7 +17,7 @@ Bundan sonraki her sayfada gƶreceğiniz üç sƶzcük: * **İstemci**, host'un iƧinde yaşar ve MCP konuşur. Host, bağlandığı her sunucu iƧin bir istemci Ƨalıştırır. * **Sunucu**, bu SDK ile sizin oluşturduğunuz şeydir. İstemcilere bir şeyler sunar. Modelle hiƧbir zaman doğrudan konuşmaz. -Sunucuyu siz yazarsınız. Host'lar başkasının ürünüdür. SDK size bir de `Client` verir. Onu sunucularınızı test etmek iƧin kullanırsınız; bu sayfanın ilerisinde karşınıza Ƨıkar. +Sunucuyu siz yazarsınız. Host'lar başkasının ürünüdür. SDK size bir de `Client` verir: bir host'un bir sunucuya URL ile ulaşmak ya da onu alt süreƧ olarak başlatmak iƧin kullanacağı sınıfın ta kendisi. Bu sayfanın ilerisinde karşınıza Ƨıkar; sunucularınızı da onunla test edeceksiniz. ## Üç temel öğe {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector'da üç sekme gƶrdünüz. Üç tane olduğunu nereden bildi? Bir istemci bağlandığında sunucu **yeteneklerini** beyan eder: hangi istek ailelerini yanıtlayacağını. İstemci, neyi isteyeceğine karar vermek iƧin bu beyanı kullanır. Bunu siz hiƧ yazmadınız; `MCPServer` sizin yerinize beyan eder. -Kendiniz bakın. SDK'nın `Client`'ı sunucu nesnesini doğrudan kabul eder ve ona **bellek iƧinde** bağlanır (alt süreƧ yok, port yok): +Kendiniz bakın. Bir terminalde `server.py` dosyasını HTTP üzerinden Ƨalışır halde bırakın: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +ve başka bir terminalden bir istemciyi ona yƶneltin: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -115,11 +113,12 @@ Bu sƶzlük, sunucunuzun beyan ettiği **yeteneklerdir**. Bağlanan her istemcin `MCPServer` üç temel öğenin hepsini sunar; bu yüzden üçü de her zaman beyan edilir. -Orada ne olmadığına dikkat edin. `completions` (kaynak şablonları ve prompt'lar iƧin argüman otomatik tamamlama) sizin yazacağınız bir işleyici gerektirir; bu sunucuda yok, dolayısıyla yetenek de yok ve uslu bir istemci sormaz. İsteğe bağlı her şey iƧin kural budur: şeyi kaydedin, yetenek belirir; **[Tamamlamalar](../servers/completions.md)** bunu kanıtlar. +Orada ne olmadığına dikkat edin. `completions` (kaynak şablonları ve prompt'lar iƧin argüman otomatik tamamlama) sizin yazacağınız bir işleyici gerektirir; bu sunucuda yok, dolayısıyla yetenek de yok ve kurallara uyan bir istemci sormaz. İsteğe bağlı her şey iƧin kural budur: şeyi kaydedin, yetenek belirir; **[Tamamlamalar](../servers/completions.md)** bunu kanıtlar. !!! info - `Client(mcp)`, bu belgelerdeki her ƶrneğin test edildiği aynı bellek iƧi istemcidir; - sizinkileri de bƶyle test edeceksiniz. Kendine ait koca bir sayfası var: **[Test etme](testing.md)**. + Bu `client.py` eksiksiz bir MCP istemcisidir; sayfası da **[İstemci](../client/index.md)**. + Bir testte terminali ve portu atlar, `Client`'a doğrudan sunucu nesnesinin kendisini verirsiniz: + `Client(mcp)`. Onun da kendine ait koca bir sayfası var: **[Test etme](testing.md)**. ## Yazmadıklarınız {#what-you-did-not-write} @@ -128,7 +127,7 @@ Bu sayfaya dƶnüp bir bakın. Üç küçük Python fonksiyonu yazdınız. Şunl * Bir JSON Schema. `a: int, b: int`, `add` şemasının *ta kendisidir*. * Bir istek işleyici. `tools/list`, `resources/read`, `prompts/get`: hepsi sizin yerinize sunulur. * Bir yetenek beyanı. `MCPServer` onu sizin yerinize yaptı. -* Tek satır protokol. Sürüm anlaşması, JSON-RPC ƧerƧevelemesi, yetenek değiş tokuşu: hepsi `mcp dev` ve `Client(mcp)` iƧinde oldu ve siz hiƧbirini gƶrmediniz. +* Tek satır protokol. Sürüm anlaşması, JSON-RPC ƧerƧevelemesi, yetenek değiş tokuşu: hepsi `mcp dev` ve `client.py` iƧinde oldu ve siz hiƧbirini gƶrmediniz. SDK'nın bütün meselesi bu oran. @@ -139,6 +138,6 @@ SDK'nın bütün meselesi bu oran. * Her temel öğe iƧin bir dekoratƶr: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Ad, aƧıklama ve şema fonksiyondan gelir. * İƧinde `{param}` olan bir URI, somut kaynaklardan ayrı listelenen bir kaynak **şablonu** oluşturur. * Sunucunun **yetenekleri** sizin yerinize beyan edilir ve bir istemci yalnızca sunucunun beyan ettiklerini ister. -* `Client(mcp)` sunucu nesnesine bellek iƧinde bağlanır: ilk günden test düzeneğiniz. +* `Client("http://localhost:8000/mcp")` Ƨalışan sunucunuzla konuşur. Ona bunun yerine sunucu nesnesini verin, `Client(mcp)`, ve ilk günden test düzeneğiniz olur. Sırada **[GerƧek bir host'a bağlanma](real-host.md)** var: bu sunucu, gerƧekten, Claude Desktop'ın ya da bir IDE'nin iƧinde. Ardından **[Test etme](testing.md)**: bir sayfa, bir bellek iƧi istemci ve Ƨalışıp Ƨalışmadığını bir daha asla tahmin etmek zorunda kalmazsınız. Ondan sonra her temel öğenin kendi sayfası var; modelin yƶnettiğiyle başlıyoruz: **[AraƧlar](../servers/tools.md)**. diff --git a/i18n/tr/pages/get-started/testing.md b/i18n/tr/pages/get-started/testing.md index a5ce1e0c..8b61d6f5 100644 --- a/i18n/tr/pages/get-started/testing.md +++ b/i18n/tr/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Test etme {#testing} -Python SDK, **bellek iƧi aktarıma** sahip bir `Client` sınıfıyla gelir: ona sunucu nesnenizi geƧirirsiniz, o da doğrudan bağlanır. +SDK'nın `Client` sınıfı, yani bir URL'ye bağlanan ya da bir alt süreƧ başlatan sınıfın ta kendisi, **bellek iƧinde** de bağlanır: ona sunucu nesnenizi geƧirin, o da sunucuyla doğrudan konuşur. -Alt süreƧ yok. Port yok. HiƧ aktarım yok. FastAPI'nin `TestClient`'ıyla aynı fikir. +Alt süreƧ yok. Port yok. Ağ üzerinden giden hiƧbir şey yok. FastAPI'nin `TestClient`'ıyla aynı fikir. ## Temel kullanım {#basic-usage} @@ -97,7 +97,7 @@ budur: testiniz temizlenmiş mesaj yerine gerƧek mesajı gƶrür. Testlerde aƧık bırakın. Üretim kodunda bir anlamı yoktur. -## Varsayılan olarak süreƧ iƧi {#in-process-by-default} +## Varsayılan olarak nesilden bağımsız {#era-neutral-by-default} !!! note `Client(mcp)` süreƧ iƧinde bağlanır ve varsayılan olarak **nesilden bağımsızdır**: sunucuyu diff --git a/i18n/tr/pages/handlers/multi-round-trip.md b/i18n/tr/pages/handlers/multi-round-trip.md index e4fe64c1..939a1006 100644 --- a/i18n/tr/pages/handlers/multi-round-trip.md +++ b/i18n/tr/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # Ƈok turlu istekler {#multi-round-trip-requests} @@ -164,7 +164,7 @@ Düşük seviyeli `Server` hiƧbir şeyin hazır gelmediği katmandır: `MCPServ ## 2026-07-28 sürümüne ƶzgü bir sonuƧ {#a-2026-07-28-result} -`InputRequiredResult` yalnızca **2026-07-28** protokol sürümünde vardır. Bellek iƧi `Client(server)` onu sizin yerinize anlaşarak belirler; ağ üzerinde `mode="auto"` keşfeder. Bağlandıktan sonra `client.protocol_version` size ne elde ettiğinizi sƶyler. +`InputRequiredResult` yalnızca **2026-07-28** protokol sürümünde vardır. `Client`'ın varsayılan `mode="auto"` ayarı onu her bağlantıda keşfeder. Bağlandıktan sonra `client.protocol_version` size ne elde ettiğinizi sƶyler. !!! warning 2026 ƶncesi bir oturumda `InputRequiredResult` koyacak bir yer yoktur. `mode="legacy"` bir diff --git a/i18n/tr/pages/handlers/progress.md b/i18n/tr/pages/handlers/progress.md index 373b6394..98d86bb9 100644 --- a/i18n/tr/pages/handlers/progress.md +++ b/i18n/tr/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # İlerleme {#progress} @@ -29,19 +29,17 @@ Bir **`Context`** parametresi alın ve `report_progress`'i Ƨağırın: İstemci, `call_tool`'a `progress_callback=` geƧirerek **Ƨağrı başına** dahil olur: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,28 +54,31 @@ anyio.run(main) Callback, sunucunun bildirdiklerini olduğu gibi alan `async` bir fonksiyondur: `progress`, `total`, `message`. !!! info - `Client(mcp)` doğrudan sunucu nesnesine, bellek iƧinde bağlanır; **[Test etme](../get-started/testing.md)** - sayfasının üzerine kurulduğu istemcinin aynısıdır. `Client` hangi aktarımı kullanırsa kullansın - `progress_callback` aynı parametredir; birazdan gƶreceğiniz *zamanlama* ise bellek iƧi bağlantıya - ƶzgüdür. Bu bağlantı callback'inizi satır iƧinde Ƨalıştırır, bu yüzden her bildirim `call_tool` - dƶnmeden ƶnce ulaşır. GerƧek bir aktarım üzerinde bildirimler sonuƧla yarışır ve yavaş bir callback, - `call_tool` dƶndükten sonra hĆ¢lĆ¢ Ƨalışıyor olabilir. + `Client`'a ne verirseniz verin `progress_callback` aynı parametredir: buradaki gibi bir URL, bir + `StdioServerParameters` ya da testteki sunucu nesnesi. Yine de gerƧek bir aktarım üzerinde + zamanlamaya dikkat edin. Her bildirim yanıtın yanında, kendi başına iletilir; bu yüzden yavaş bir + callback, `call_tool` dƶndükten sonra hĆ¢lĆ¢ Ƨalışıyor olabilir. Yalnızca süreƧ iƧi test bağlantısı + callback'i satır iƧinde Ƨalıştırır ve her bildirimin ƶnce ulaşmasını garanti eder. ### Deneyin {#try-it} -`client.py` dosyasını `server.py` dosyasının yanına koyun ve Ƨalıştırın: +`server.py` dosyasını HTTP üzerinden sunun, ardından istemciyi ikinci bir terminalden Ƨalıştırın: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Sunucudaki her `await ctx.report_progress(...)`, istemcide sırasıyla bir `show` Ƨağrısına dƶnüştü ve her iki satır da `call_tool` dƶnmeden **ƶnce** yazdırıldı. İlerleme sonucun iƧine paketlenmez; araƧ hĆ¢lĆ¢ Ƨalışırken akar. +Sunucudaki her `await ctx.report_progress(...)`, istemcide sırasıyla bir `show` Ƨağrısına dƶnüştü. İlerleme sonucun iƧine paketlenmez. AraƧ hĆ¢lĆ¢ Ƨalışırken akar. !!! warning `progress_callback` `Client`'a değil, **Ƨağrıya** aittir. Bunun iƧin bir kurucu argümanı yoktur, diff --git a/i18n/tr/pages/protocol-versions.md b/i18n/tr/pages/protocol-versions.md index b1fbd766..8705fc92 100644 --- a/i18n/tr/pages/protocol-versions.md +++ b/i18n/tr/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Protokol sürümleri {#protocol-versions} @@ -11,9 +11,17 @@ MCP'nin iki nesli var. Bununla neredeyse hiƧ ilgilenmeniz gerekmez, çünkü anlaşmayı sizin yerinize `Client` yapar. Bu sayfa, bunu denetleyen tek yapıcı argümanı, yani `mode=` parametresini ve onu değiştireceğiniz üç durumu anlatır. +Bu sayfadaki her kod parƧası, **[İstemci](client/index.md)** sayfasındaki Bookshop `server.py` sunucusuyla konuşan bir `client.py` dosyasıdır. O sunucuyu bir terminalde başlatın: + +```console +uv run mcp run server.py --transport streamable-http +``` + +Ardından her kod parƧasını ikinci bir terminalde `python client.py` ile Ƨalıştırın. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ Her iki durumda da bağlanmış olarak Ƨıkarsınız ve hangisinin gerƧekleşt Ɩzelliğin tamamı bu. Tek bir `Client`, her nesilden sunucu, kodunuzda dallanma yok. !!! info - `MCPServer`, `server/discover` isteğini her aktarımda yanıtlar (bellek iƧi, stdio, Streamable - HTTP); bu yüzden kendi sunucunuza karşı `auto` her zaman `2026-07-28`'e ulaşır. Geri dƶnüş - yalnızca gerƧek bir 2026 ƶncesi sunucuya karşı devreye girer, ki tam da o zaman bunu istersiniz. + `MCPServer`, `server/discover` isteğini her aktarımda yanıtlar (Streamable HTTP, stdio ve + testlerinizin kullandığı süreƧ iƧi bağlantı); bu yüzden kendi sunucunuza karşı `auto` her zaman + `2026-07-28`'e ulaşır. Geri dƶnüş yalnızca gerƧek bir 2026 ƶncesi sunucuya karşı devreye girer, + ki tam da o zaman bunu istersiniz. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ Sunucunun başlattığı bir istek, sunucunun *sizi* Ƨağırmasıdır: `ctx.eli `mode`, modern bir protokol sürümü dizgesini de kabul eder. Bugün bu küme tam olarak `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ Sorgu ucuzdur, ancak yine de her yeniden bağlanmada ƶdediğiniz bir turdur ve Ɩyleyse saklayın. Bir `auto` bağlantısından sonra `client.session.discover_result`, sunucunun gƶnderdiği `DiscoverResult`'ı olduğu gibi tutar: `supported_versions`, `capabilities`, `instructions` ve sunucunun sonucun `_meta` alanına işlediği kimlik. Bir sonraki sefer bunu `prior_discover=` olarak geri verin: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/tr/pages/run/asgi.md b/i18n/tr/pages/run/asgi.md index 7ed6be8d..4a3676c5 100644 --- a/i18n/tr/pages/run/asgi.md +++ b/i18n/tr/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # Mevcut bir uygulamaya ekleme {#add-to-an-existing-app} @@ -100,7 +100,7 @@ Sondaki o `/mcp`, `streamable_http_path` değeridir. Bunu `"/"` yapın, mount ƶ --8<-- "docs_src/asgi/tutorial004.py" ``` -Artık istemciler `/notes/mcp` yoluna değil `/notes` yoluna bağlanır. +Artık istemciler `/notes/mcp` yoluna değil `/notes/` yoluna bağlanır. ## Tarayıcı istemcileri iƧin CORS {#cors-for-browser-clients} @@ -143,4 +143,4 @@ Tarayıcı tabanlı bir istemcinin sizden iki izne ihtiyacı vardır: MCP istek * Tarayıcı istemcilerinin CORS'a ihtiyacı vardır: `Mcp-*` istek başlıkları iƧin `allow_headers`, yanıt iƧin `expose_headers=["Mcp-Session-Id"]`. * `@mcp.custom_route()`, `/mcp`'nin yanına düz, kimlik doğrulaması olmayan HTTP endpoint'leri ekler. -Sunucu gerƧek bir URL'den erişilebilir olduğunda **[İstemci](../client/index.md)** ona bir sunucu nesnesi yerine o URL ile bağlanır. +Sunucu gerƧek bir URL'den erişilebilir olduğunda **[İstemci](../client/index.md)** ona o URL ile bağlanır. diff --git a/i18n/tr/pages/run/authorization.md b/i18n/tr/pages/run/authorization.md index c04c1eef..484d7b42 100644 --- a/i18n/tr/pages/run/authorization.md +++ b/i18n/tr/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # Yetkilendirme {#authorization} @@ -23,12 +23,12 @@ Bu sayfa sunucu tarafını anlatır. Yetkilendirme sunucunuzu keşfeden ve token SDK'nın geƧerli bir token'ın neye benzediği konusunda bir fikri yoktur. Bunu **`TokenVerifier`**'ı uygulayarak siz sƶylersiniz: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` tek bir asenkron metodu olan bir protokoldür. `verify_token`, `Authorization` başlığındaki ham token'ı alır; geƧerliyse bir **`AccessToken`**, değilse `None` dƶndürür. Uygulanacak başka bir şey yok. -* Buradaki, token'ı bir tabloda arar. GerƧek bir doğrulayıcı JWT imzasını doğrular ya da yetkilendirme sunucusunun token-introspection endpoint'ini Ƨağırır. O kod sizindir; SDK onu yalnızca Ƨağırır. +* Buradaki, token'ı bir tabloda arar; her girdi, token'ın hangi kaynak iƧin verildiğini kaydeder. GerƧek bir doğrulayıcı JWT imzasını doğrular ya da yetkilendirme sunucusunun token-introspection endpoint'ini Ƨağırır ve token'ın kimin iƧin verildiğini (`aud` değerini) `AccessToken.resource` iƧinde bildirir. O kod sizindir; SDK onu yalnızca Ƨağırır. * `token_verifier=` ve `auth=` her zaman birlikte kullanılır. Birini diğeri olmadan geƧirirseniz `MCPServer(...)` daha tek bir istek sunmadan `ValueError` fırlatır. `AuthSettings`, kaynak sunucunuzun dışa dƶnük yüzüdür: @@ -36,6 +36,10 @@ SDK'nın geƧerli bir token'ın neye benzediği konusunda bir fikri yoktur. Bunu * `issuer_url`: token'larınızı veren yetkilendirme sunucusu. * `resource_server_url`: bu MCP endpoint'inin herkese aƧık URL'si. Bir token'ın *hangi* kaynak iƧin olduğunu belirtir ve keşif belgesi burada bulunur. * `required_scopes`: her token bunların hepsini taşımalıdır. +* `validate_token_resource`: `AccessToken.resource` değeri `resource_server_url` olmayan her token'ı reddeder. `resource_server_url` ayarlıyken bunu ayarlamadan bırakmak uyarı verir (`MCPDeprecationWarning`) ve `False` gibi davranır; 3.0 sürümü kaynak sunucuları iƧin varsayılanı `True` yapar. + * Yetkilendirme sunucunuz token'ları istemcinin istediği `resource` değerine bağlıyorsa aƧın; MCP istemcileri bu değeri her zaman gƶnderir. `resource_server_url` değerini istemcilerin bağlandığı URL'nin birebir aynısı olarak tutun. + * Yetkilendirme sunucunuz kendi audience tanımlayıcılarını kullanıyorsa (bir Auth0 API tanımlayıcısı, bir Entra uygulama kimliği) kapalı bırakın ve bunun yerine `aud` değerini doğrulayıcınızda denetleyin; bu sunucu iƧin olmayan bir token'da `None` dƶndürün. + * `aud` bir listeyse, `resource_server_url` ile eşit olan girdiyi `resource` alanına koyun. !!! tip SDK deposundaki `examples/servers/simple-auth/` dizininde, gerƧek bir yetkilendirme sunucusunun @@ -91,7 +95,7 @@ Sunucunuzu hiƧ duymamış bir istemci iƧeri giden yolu bu belgeyle bulur: `aut Herhangi bir işleyicinin iƧinde **`get_access_token()`**, doğrulayıcınızın geƧerli istek iƧin dƶndürdüğü `AccessToken`'dır: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -125,6 +129,6 @@ Bir yetkilendirme sunucusu, kullanıcının onay ekranından tıklayarak geƧmes * `token_verifier=` ve `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` her zaman birlikte kullanılır. * SDK, [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata belgesini `/.well-known/oauth-protected-resource/...` altında yayımlar ve kimliği doğrulanmamış istekleri, `WWW-Authenticate` başlığı ona işaret eden bir 401 ile yanıtlar. Keşif hikĆ¢yesinin tamamı bu. * Herhangi bir işleyicide `get_access_token()`, kimin Ƨağırdığını sƶyler. -* Yetkilendirme bir HTTP meselesidir. `stdio` ve bellek iƧi istemci onu hiƧ gƶrmez. +* Yetkilendirme bir HTTP meselesidir. `stdio` ve bellek iƧi test istemcisi onu hiƧ gƶrmez. İstemci yarısı (yetkilendirme sunucunuzu keşfetme ve token'ı sizin yerinize alma) **[OAuth istemcileri](../client/oauth-clients.md)** sayfasında. Kullanıcıdan kimlik istemek yerine bir kimliği *beyan eden* istemci ise **[Kimlik beyanı](../client/identity-assertion.md)** sayfasında. diff --git a/i18n/tr/pages/run/deploy.md b/i18n/tr/pages/run/deploy.md index 3155935d..29a6eaf4 100644 --- a/i18n/tr/pages/run/deploy.md +++ b/i18n/tr/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Dağıtım ve ƶlƧekleme {#deploy-scale} @@ -47,6 +47,22 @@ GerƧek bir ana bilgisayar adının arkasına dağıtıldığında, aynı varsay her bağlantıyı reddeden bir sunucu, aksi kanıtlanana kadar bir Host izin listesi sorunudur. **[Sorun giderme](../troubleshooting.md)** de buradan başlar. +## TLS sonlandıran bir vekil sunucunun arkasında {#behind-a-tls-terminating-proxy} + +TLS bir vekil sunucuda (bir ingress, bir yük dengeleyici, Caddy, nginx) sona eriyor ve uvicorn onun arkasında düz HTTP sunuyorsa, uvicorn'a vekil sunucunun `X-Forwarded-*` başlıklarına güvenmesini sƶyleyin: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Bu olmadan uygulama `http://` üzerinden sunulduğunu sanır ve verdiği her yƶnlendirme (en yaygını `/mcp` → `/mcp/`) `http://…` adresini gƶsterir. Python istemcisi bir HTTPS endpoint'inden düz HTTP'ye giden bir yƶnlendirmeyi izlemeyi reddeder ve bunu aƧıkƧa sƶyler: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +İstemci tarafındaki geƧici Ƨƶzüm, sunucunun sunduğu tam URL'yi (`https://mcp.example.com/mcp/`, sondaki eğik Ƨizgi dahil) yapılandırmaktır; bƶylece hiƧ yƶnlendirme olmaz. Asıl Ƨƶzüm yukarıdaki bayraktır. `FORWARDED_ALLOW_IPS` aynı ayarın ortam değişkeni yazımıdır; `*` her atlama noktasına güvenir, bu da yalnızca uvicorn'a vekil sunucudan başka hiƧbir şey ulaşamıyorsa doğrudur. + ## Worker'lar ve kimin yapışkan olması gerektiği {#workers-and-who-has-to-be-sticky} Ana bilgisayar adı yanıt vermeye başladıktan sonra, arkasına birden fazla worker koyun. Bunun iƧin SDK'da bir ayar yoktur; bir Starlette uygulamasını, herhangi bir ASGI uygulamasını ƶlƧeklediğiniz gibi ƶlƧeklersiniz: nesneyi, fork etmeyi bilen bir şeye verirsiniz: @@ -171,6 +187,7 @@ Bir `MCPServer` bir uygulama sunucusu değil, bir protokol gerƧeklemesidir. Bun ## Ɩzet {#recap} * Varsayılan olarak uygulama yalnızca localhost'a gƶnderilen istekleri yanıtlar. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` yayına Ƨıkış kapısıdır: onu geƧirene kadar gerƧek bir ana bilgisayar adının arkasındaki her istek bir `421`'dir ve nedeni yalnızca sunucunun log'undadır. +* TLS sonlandıran bir vekil sunucunun arkasında uvicorn'u `--proxy-headers --forwarded-allow-ips=...` ile Ƨalıştırın; yoksa yƶnlendirmeleri `http://` adresini gƶsterir ve istemci onları reddeder. * 2026-07-28'de oturum yoktur ve bir yük dengeleyicinin yapışacağı hiƧbir şey yoktur. `stateless_http=True` yalnızca eski nesle ait bir ayardır, çünkü modern bir istek o bayrak hiƧ okunmadan yƶnlendirilir ve yanıtlanır. * Varsayılan `requestState` anahtarı, süreƧ başına basılan `os.urandom(32)`'dir. Farklı bir worker'a ulaşan Ƨok turlu bir yeniden deneme `-32602` *"Invalid or expired requestState"* ile başarısız olur. * Ƈƶzüm `RequestStateSecurity(keys=[...])` **ve** her ƶrnekte aynı sunucu adıdır. Ad, token'ın varsayılan audience claim'idir. Aynı anahtarlar, aynı ad. diff --git a/i18n/tr/pages/run/index.md b/i18n/tr/pages/run/index.md index b4c20621..fe5c4147 100644 --- a/i18n/tr/pages/run/index.md +++ b/i18n/tr/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Sunucunuzu Ƨalıştırma {#running-your-server} @@ -75,6 +75,11 @@ Her aktarımın kendi anahtar sƶzcük argümanları vardır ve hepsi `run()` ü * `max_request_body_size`: bayt cinsinden kabul edilen en büyük istek gƶvdesi. Varsayılan olarak 4 MiB; daha büyük istekler, ayrıştırma veya oturum oluşturma ƶncesinde HTTP 413 alır. Bunu yalnızca meşru MCP mesajları bu boyutu aştığında yükseltin. +* `session_idle_timeout`: eski nesil bir oturumun, sunucu onu kapatmadan ƶnce sürmekte olan hiƧbir şey + olmadan bekleyebileceği saniye sayısı. Varsayılan 1800. `None` bunu devre dışı bırakır. Bkz. + [Oturum ƶmrü ve sınırlar](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: bir sürecin aynı anda kaƧ eski nesil oturum tuttuğu. Varsayılan 10 000. `None` + sınırı kaldırır. Aynı bƶlümde ele alınır. * `event_store`, `retry_interval`, `transport_security`: kaldığı yerden devam edebilme ve DNS rebinding koruması. localhost dışında bir yere dağıtım yapana kadar bekleyebilirler; `transport_security` konusunu **[Dağıtım ve ƶlƧekleme](deploy.md)** ele alır. !!! warning diff --git a/i18n/tr/pages/run/legacy-clients.md b/i18n/tr/pages/run/legacy-clients.md index da62841d..055a9930 100644 --- a/i18n/tr/pages/run/legacy-clients.md +++ b/i18n/tr/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # Eski nesil istemcilere hizmet verme {#serving-legacy-clients} @@ -22,15 +22,25 @@ Yani eski nesil istemci, *ona gƶre* bir şey inşa ettiğiniz bir hedef değil. ## Tek işleyici, iki nesil {#one-handler-both-eras} -İşte kullanıcıya bir şey sorması gereken bir araƧ ve onu Ƨağıran her iki nesilden istemci: +İşte kullanıcıya bir şey sorması gereken bir araƧ: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve`, modelin sağlamadığı tek bir şeye ihtiyaƧ duyar: kaƧ kopya. Bir araƧ bunu `Annotated[..., Resolve(ask_quantity)]` ile bildirir (ayrıntıların tamamı **[Bağımlılıklar](../handlers/dependencies.md)** sayfasında). `reserve` iƧinde hiƧbir şey bir sürüm adı vermez, bir yetenek kontrol etmez ya da dallanmaz. -İki istemci **aynı anda**, aynı `mcp` nesnesi üzerinde aƧıktır. `mode="legacy"`, `initialize` el sıkışmasını Ƨalıştırır: 2026 ƶncesi bir istemcinin aƧtığı bağlantının ta kendisi. Diğeri varsayılanı alır ve `2026-07-28` sürümünde karar kılar. +Onu HTTP üzerinden sunun. İşte onu Ƨağıran her iki nesilden istemci: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +İki istemci **aynı anda**, Ƨalışan aynı sunucuya karşı aƧıktır. `mode="legacy"`, `initialize` el sıkışmasını Ƨalıştırır: 2026 ƶncesi bir istemcinin aƧtığı bağlantının ta kendisi. Diğeri varsayılanı alır ve `2026-07-28` sürümünde karar kılar. İkinci bir terminalden `python client.py` komutunu Ƨalıştırın: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -64,6 +74,31 @@ Tek worker'da bu gƶrünmez. İki worker'da ise sorunun tamamı budur: `Mcp-Sess **devam ettirilebilirliktir** (kaƧırılan SSE olaylarını *aynı* oturuma yeniden bağlanan bir istemciye yeniden oynatmak). Bir oturumu asla başka bir süreƧten erişilebilir kılmaz. +## Oturum ƶmrü ve sınırlar {#session-lifetime-and-limits} + +Eski nesil bir oturum sonsuza dek yaşamaz ve tek bir süreƧ bunlardan sınırsız sayıda tutmaz. Bunu iki ayar denetler. İkisi de `run()`, `streamable_http_app()` ve `Server.streamable_http_app()` üzerinde birer anahtar sƶzcük argümanıdır. Modern (`2026-07-28`) bağlantılarda ve `stateless_http=True` durumunda oturum yoktur; bu yüzden iki ayar da onlar iƧin geƧerli değildir. + +| Ayar | Varsayılan | Ne yapar | İstemci ne gƶrür | Kapatmak iƧin | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 dk) | Bu süre boyunca devam eden hiƧbir işlemi olmayan bir oturumu kapatır. | `404 Session not found`. Yeniden `initialize` gƶndermesi gerekir. | `None` | +| `max_sessions` | `10_000` | Bu sayının ƶtesinde oturum aƧmayı reddeder. Mevcut oturumlara dokunulmaz ve hiƧbir şey tahliye edilmez. | JSON-RPC kodu `-32603` ile `503 Too many open sessions`. | `None` | + +Neler "devam eden işlem" sayılır: + +* AƧık bir `GET` akışı. SDK istemcileri bir tane aƧık tutar; bu yüzden bağlı bir istemcinin oturumu asla sona ermez. +* HĆ¢lĆ¢ yanıtlanmakta olan bir istek. Zaman aşımından uzun süren bir araƧ Ƨağrısı kesilmez; geri sayım ancak o bittiğinde başlar. +* Başka hiƧbir şey. İstekler arasında saat işler. Oturumdaki herhangi bir istek onu yeniden başlatır, `ping` dahil. Bir oturum sona erdikten sonra onu hiƧbir şey diriltmez. + +Oturumunu `DELETE` ile bitiren bir istemci onu hemen serbest bırakır. AƧılış isteği reddedilen bir istemci de ƶyle. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +İki olay da sunucu log'unda gƶrünür. Sona erme, `INFO` düzeyinde `Session idle timeout` olarak gƶrünür. Reddedilen bir aƧılış ise `WARNING` düzeyinde `Refusing to open a new session: sessions are already open` olarak. + +Sınırlar süreƧ başınadır. Dƶrt worker'la tavan, `max_sessions` değerinin dƶrt katıdır ve her worker kendi oturumlarını kendisi sonlandırır. + ## Tek ayar düğmesi: `stateless_http` {#the-one-knob-stateless_http} Yapışkanlık ƶdemeyi reddettiğiniz bir bedelse, değiştirebileceğiniz tam olarak tek bir şey var. @@ -89,8 +124,7 @@ Onunla ilgili iki şey, ne yaptığından daha ƶnemli. !!! check Yanlış olanı yapın. `reserve`, az ƶnce iki istemciye de hizmet veren aracın ta kendisi. Onu - `stateless_http=True` ile dağıtın, aynı iki istemciyi HTTP üzerinden bağlayın ve her birinden - Ƨağırın. + `stateless_http=True` ile dağıtın, aynı iki istemciyi bağlayın ve her birinden Ƨağırın. Modern istemci hĆ¢lĆ¢ `Reserved 2 of 'Dune'.` alır. Modern kol değişmedi. diff --git a/i18n/tr/pages/troubleshooting.md b/i18n/tr/pages/troubleshooting.md index 3362b530..5933595a 100644 --- a/i18n/tr/pages/troubleshooting.md +++ b/i18n/tr/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Sorun giderme {#troubleshooting} @@ -13,6 +13,12 @@ Girdilerin birkaƧı şu tek sunucuya karşı Ƨalışır. Bir araƧ ve bir şab --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Bu girdiler ona `http://localhost:8000/mcp` adresinden ulaşır; bu yüzden onu HTTP üzerinden Ƨalışır durumda bırakın: + +```console +uv run mcp run server.py --transport streamable-http +``` + Bu sayfanın alıntıladığı hatalar gerƧektir: SDK'nın kendi test paketi her birini yeniden üretir. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ Bu bir MCP hatası değil. anyio gürültüsüdür ve asıl hatanız yapıştır ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ Bununla yapılacak iki şey var: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ Bunun sayfası, tek uygulamada birden fazla sunucu ve FastAPI dahil, **[Mevcut b ## `MCPError: Session not found` {#mcperror-session-not-found} -Sunucu, istemcinizin gƶnderdiği `Mcp-Session-Id`'yi tanımıyor; bunun nedeni neredeyse her zaman sunucunun **yeniden başlamış** olmasıdır (ya da farklı bir ƶrneğe yƶnlendirilmişsinizdir). Oturumlar o tek sürecin belleğinde yaşar. +Sunucu, istemcinizin gƶnderdiği `Mcp-Session-Id`'yi tanımıyor. Ya sunucu **yeniden başladı** (ya da farklı bir ƶrneğe yƶnlendirildiniz) ya da `session_idle_timeout` süresi boyunca (varsayılan olarak 30 dakika) hiƧbir şey işlemde olmadığı iƧin oturumun **süresi doldu**. [Oturum ƶmrü ve sınırları](run/legacy-clients.md#session-lifetime-and-limits) bƶlümüne bakın. Oturumlar o tek sürecin belleğinde yaşar. Bulunacak bir sunucu hatası yok. HTTP yanıtı, gƶvdesi JSON-RPC *olan* bir `404`'tür; bu yüzden yukarıdaki `421`'in aksine python `Client` bunu size birebir gƶsterir: @@ -262,9 +268,9 @@ Bulunacak bir sunucu hatası yok. HTTP yanıtı, gƶvdesi JSON-RPC *olan* bir `4 Ƈƶzüm yeniden bağlanmaktır: `async with Client(...)` bloğundan Ƨıkın ve yeni bir oturum üzerinde anlaşan yeni bir bloğa girin. Uzun ƶmürlü bir istemci iƧin bu, Ƨağrılarınızın etrafında `MCPError`'ı yakalamak ve ƶlü bir oturumun iƧinde yeniden denemek yerine bu mesajda yeniden bağlanmak demektir. -Bu, yeniden başlatma *olmadan* oluyorsa, yapışkan oturumlar olmadan birden fazla worker Ƨalıştırıyorsunuz demektir: her worker kendi oturum tablosunu tutar, bu yüzden yanlış olana yƶnlendirilen bir istek buraya düşer. Bu konu ve iki Ƨƶzümü (yapışkan yƶnlendirme veya `stateless_http=True`) **[Dağıtım ve ƶlƧekleme](run/deploy.md)** ile **[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** sayfalarında. +Bu, yeniden başlatma *olmadan* ve istemci o kadar uzun süre sessiz kalmadan oluyorsa, yapışkan oturumlar olmadan birden fazla worker Ƨalıştırıyorsunuz demektir: her worker kendi oturum tablosunu tutar, bu yüzden yanlış olana yƶnlendirilen bir istek buraya düşer. Bu konu ve iki Ƨƶzümü (yapışkan yƶnlendirme veya `stateless_http=True`) **[Dağıtım ve ƶlƧekleme](run/deploy.md)** ile **[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** sayfalarında. -Sunucu operatƶrü iƧin eşleşen log satırı `Rejected request with unknown or expired session ID: `'dir. `INFO` düzeyinde log'a yazılır; bu yüzden olağan `WARNING` eşiğinde gƶrünmez. Bir dağıtımın hemen ardından bunu ƶbekler halinde gƶrmek normaldir; bağlı her istemci yeniden bağlanıyordur. +Sunucu operatƶrü iƧin eşleşen log satırı `Rejected request with unknown or expired session ID: `'dir. `INFO` düzeyinde log'a yazılır; bu yüzden olağan `WARNING` eşiğinde gƶrünmez. Bir dağıtımın hemen ardından bunu ƶbekler halinde gƶrmek normaldir; bağlı her istemci yeniden bağlanıyordur. Oturumun süresi dolduğunda ise bu satırdan ƶnce, yine `INFO` düzeyinde, `Session idle timeout` gelir. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ Modern protokolün kaldırdığı bir istek olmasına rağmen bu hatayı **üret Sunucunuz kullanıcıya bir şey sormak istiyor ve bu istemci kendisine soru sorulabileceğini hiƧ sƶylemedi. -Bir elicitation (kullanıcıdan bilgi isteme) Ƨƶzümleyicisi, bağlı istemci form elicitation'ı bildirmediğinde baştan reddeder ve `e.error.data` tam olarak neyin eksik olduğunu adlandırır: +Bu Bistro, rezervasyon yapmadan ƶnce bir Ƨƶzümleyici aracılığıyla sorar: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Bunu Weather sunucusunun yerine sunun ve `book_table`'ı hiƧbir `elicitation_callback` geƧirmemiş bir istemciden Ƨağırın. Ƈƶzümleyici baştan reddeder, çünkü bağlı istemci form elicitation'ı (kullanıcıdan bilgi isteme) hiƧ bildirmemiştir; `e.error.data` ise tam olarak neyin eksik olduğunu adlandırır: ```json { @@ -290,7 +302,7 @@ Bir elicitation (kullanıcıdan bilgi isteme) Ƨƶzümleyicisi, bağlı istemci ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ Bunu eski nesil bir bağlantıda `ctx.elicit()`'ten gƶrürsünüz; herhangi bir İşleyiciniz, isteğin ortasında istemciye ulaşmaya Ƨalıştı; hem de Ƨağrısının sunucudan gelen bir isteği taşıyabilecek hiƧbir kanalı olmayan bir bağlantıda. Bir Ƨağrıyı bu duruma sokan üç sunucu yapılandırması var. -**Bir `2026-07-28` bağlantısı: her aktarımda, her zaman.** Modern protokolde sunucunun başlattığı istek diye bir şey hiƧ yoktur; bu yüzden sunucu daha hiƧbir şey gƶnderilmeden reddeder. Bununla karşılaşmanın klasik yolu bir aracın iƧindeki `ctx.elicit()`'tir (hem de daha ilk bellek iƧi testte, çünkü `Client(server)` sorulmadan `2026-07-28` üzerinde anlaşır) ve `elicitation_callback=` geƧirmek hiƧbir şeyi değiştirmez, çünkü istemciye yanıtlayacağı bir istek hiƧ ulaşmaz: +**Bir `2026-07-28` bağlantısı: her aktarımda, her zaman.** Modern protokolde sunucunun başlattığı istek diye bir şey hiƧ yoktur; bu yüzden sunucu daha hiƧbir şey gƶnderilmeden reddeder. Bununla karşılaşmanın klasik yolu bir aracın iƧindeki `ctx.elicit()`'tir; genellikle de o aracın daha ilk bellek iƧi **[testinde](get-started/testing.md)**, çünkü `Client(mcp)` sorulmadan `2026-07-28` üzerinde anlaşır. `elicitation_callback=` geƧirmek hiƧbir şeyi değiştirmez, çünkü istemciye yanıtlayacağı bir istek hiƧ ulaşmaz: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ Sunucu, istemcinizin geri yansıttığı `requestState` token'ını doğrulayama ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -417,7 +429,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * Sunucu log'undaki `Tool already exists:`, aynı adlı iki aracın teke indiğinin tek işaretidir. * Tek 421, üç yazım: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (geri kalan her şey), `Invalid Host header: ` (sunucu log'u). Ƈƶzüm: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> ana uygulamanın lifespan'i `mcp.session_manager.run()`'a hiƧ girmemiş, bağlanmış bir uygulama. -* `Session not found` -> sunucu yeniden başladı; yeniden bağlanın. +* `Session not found` -> sunucu yeniden başladı ya da oturumun süresi doldu (`session_idle_timeout`); yeniden bağlanın. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` sunucudan istemciye bir kanala ihtiyaƧ duyar: bir `2026-07-28` bağlantısında hiƧ yoktur, `stateless_http=True` eski nesil olanı, `json_response=True` ise istek kapsamlı olanı ortadan kaldırır. Bir Ƨƶzümleyici kullanın (eski nesil bir istemci iƧin ayrıca kanalı koruyan bir sunucu gerekir). Komşusu `Method not found`, karşı tarafın protokol sürümünde olmayan bir yƶntem iƧin yapılmış bir istektir. * `Client did not declare the form elicitation capability ...` ve `Elicitation not supported` -> istemcide `elicitation_callback=` eksik. * `Invalid or expired requestState` nedenini ağ üzerinde asla sƶylemez. Sunucu log'u sƶyler; `unknown key`, `RequestStateSecurity(keys=[...])`'i worker'lar arasında paylaşın demektir. diff --git a/i18n/tr/pages/whats-new.md b/i18n/tr/pages/whats-new.md index 6380cf36..04297386 100644 --- a/i18n/tr/pages/whats-new.md +++ b/i18n/tr/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2'deki yenilikler {#whats-new-in-v2} @@ -42,11 +42,11 @@ Bir aracın ihtiyaƧ duyduğu her şey modelden gelmek zorunda değil. v2 ile ge v1 size iƧ iƧe üç katman veriyordu: ham akışlar üreten bir aktarım bağlam yƶneticisi, bunların etrafına sarılmış bir `ClientSession` ve elle Ƨağrılan bir `await session.initialize()`. v2'de tek bir nesne var: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` bir sunucu nesnesi (bellek iƧi, aktarım yok: test senaryosu), bir URL (Streamable HTTP), bir `StdioServerParameters` (bir stdio alt süreci) ya da `sse_client(...)` gibi başka herhangi bir aktarım bağlam yƶneticisi alır. `async with` bloğuna girmek bağlantıyı kurar ve sunucu hangi nesli konuşuyorsa ona gƶre protokol sürümünde anlaşır; ardından `client.server_capabilities` ve `client.protocol_version` hazırdır, sunucu kendini tanıttığında `client.server_info` da ƶyle (artık `Implementation | None` türünde, çünkü 2026 neslinde kimlik isteğe bağlı). v1'de kaydettiğiniz ƶrnekleme ve elicitation callback'leri hĆ¢lĆ¢ Ƨalışır (gƶvdeleri, bu sayfadaki her şey gibi aynı snake_case ƶznitelik yeniden adlandırmasını gƶrür); artık 2026 tarzı sonuƧ-iƧinde-isteklere de (aşağıda) yanıt verirler ve teker teker değil eşzamanlı Ƨalışırlar. Düşük düzey yüzeyi isteyenler iƧin `ClientSession` hĆ¢lĆ¢ altta duruyor ve `client.session` onu size verir; o da taşındı (yeni dispatcher motoru üzerinde Ƨalışır ve kendi imzalarından bazıları değişti), bu yüzden aşağı inmeden ƶnce **[GeƧiş kılavuzu](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** sayfasını okuyun. +`Client` bir URL (Streamable HTTP), bir `StdioServerParameters` (bir stdio alt süreci), `sse_client(...)` gibi başka herhangi bir aktarım bağlam yƶneticisi ya da testlerde sunucu nesnesinin kendisini (bellek iƧi, aktarım yok) alır. `async with` bloğuna girmek bağlantıyı kurar ve sunucu hangi nesli konuşuyorsa ona gƶre protokol sürümünde anlaşır; ardından `client.server_capabilities` ve `client.protocol_version` hazırdır, sunucu kendini tanıttığında `client.server_info` da ƶyle (artık `Implementation | None` türünde, çünkü 2026 neslinde kimlik isteğe bağlı). v1'de kaydettiğiniz ƶrnekleme ve elicitation callback'leri hĆ¢lĆ¢ Ƨalışır (gƶvdeleri, bu sayfadaki her şey gibi aynı snake_case ƶznitelik yeniden adlandırmasını gƶrür); artık 2026 tarzı sonuƧ-iƧinde-isteklere de (aşağıda) yanıt verirler ve teker teker değil eşzamanlı Ƨalışırlar. Düşük düzey yüzeyi isteyenler iƧin `ClientSession` hĆ¢lĆ¢ altta duruyor ve `client.session` onu size verir; o da taşındı (yeni dispatcher motoru üzerinde Ƨalışır ve kendi imzalarından bazıları değişti), bu yüzden aşağı inmeden ƶnce **[GeƧiş kılavuzu](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** sayfasını okuyun. **[Client](client/index.md)** sayfası onu tanıtır, **[İstemci aktarımları](client/transports.md)** dƶrt bağlantı biƧimini anlatır, **[İstemci callback'leri](client/callbacks.md)** callback'lerin kendisini ele alır ve **[Test etme](get-started/testing.md)** v1'in `create_connected_server_and_client_session()` yardımcısının yerini alan bellek iƧi kalıbı gƶsterir. @@ -171,11 +171,15 @@ Bunun istemci tarafı **[Protokol sürümleri](protocol-versions.md)** sayfasın Yerine gelen Ƨƶzüm Ƨağrıyı tersine Ƨevirir. Kullanıcıdan bir şeye ihtiyaƧ duyan araƧ soruyu *dƶndürür* (`InputRequiredResult`), istemci onu her zamanki callback'leriyle yanıtlar ve Ƨağrı yanıtlar eklenmiş hĆ¢lde yeniden denenir. Bu dƶngüyü sizin iƧin `Client` yürütür. Sunucuda sonucu nadiren kendiniz kurarsınız, çünkü bunu bir **[bağımlılık](handlers/dependencies.md)** yapar: bir parametreyi `Resolve(ask_quantity)` ile işaretleyin (`ask_quantity` sizin yazdığınız sıradan bir fonksiyondur), SDK de bağlantının desteklediği mekanizma hangisiyse onun üzerinden sorar: eski nesil bir oturumda canlı bir elicitation isteği, 2026'da Ƨok turlu bir istek. Tek araƧ gƶvdesi, iki nesil birden: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Bu dosya tüm vaadin tek yerde ƶzeti: bir sunucu, `Resolve` destekli bir araƧ ve ikisi de yanıtını bellek iƧinde alan bir eski nesil istemci ile bir modern istemci. **[Ƈok turlu istekler](handlers/multi-round-trip.md)** mekanizmayı aƧıklar (SDK'nın sizin iƧin mühürleyip doğruladığı `request_state` dĆ¢hil); sorma kısmı **[Elicitation](handlers/elicitation.md)** sayfasında. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Bu iki dosya tüm vaadin ƶzeti: bir sunucu, `Resolve` destekli bir araƧ ve ikisi de yanıtını Ƨalışan aynı sunucudan alan bir eski nesil istemci ile bir modern istemci (**[Eski nesil istemcilere hizmet verme](run/legacy-clients.md)** bunları adım adım anlatır). **[Ƈok turlu istekler](handlers/multi-round-trip.md)** mekanizmayı aƧıklar (SDK'nın sizin iƧin mühürleyip doğruladığı `request_state` dĆ¢hil); sorma kısmı **[Elicitation](handlers/elicitation.md)** sayfasında. !!! warning "Taşınmış bir v1 sunucusunun davranış değiştirdiği tek yer burası" Buna ilk sizin testleriniz Ƨarpar: `Client(mcp)` v2 sunucunuzla varsayılan olarak 2026-07-28 diff --git a/i18n/uk/pages/advanced/apps.md b/i18n/uk/pages/advanced/apps.md index 8929b7ae..76f5f941 100644 --- a/i18n/uk/pages/advanced/apps.md +++ b/i18n/uk/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,13 +18,13 @@ SDK постачає це ŃŠŗ вбуГоване Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń `Apps` (` ## ГоГинник Ń–Š· циферблатом {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` Чотири кроки: -* `Apps()`: оГин ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ тримає Š²Š°ŃˆŃ– Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š· прив'ŃŠ·Š°Š½ŠøŠ¼ UI та їхні Ń€ŠµŃŃƒŃ€ŃŠø. +* `Apps()`: оГин ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ тримає Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š· прив'ŃŠ·Š°Š½ŠøŠ¼ UI та їхні Ń€ŠµŃŃƒŃ€ŃŠø. * `@apps.tool(resource_uri="ui://clock/app.html")`: звичайний Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ ŠæŠ»ŃŽŃ позначка `_meta.ui.resourceUri`. Усе, що приймає `@mcp.tool()` (name, title, description, ...), ŠæŠµŃ€ŠµŠ“Š°Ń”Ń‚ŃŒŃŃ Галі. * `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)`: віГповіГний Ń€ŠµŃŃƒŃ€Ń, ŃŠŗŠøŠ¹ Š²Ń–Š“Š“Š°Ń”Ń‚ŃŒŃŃ ŃŠŗ `text/html;profile=mcp-app`. Даме цей MIME-тип каже Ń…Š¾ŃŃ‚Ńƒ: «це Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, віГобрази його». * `MCPServer("clock", extensions=[apps])`: ŃƒŠ²Ń–Š¼ŠŗŠ½ŠµŠ½Š½Ń. Тепер сервер Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” `io.modelcontextprotocol/ui` у `capabilities.extensions`. @@ -39,11 +39,31 @@ SDK постачає це ŃŠŗ вбуГоване Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń `Apps` (` МоГель читає `content`; iframe — Š“Š»Ń Š»ŃŽŠ“ŠµŠ¹. Єост Ń–Š· ŠæŃ–Š“Ń‚Ń€ŠøŠ¼ŠŗŠ¾ŃŽ UI все оГно переГає текстовий Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ моГелі, а ŃŃƒŃ‚Š¾ текстовий клієнт Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” *лише* його. Тож канонічний шаблон — оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, Гві віГповіГі. ŠŸŠ¾Š³Š»ŃŠ½ŃŒŃ‚Šµ на `get_time` ще раз: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -`client_supports_apps(ctx)` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `True` лише тоГі, коли клієнт оголосив Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½ŃŃ… `mimeTypes`. Поле обов'ŃŠ·ŠŗŠ¾Š²Šµ, тож клієнт, ŃŠŗŠøŠ¹ його ŠæŃ€Š¾ŠæŃƒŃŃ‚ив, не Š·Š°Ń€Š°Ń…Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ. Даме це й Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” `main()` у Ń‚Š¾Š¼Ńƒ самому файлі: ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŃƒ половину ŃƒŠ·Š³Š¾Š“Š¶ŠµŠ½Š½Ń — і у Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ ŠæŃ€ŠøŃ…Š¾Š“ŠøŃ‚ŃŒ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½ŠøŠ¹ варіант. +`client_supports_apps(ctx)` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `True` лише тоГі, коли клієнт оголосив Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń `io.modelcontextprotocol/ui` **і** вказав `text/html;profile=mcp-app` у своїх Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½ŃŃ… `mimeTypes`. Поле обов'ŃŠ·ŠŗŠ¾Š²Šµ, тож клієнт, ŃŠŗŠøŠ¹ його ŠæŃ€Š¾ŠæŃƒŃŃ‚ив, не Š·Š°Ń€Š°Ń…Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ. ŠžŃŃŒ ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ° половина ŃƒŠ·Š³Š¾Š“Š¶ŠµŠ½Š½Ń: + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +ВіГГайте `server.py` через HTTP, а потім Ń–Š· Š“Ń€ŃƒŠ³Š¾Š³Š¾ термінала Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ клієнт: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +ŠŸŠ¾Š²ŠµŃ€Š½ŃƒŠ»Š°ŃŃ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š° Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ. ŠŸŃ€ŠøŠ±ŠµŃ€Ń–Ń‚ŃŒ `extensions=[APPS_SUPPORT]` Š· виклику `Client` — і та сама програма Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ Š½Š°Š“Ń€ŃƒŠŗŃƒŃ” `The time is 2026-06-26T12:00:00Z.`, а це все, що коли-небуГь Š±Š°Ń‡ŠøŃ‚ŃŒ ŃŃƒŃ‚Š¾ текстовий клієнт. !!! warning ŠŃ–ŠŗŠ¾Š»Šø не повертайте заглушку на ŠŗŃˆŃ‚алт `"[Rendered UI]"` ŃŠŗ єГиний вміст. Якщо резервний текст марний, Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ марний Š“Š»Ń кожного ŃŃƒŃ‚Š¾ текстового клієнта й Š“Š»Ń самої моГелі. ŠŠ°ŠæŠøŃˆŃ–Ń‚ŃŒ Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Šµ Ń€ŠµŃ‡ŠµŠ½Š½Ń. diff --git a/i18n/uk/pages/advanced/extensions.md b/i18n/uk/pages/advanced/extensions.md index 9e73f6b6..1c935aec 100644 --- a/i18n/uk/pages/advanced/extensions.md +++ b/i18n/uk/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # Š Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ŠŠ°Š¹Š¼ŠµŠ½ŃˆŠµ корисне Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń — оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ і карта Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½ŃŒ: -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` — це Š·Š½Š°Ń‡ŠµŠ½Š½Ń, що Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ”Ń‚ŃŒŃŃ в `capabilities.extensions["com.example/stamps"]`. ŠŸŠ¾Š²ŠµŃ€Š½Ń–Ń‚ŃŒ `{}` (типове Š·Š½Š°Ń‡ŠµŠ½Š½Ń), щоб оголосити Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń без Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½ŃŒ. * Š Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń ніколи не Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” сервер. Воно Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” свій внесок ŃŠŗ Гані; `MCPServer` їх споживає. ŠŃ–ŃŠŗŠ¾Š³Š¾ `self.server`, ŃŠŗŠøŠ¹ можна було б Š·Š¼Ń–Š½ŃŽŠ²Š°Ń‚Šø, немає. -А `main()` — Гоказ: клієнт у пам'яті, піГ'єГнаний Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ Го `mcp`: +Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ його через HTTP, а Гоказом буГе клієнт: -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +Кожен `server.py` на цій сторінці Š·Š°ŠæŃƒŃŠŗŠ°Ń”Ń‚ŃŒŃŃ цією ŠŗŠ¾Š¼Š°Š½Š“Š¾ŃŽ, а кожен `client.py` ŠæŃ€Š°Ń†ŃŽŃ” ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· ним: `python client.py` у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі. + ### ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń власних метоГів {#serving-your-own-methods} Š Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń може Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ²Š°Ń‚Šø **нові метоГи запитів**: власні Гієслова, ŃŠŗŃ– Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃŽŃ‚ŃŒŃŃ ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· метоГами специфікації: -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### ŠšŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ° сторона {#the-client-side} -`main()` у Ń‚Š¾Š¼Ńƒ самому файлі — це Š²ŃŃ ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ° частина, обиГві її половини: +ŠšŠ»Ń–Ń”Š½Ń‚ — окрема програма, і в ній обиГві половини ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ¾Ń— частини: -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń. ŠžŠ³Š¾Š»Š¾ŃˆŠµŠ½Š½Ń ŃŃ‚Š°ŃŽŃ‚ŃŒ `ClientCapabilities.extensions`: на Š·'єГнанні версії 2026-07-28 карта ŠæŠ¾Š“Š¾Ń€Š¾Š¶ŃƒŃ” в конверті `_meta` кожного Š·Š°ŠæŠøŃ‚Ńƒ, тож сервер Š±Š°Ń‡ŠøŃ‚ŃŒ її в **кожному** запиті; на Š·'єГнанні старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń вона ŠæŠµŃ€ŠµŠ“Š°Ń”Ń‚ŃŒŃŃ Š· Ń€ŃƒŠŗŠ¾ŃŃ‚ŠøŃŠŗŠ°Š½Š½ŃŠ¼ `initialize`. Š”ŠµŃ€Š²ŠµŃ€Š½Š¾Š¼Ńƒ коГу байГуже, ŃŠŗŠøŠ¹ саме ŃˆŠ»ŃŃ…: `require_client_extension(ctx, ...)` і `ctx.session.check_client_capability(...)` Ń‡ŠøŃ‚Š°ŃŽŃ‚ŃŒ ŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Šµ Гжерело в обох випаГках. * Š’ŠµŠ½Š“Š¾Ń€ŃŃŒŠŗŃ– метоГи Š¾ŠæŃƒŃŠŗŠ°ŃŽŃ‚ŃŒŃŃ на оГин ŃˆŠ°Ń€ нижче, Го `client.session.send_request(...)`; повноцінні метоГи в `Client` Š·'ŃŠ²Š»ŃŃŽŃ‚ŃŒŃŃ лише Š“Š»Ń Гієслів специфікації. `send_request` приймає буГь-ŃŠŗŠøŠ¹ піГклас `Request`, тож Š²ŠµŠ½Š“Š¾Ń€ŃŃŒŠŗŠøŠ¹ запит ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ ŃŠŗ є. +* `SearchRequest` і Гві моГелі, ŃŠŗŃ– він несе, — це мережевий контракт Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń, тож клієнт Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” їх Š“Š»Ń себе сам. ŠžŠæŃƒŠ±Š»Ń–ŠŗŠ¾Š²Š°Š½Šµ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń постачало б їх у пакеті, ŃŠŗŠøŠ¹ Ń–Š¼ŠæŠ¾Ń€Ń‚ŃƒŃŽŃ‚ŃŒ обиГві сторони. ### ŠŸŠµŃ€ŠµŃ…Š¾ŠæŠ»ŠµŠ½Š½Ń `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## Š’ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ¾Š³Š¾ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń {#using-a-client-extension} -**ŠšŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠµ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń** — той самий контракт Š· боку споживача: набір ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ¾Ń— повеГінки за оГним іГентифікатором. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Šø в `Client(extensions=[...])` і викликайте Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø ŃŠŗ зазвичай: +**ŠšŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠµ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń** — той самий контракт Š· боку споживача: набір ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ¾Ń— повеГінки за оГним іГентифікатором. Дервер тут віГповіГає на `buy` не товаром, а ŠŗŠ²ŠøŃ‚Š°Š½Ń†Ń–Ń”ŃŽ, ŃŠŗŃƒ треба погасити, — і лише ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ, що оголосив Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń: -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +ŠŠ° клієнті переГайте ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Šø в `Client(extensions=[...])` і викликайте Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø ŃŠŗ зазвичай: + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` повертає звичайний `CallToolResult`, ŃŠŗ і буГь-ŃŠŗŠøŠ¹ Ń–Š½ŃˆŠøŠ¹ виклик. Що змінило Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń: тепер сервер може віГповісти на `buy` **Ń„Š¾Ń€Š¼Š¾ŃŽ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ńƒ** `receipt` Š·Š°Š¼Ń–ŃŃ‚ŃŒ остаточного Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ńƒ, а `Receipts` Š“Š¾Š²Š¾Š“ŠøŃ‚ŃŒ її Го ŠŗŃ–Š½Ń†Ń (тут — ŠæŠ¾Š³Š°ŃˆŠ°ŃŽŃ‡Šø ŠŗŠ²ŠøŃ‚Š°Š½Ń†Ń–ŃŽ Š½Š°ŃŃ‚ŃƒŠæŠ½ŠøŠ¼ викликом), ŠæŠµŃ€Ńˆ ніж `call_tool` поверне Š·Š½Š°Ń‡ŠµŠ½Š½Ń. Š£ місці виклику нічого не Š·Š¼Ń–Š½ŃŽŃ”Ń‚ŃŒŃŃ. ŠŸŃ€ŠøŠ±ŠµŃ€Ń–Ń‚ŃŒ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń — і нічого Š· Ń†ŃŒŠ¾Š³Š¾ не Ń–ŃŠ½ŃƒŃ”: шлагбаум сервера Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ” ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ, ŃŠŗŠøŠ¹ його не оголосив (помилка -32021), а Š·Š°ŃŠ²Š»ŠµŠ½Š° форма віГ сервера, що Š¾Š±Ń…Š¾Š“ŠøŃ‚ŃŒ шлагбаум, не ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ ŠæŠµŃ€ŠµŠ²Ń–Ń€ŠŗŃƒ — точно так, ŃŠŗ ŃŠæŠµŃ†ŠøŃ„Ń–ŠŗŠ°Ń†Ń–Ń вимагає Š“Š»Ń нерозпізнаного `resultType`. Вимкнено за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼, Š· обох кінців Š·'Ń”Š“Š½Š°Š½Š½Ń. @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ŠŠ°ŠæŠøŃŠ°Š½Š½Ń ŠŗŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ¾Š³Š¾ Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń {#writing-a-client-extension} Š£ŃŠæŠ°Š“ŠŗŃƒŠ¹Ń‚Šµ `ClientExtension` і перевизначте лише те, що потрібно. Три виГи внеску, кожен Ń–Š· Ń‚ŠøŠæŠ¾Š²Š¾ŃŽ Ń€ŠµŠ°Š»Ń–Š·Š°Ń†Ń–Ń”ŃŽ: `settings()`, `claims()` і `notifications()`. -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * ІГентифікатор ŠæŃ–Š“ŠæŠ¾Ń€ŃŠ“ŠŗŠ¾Š²ŃƒŃ”Ń‚ŃŒŃŃ тій самій граматиці, що й на сервері, і ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ”Ń‚ŃŒŃŃ піГ час Š²ŠøŠ·Š½Š°Ń‡ŠµŠ½Š½Ń класу. @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### Дієслова Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½ŃŒ {#extension-verbs} -Власні метоГи запитів Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń не ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ŃŒ реєстрації на клієнті. Тип Š²ŠµŠ½Š“Š¾Ń€ŃŃŒŠŗŠ¾Š³Š¾ Š·Š°ŠæŠøŃ‚Ńƒ ŃƒŃŠæŠ°Š“ŠŗŠ¾Š²ŃƒŃ” `mcp.types.Request` і ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ через `client.session.send_request`, ŃŠŗ у розГілі [ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń власних метоГів](#serving-your-own-methods). ŠžŠ“Š½Šµ Š“Š¾ŠæŠ¾Š²Š½ŠµŠ½Š½Ń: коли ŠŗŠ»ŃŽŃ‡ параметрів Š¼ŃƒŃŠøŃ‚ŃŒ ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŠøŃŃ в заголовку `Mcp-Name` (специфікації Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½ŃŒ, ŃŠŗ-от tasks, Š²ŠøŠ¼Š°Š³Š°ŃŽŃ‚ŃŒ Ń†ŃŒŠ¾Š³Š¾ Š“Š»Ń своїх Гієслів), тип Š·Š°ŠæŠøŃ‚Ńƒ Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” `name_param`: +Власні метоГи запитів Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń не ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ŃŒ реєстрації на клієнті. Тип Š²ŠµŠ½Š“Š¾Ń€ŃŃŒŠŗŠ¾Š³Š¾ Š·Š°ŠæŠøŃ‚Ńƒ ŃƒŃŠæŠ°Š“ŠŗŠ¾Š²ŃƒŃ” `mcp.types.Request` і ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ через `client.session.send_request`, ŃŠŗ у розГілі [ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń власних метоГів](#serving-your-own-methods). Š’Ń–Š·ŃŒŠ¼Ń–Š¼Š¾ сервер, Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń ŃŠŗŠ¾Š³Š¾ Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” оГне Гієслово про іменоване Š·Š°Š²Š“Š°Š½Š½Ń: -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +ŠžŠ“Š½Šµ Š“Š¾ŠæŠ¾Š²Š½ŠµŠ½Š½Ń на клієнті: коли ŠŗŠ»ŃŽŃ‡ параметрів Š¼ŃƒŃŠøŃ‚ŃŒ ŠæŠµŃ€ŠµŠ“Š°Š²Š°Ń‚ŠøŃŃ в заголовку `Mcp-Name` (специфікації Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½ŃŒ, ŃŠŗ-от tasks, Š²ŠøŠ¼Š°Š³Š°ŃŽŃ‚ŃŒ Ń†ŃŒŠ¾Š³Š¾ Š“Š»Ń своїх Гієслів), тип Š·Š°ŠæŠøŃ‚Ńƒ Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + Š”ŠµŃŃ–Ń Š“Š·ŠµŃ€ŠŗŠ°Š»ŠøŃ‚ŃŒ `params["jobId"]` у `Mcp-Name` на кожному ŃˆŠ»ŃŃ…Ńƒ Š½Š°Š“ŃŠøŠ»Š°Š½Š½Ń, а Š²Ń–Š“ŃŃƒŃ‚Š½Ń” Š·Š½Š°Ń‡ŠµŠ½Š½Ń Гає Š³ŃƒŃ‡Š½Ńƒ помилку Š·Š°Š¼Ń–ŃŃ‚ŃŒ того, щоб мовчки ŠæŃ€Š¾ŠæŃƒŃŃ‚ити обов'ŃŠ·ŠŗŠ¾Š²ŠøŠ¹ заголовок. ## Чого Ń€Š¾Š·ŃˆŠøŃ€ŠµŠ½Š½Ń не може {#what-an-extension-cannot-do} diff --git a/i18n/uk/pages/advanced/low-level-server.md b/i18n/uk/pages/advanced/low-level-server.md index 98926a31..2e9859f8 100644 --- a/i18n/uk/pages/advanced/low-level-server.md +++ b/i18n/uk/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # ŠŠøŠ·ŃŒŠŗŠ¾Ń€Ń–Š²Š½ŠµŠ²ŠøŠ¹ Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### Š”ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ самі {#try-it} -Inspector тут не Гопоможе: `mcp dev` і `mcp run` ŠæŃ€ŠøŠ¹Š¼Š°ŃŽŃ‚ŃŒ лише `MCPServer`. `Client`, що ŠæŃ€Š°Ń†ŃŽŃ” в пам'яті, Го Ń†ŃŒŠ¾Š³Š¾ байГужий: Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–Š²Š½ŠµŠ²ŠøŠ¹ `Server` він приймає так само, ŃŠŗ і `MCPServer`: +`mcp dev` і `mcp run` ŠæŃ€ŠøŠ¹Š¼Š°ŃŽŃ‚ŃŒ лише `MCPServer`, тож цей сервер Š“Š¾Š²ŠµŠ“ŠµŃ‚ŃŒŃŃ Š·Š°ŠæŃƒŃŠŗŠ°Ń‚Šø самостійно. ŠžŃŃ‚Š°Š½Š½Ń–Š¹ Ń€ŃŠ“Š¾Šŗ `server.py` Š±ŃƒŠ“ŃƒŃ” Š· нього звичайний ASGI-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, а uvicorn його Š·Š°ŠæŃƒŃŠŗŠ°Ń”: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +Š”ŠæŃ€ŃŠ¼ŃƒŠ¹Ń‚Šµ Inspector або буГь-ŃŠŗŠøŠ¹ клієнт на `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None`. Високорівневий сервер загортає `-> str` у `{"result": ...}` за вас; тут ніхто не Š±ŃƒŠ“ŃƒŃ” того, чого не побуГували ви. * `list_tools` повертає ŃŃ…ŠµŠ¼Ńƒ, ŃŠŗŃƒ набрали **ви**, символ у символ. Š£ високорівневій версії на кожній властивості було `"title": "Query"`, а в корені — `"title": "search_booksArguments"`: артефакти Pydantic. Š¢ŃƒŃ‚, ŃŠŗŃ‰Š¾ Ń‰Š¾ŃŃŒ є в переГаних Ганих, — це ви його Ń‚ŃƒŠ“Šø поклали. +Š£ тесті uvicorn і порт не потрібні: `Client(server)` приймає Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–Š²Š½ŠµŠ²ŠøŠ¹ `Server` у Ń‚Š¾Š¼Ńƒ ж процесі так само, ŃŠŗ і `MCPServer`, — саме цей піГхіГ описано на сторінці **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)**. + ## ŠŃ–Ń…Ń‚Š¾ нічого не ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” за вас {#nothing-is-checked-for-you} `MCPServer` Š²Ń–Š“Ń…ŠøŠ»ŃŃ” хибний Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ще Го того, ŃŠŗ ваша Ń„ŃƒŠ½ŠŗŃ†Ń–Ń Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚ŃŒŃŃ, ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃŽŃ‡Šø виклик за ŃŃ…ŠµŠ¼Š¾ŃŽ, ŃŠŗŃƒ сам Š·Š³ŠµŠ½ŠµŃ€ŃƒŠ²Š°Š² (**[Š†Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø](../servers/tools.md)**). @@ -215,4 +221,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” буГь-ŃŠŗŠøŠ¹ метоГ. `initialize` зарезервовано. * ŠœŠ¾Š¶Š»ŠøŠ²Š¾ŃŃ‚Ń–, ŃŠŗŃ– Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” `Server`, Š²ŠøŠ²Š¾Š“ŃŃ‚ŃŒŃŃ Š· того, ŃŠŗŃ– обробники ви Š·Š°Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ²Š°Š»Šø. -`Client(server)` ŠæŠ¾Š²Š¾Š“ŠøŠ²ŃŃ Š· обома серверами оГнаково, бо вони *і є* тим самим протоколом — у Ń†ŃŒŠ¾Š¼Ńƒ й увесь сенс. ŠŠ°ŃŃ‚ŃƒŠæŠ½ŠøŠ¹ ŃˆŠ°Ń€ нижче — взагалі не клас: це **[Middleware](middleware.md)**. +ŠšŠ»Ń–Ń”Š½Ń‚ ŠæŠ¾Š²Š¾Š“ŠøŠ²ŃŃ Š· обома серверами оГнаково, бо вони *і є* тим самим протоколом — у Ń†ŃŒŠ¾Š¼Ńƒ й увесь сенс. ŠŠ°ŃŃ‚ŃƒŠæŠ½ŠøŠ¹ ŃˆŠ°Ń€ нижче — взагалі не клас: це **[Middleware](middleware.md)**. diff --git a/i18n/uk/pages/advanced/pagination.md b/i18n/uk/pages/advanced/pagination.md index 01888ab4..fc962221 100644 --- a/i18n/uk/pages/advanced/pagination.md +++ b/i18n/uk/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # ŠŸŠ°Š³Ń–Š½Š°Ń†Ń–Ń {#pagination} @@ -31,9 +31,13 @@ translation: ### Š”ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ самі {#try-it} -`Client(server)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Го Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–вневого `Server` у пам'яті так само, ŃŠŗ і Го `MCPServer`. +`mcp run` приймає лише `MCPServer`, тож цей сервер Š“Š¾Š²ŠµŠ“ŠµŃ‚ŃŒŃŃ Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Ń‚Šø самостійно. ŠžŃŃ‚Š°Š½Š½Ń–Š¹ Ń€ŃŠ“Š¾Šŗ `server.py` Š±ŃƒŠ“ŃƒŃ” Š· `Server` звичайний ASGI-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, а uvicorn його Š·Š°ŠæŃƒŃŠŗŠ°Ń”: -Викличте `list_resources()` без Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ів. ŠŸŠ¾Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ Š“ŠµŃŃŃ‚ŃŒ Ń€ŠµŃŃƒŃ€ŃŃ–Š², віГ `book-1` Го `book-10`, а `next_cursor` — Ń€ŃŠ“Š¾Šŗ `"10"`. +```console +uvicorn server:app --port 8000 +``` + +Š”ŠæŃ€ŃŠ¼ŃƒŠ¹Ń‚Šµ буГь-ŃŠŗŠøŠ¹ клієнт (**[ŠšŠ»Ń–Ń”Š½Ń‚](../client/index.md)** або Inspector) на `http://localhost:8000/mcp` і викличте `list_resources()` без Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ів. ŠŸŠ¾Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ Š“ŠµŃŃŃ‚ŃŒ Ń€ŠµŃŃƒŃ€ŃŃ–Š², віГ `book-1` Го `book-10`, а `next_cursor` — Ń€ŃŠ“Š¾Šŗ `"10"`. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ його назаГ через `list_resources(cursor="10")` — і ŠæŠµŃ€ŃˆŠøŠ¹ Ń€ŠµŃŃƒŃ€Ń уже `book-11`, а новий `next_cursor` — `"20"`. @@ -43,7 +47,7 @@ translation: Кожен метоГ `list_*` класу `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) приймає іменований параметр `cursor=`. Вичерпати список Š·Ń– сторінками — це оГин `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -51,7 +55,7 @@ translation: * Š Š¾Š·ŃˆŠøŃ€ŃŽŠ¹Ń‚Šµ список **Го** того, ŃŠŗ Š“ŠøŠ²ŠøŃ‚ŠøŃŃ на `next_cursor`: на останній сторінці теж є Ń€ŠµŃŃƒŃ€ŃŠø. * `next_cursor is None` — це вихіГ. Усе Ń–Š½ŃˆŠµ йГе ŠæŃ€ŃŠ¼Š¾ назаГ у `cursor=`, без змін. -Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ його `main()` — і він Š½Š°Š“Ń€ŃƒŠŗŃƒŃ” `100 resources`: Š“ŠµŃŃŃ‚ŃŒ сторінок по Š“ŠµŃŃŃ‚ŃŒ, Š·ŃˆŠøŃ‚ŠøŃ… циклом, ŃŠŗŠøŠ¹ так і не Š“Ń–Š·Š½Š°Š²ŃŃ, що сторінок було Š“ŠµŃŃŃ‚ŃŒ. +Поки uvicorn усе ще Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” `server.py`, Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `python client.py` у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі. Він Š½Š°Š“Ń€ŃƒŠŗŃƒŃ” `100 resources`: Š“ŠµŃŃŃ‚ŃŒ сторінок по Š“ŠµŃŃŃ‚ŃŒ, Š·ŃˆŠøŃ‚ŠøŃ… циклом, ŃŠŗŠøŠ¹ так і не Š“Ń–Š·Š½Š°Š²ŃŃ, що сторінок було Š“ŠµŃŃŃ‚ŃŒ. Це той самий цикл, ŃŠŗŠøŠ¹ **[ŠšŠ»Ń–Ń”Š½Ń‚](../client/index.md)** ŠæŠ¾ŠŗŠ°Š·ŃƒŃ” Š“Š»Ń кожного Гієслова `list_*`, і проти сервера без сторінок він нічого не ŠŗŠ¾ŃˆŃ‚ŃƒŃ”: `next_cursor` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None` вже в ŠæŠµŃ€ŃˆŃ–Š¹ віГповіГі, і цикл Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ оГин раз. diff --git a/i18n/uk/pages/client/caching.md b/i18n/uk/pages/client/caching.md index 95ba40ec..055bd21d 100644 --- a/i18n/uk/pages/client/caching.md +++ b/i18n/uk/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # ŠŸŃ–Š“ŠŗŠ°Š·ŠŗŠø щоГо ŠŗŠµŃˆŃƒŠ²Š°Š½Š½Ń {#caching-hints} @@ -30,7 +30,7 @@ translation: ŠŠ° Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–Š²Š½ŠµŠ²Š¾Š¼Ńƒ `Server` обробники Š±ŃƒŠ“ŃƒŃŽŃ‚ŃŒ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Šø Š²Ń€ŃƒŃ‡Š½Ńƒ, а `ttl_ms` / `cache_scope` — звичайні ŠæŠ¾Š»Ń моГелей Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń–Š². ŠžŠ±Ń€Š¾Š±Š½ŠøŠŗ, ŃŠŗŠøŠ¹ заГає їх ŃŠ²Š½Š¾, завжГи перемагає мапу ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, поле за полем: -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -44,10 +44,24 @@ translation: Š£ сесії 2026-07-28 `Client` Š“Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ”Ń‚ŃŒŃŃ піГказок за вас: він має вбуГований кеш віГповіГей, ŃƒŠ²Ń–Š¼ŠŗŠ½ŠµŠ½ŠøŠ¹ за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼. Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚, що ŠæŃ€ŠøŠ¹ŃˆŠ¾Š² Ń–Š· `ttlMs`, Š·Š±ŠµŃ€Ń–Š³Š°Ń”Ń‚ŃŒŃŃ, і іГентичний виклик у межах Ń†ŃŒŠ¾Š³Š¾ TTL Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ Š· кешу без Š·Š²ŠµŃ€Š½ŠµŠ½Š½Ń Го сервера. Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ *без* піГказки не ŠŗŠµŃˆŃƒŃ”Ń‚ŃŒŃŃ: Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Šø без піГказок Š¾Ń‚Ń€ŠøŠ¼ŃƒŃŽŃ‚ŃŒ `CacheConfig.default_ttl_ms`, типове Š·Š½Š°Ń‡ŠµŠ½Š½Ń ŃŠŗŠ¾Š³Š¾ `0` (Š¾Š“Ń€Š°Š·Ńƒ застарілий), тож сервер, ŃŠŗŠøŠ¹ нічого не Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ”, Š±Š°Ń‡ŠøŃ‚ŃŒ рівно той самий трафік виклик-за-викликом, що й завжГи. -```python title="client.py" hl_lines="33 35 38" +Щоб побачити це на власні очі, Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `server.py` Š· ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š½ŃŒŠ¾Š³Š¾ Ń€Š¾Š·Š“Ń–Š»Ńƒ через uvicorn (його останній Ń€ŃŠ“Š¾Šŗ Š±ŃƒŠ“ŃƒŃ” ASGI-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ). ŠžŠ±Ń€Š¾Š±Š½ŠøŠŗ Š“Ń€ŃƒŠŗŃƒŃ” Ń€ŃŠ“Š¾Šŗ Ń‰Š¾Ń€Š°Š·Ńƒ, коли справГі Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ: + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `python client.py` у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі. Він Š½Š°Š“Ń€ŃƒŠŗŃƒŃ” піГказки, ŃŠŗŃ– містив ŠæŠµŃ€ŃˆŠøŠ¹ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚: `ttlMs` обробника ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· `cacheScope` Š· мапи: + +```text +1000 public +``` + +Š ŠµŃˆŃ‚Ńƒ розповіГає термінал сервера: між логами запитів uvicorn Ń€ŃŠ“Š¾Šŗ `tools/list served` Š·'ŃŠ²Š»ŃŃ”Ń‚ŃŒŃŃ тричі. + Чотири виклики, три Š¾Ń‚Ń€ŠøŠ¼Š°Š½Š½Ń. Š”Ń€ŃƒŠ³ŠøŠ¹ виклик знайшов свіжий запис і не Š“Ń–Š¹ŃˆŠ¾Š² Го сервера; ŠæŠµŃ€ŠµŠ²ŠµŠ“ŠµŠ½Š½Ń (впроваГженого) гоГинника за межу TTL змусило третій знову Š·Š²ŠµŃ€Š½ŃƒŃ‚ŠøŃŃ Го сервера; четвертий сказав `cache_mode="refresh"`. Цей іменований Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ є в Šæ'ŃŃ‚Šø метоГів Ń–Š· ŠŗŠµŃˆŃƒŠ²Š°Š½Š½ŃŠ¼ (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`): * `"use"` (за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼) віГГає свіжий запис, ŃŠŗŃ‰Š¾ він є, а ŃŠŗŃ‰Š¾ ні — Š·Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ Го сервера і зберігає Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. @@ -56,7 +70,7 @@ translation: ŠžŠ“Š½Šµ правило ŃŃ‚Š¾Ń—Ń‚ŃŒ наГ `"use"`: **виклики Š· `meta` завжГи Š“Š¾Ń…Š¾Š“ŃŃ‚ŃŒ Го сервера.** Запит Ń–Š· заГаним `meta` (токен ŠæŠµŃ€ŠµŠ±Ń–Š³Ńƒ Š²ŠøŠŗŠ¾Š½Š°Š½Š½Ń, ŠæŠ¾Š»Ń Ń‚Ń€Š°ŃŃƒŠ²Š°Š½Š½Ń) Š¾Ń‡Ń–ŠŗŃƒŃ” Š·Š°ŠæŠøŃ‚Ńƒ Š¼ŠµŃ€ŠµŠ¶ŠµŃŽ, Ń‚Š¾Š¼Ńƒ за `cache_mode="use"` його Š¾Š±Ń€Š¾Š±Š»ŃŃŽŃ‚ŃŒ ŃŠŗ `"refresh"`: Ń‡ŠøŃ‚Š°Š½Š½Ń Š· кешу ŠæŃ€Š¾ŠæŃƒŃŠŗŠ°Ń”Ń‚ŃŒŃŃ, а отриманий Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ усе оГно Š·Š°Š¼Ń–Š½ŃŽŃ” закешований запис. `"bypass"` і ŃŠ²Š½ŠøŠ¹ `"refresh"` ŠæŠ¾Š²Š¾Š“ŃŃ‚ŃŒŃŃ ŃŠŗ завжГи. -Щоб ŠæŠ¾Š²Š½Ń–ŃŃ‚ŃŽ Š²ŠøŠ¼ŠŗŠ½ŃƒŃ‚Šø ŠŗŠµŃˆŃƒŠ²Š°Š½Š½Ń, ŃŃ‚Š²Š¾Ń€Ń–Ń‚ŃŒ клієнт ŃŠŗ `Client(server, cache=None)`: кожен виклик знову стає Š·Š²ŠµŃ€Š½ŠµŠ½Š½ŃŠ¼ Го сервера, а `cache_mode`, хоч і ŠæŃ€ŠøŠ¹Š¼Š°Ń”Ń‚ŃŒŃŃ Галі, нічого не Ń€Š¾Š±ŠøŃ‚ŃŒ. +Щоб ŠæŠ¾Š²Š½Ń–ŃŃ‚ŃŽ Š²ŠøŠ¼ŠŗŠ½ŃƒŃ‚Šø ŠŗŠµŃˆŃƒŠ²Š°Š½Š½Ń, переГайте `cache=None` піГ час ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń `Client`: кожен виклик знову стає Š·Š²ŠµŃ€Š½ŠµŠ½Š½ŃŠ¼ Го сервера, а `cache_mode`, хоч і ŠæŃ€ŠøŠ¹Š¼Š°Ń”Ń‚ŃŒŃŃ Галі, нічого не Ń€Š¾Š±ŠøŃ‚ŃŒ. ŠžŠ±Š»Š°ŃŃ‚ŃŒ також Š“Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ”Ń‚ŃŒŃŃ автоматично: записи `"private"` прив'ŃŠ·Š°Š½Ń– Го *Ń€Š¾Š·Š“Ń–Š»Ńƒ* (partition) кешу (нижче), тоГі ŃŠŗ записи `"public"` Š¼Š¾Š¶ŃƒŃ‚ŃŒ за Š±Š°Š¶Š°Š½Š½ŃŠ¼ Š“Ń–Š»ŠøŃ‚ŠøŃŃ ŃˆŠøŃ€ŃˆŠµ. А **ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń ŠæŠµŃ€ŠµŠ¼Š°Š³Š°ŃŽŃ‚ŃŒ TTL** Š“Š»Ń конкретних записів, ŃŠŗŃ– вони Š½Š°Š·ŠøŠ²Š°ŃŽŃ‚ŃŒ: ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń `list_changed` Š²ŠøŃ‚Ń–ŃŠ½ŃŃ” віГповіГний закешований список, а `resources/updated` Š²ŠøŃ‚Ń–ŃŠ½ŃŃ” закешоване Ń‡ŠøŃ‚Š°Š½Š½Ń, збережене рівно піГ його URI, хоч би ŃŠŗŠøŠ¼Šø свіжими вони були. ŠŠ° Š·'єГнанні 2026-07-28 ці ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń Š½Š°Š“Ń…Š¾Š“ŃŃ‚ŃŒ потоком `subscriptions/listen`, ŃŠŗŠøŠ¹ Š²Ń–Š“ŠŗŃ€ŠøŠ²Š°ŃŽŃ‚ŃŒ через `client.listen(...)`, і Š²ŠøŃ‚Ń–ŃŠ½ŠµŠ½Š½Ń Š·Š°Š²ŠµŃ€ŃˆŃƒŃ”Ń‚ŃŒŃŃ Го того, ŃŠŗ ваш спостерігач ŠæŠ¾Š±Š°Ń‡ŠøŃ‚ŃŒ ŠæŠ¾Š“Ń–ŃŽ; Š“Š¾ŠŗŠ»Š°Š“Š½Ń–ŃˆŠµ — на сторінці **[ŠŸŃ–Š“ŠæŠøŃŠŗŠø](subscriptions.md)**. diff --git a/i18n/uk/pages/client/callbacks.md b/i18n/uk/pages/client/callbacks.md index 73dc5328..912a32a0 100644 --- a/i18n/uk/pages/client/callbacks.md +++ b/i18n/uk/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # Колбеки клієнта {#client-callbacks} @@ -63,7 +63,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') `mode="legacy"` у виклику `Client(...)` ŃŃ‚Š¾Ń—Ń‚ŃŒ не просто так. За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ `Client(...)` ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” ŃŃƒŃ‡Š°ŃŠ½ŠøŠ¹ ŃˆŠ»ŃŃ… ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ, а на ньому немає зворотного каналу (back-channel) Š“Š»Ń запитів віГ сервера Го клієнта: `ctx.elicit` Š·Š°Š²ŠµŃ€ŃˆŃƒŃ”Ń‚ŃŒŃŃ ŠæŠ¾Š¼ŠøŠ»ŠŗŠ¾ŃŽ ще Го того, ŃŠŗ Š·Š°ŠæŃƒŃŃ‚ŠøŃ‚ŃŒŃŃ колбек. Š’ŠøŃ€Ń–ŃˆŃƒŃ” це не транспорт, а узгоГжений - протокол — оГнаково і в пам'яті, і за URL. Š¤Ń–ŠŗŃŃƒŠ¹Ń‚Šµ `mode="legacy"` Ń‰Š¾Ń€Š°Š·Ńƒ, коли клієнт має + протокол. Š¤Ń–ŠŗŃŃƒŠ¹Ń‚Šµ `mode="legacy"` Ń‰Š¾Ń€Š°Š·Ńƒ, коли клієнт має віГповіГати на такий запит; так Ń€Š¾Š±ŠøŃ‚ŃŒ кожен тест за цією ŃŃ‚Š¾Ń€Ń–Š½ŠŗŠ¾ŃŽ. Š”Š¾ŠŗŠ»Š°Š“Š½Ń–ŃˆŠµ — на сторінці **[Версії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ](../protocol-versions.md)**. Š£ сесії 2026-07-28 колбек не зникає, він просто Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” Гані Ń–Š½Š°ŠŗŃˆŠµ: коли Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ повертає @@ -151,4 +151,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` і `list_roots_callback` ŠæŃ€Š°Ń†ŃŽŃŽŃ‚ŃŒ так само, але Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃŽŃ‚ŃŒ застарілі можливості; ŃŃƒŃ‡Š°ŃŠ½Ń– сервери Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‚ŃŒ Š±Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń– запити. * `logging_callback` і `message_handler` Š¾Ń‚Ń€ŠøŠ¼ŃƒŃŽŃ‚ŃŒ ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń. Вони нічого не Š¾Š³Š¾Š»Š¾ŃˆŃƒŃŽŃ‚ŃŒ. -ŠŸŠµŃ€ŃˆŠøŠ¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `Client(...)` — об'єкт Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ. Усі його різновиГи описано на сторінці **[Транспорти клієнта](transports.md)**. +ŠŸŠµŃ€ŃˆŠøŠ¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `Client(...)` визначає транспорт. Усі його різновиГи описано на сторінці **[Транспорти клієнта](transports.md)**. diff --git a/i18n/uk/pages/client/index.md b/i18n/uk/pages/client/index.md index 91a41062..4fc9f8fa 100644 --- a/i18n/uk/pages/client/index.md +++ b/i18n/uk/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # ŠšŠ»Ń–Ń”Š½Ń‚ {#the-client} @@ -11,13 +11,23 @@ translation: ## ŠŸŠµŃ€ŃˆŠøŠ¹ клієнт {#your-first-client} -```python title="client.py" hl_lines="14-18" +ŠšŠ»Ń–Ń”Š½Ń‚Š¾Š²Ń– потрібен сервер, Š· ŃŠŗŠøŠ¼ говорити. До Ń†ŃŒŠ¾Š³Š¾ Bookshop піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ кожен приклаГ на цій сторінці. Š—Š±ŠµŃ€ŠµŠ¶Ń–Ń‚ŃŒ його ŃŠŗ `server.py` і Š·Š°Š»ŠøŃˆŃ‚Šµ ŠæŃ€Š°Ń†ŃŽŠ²Š°Ń‚Šø через HTTP: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -Дервер ŃƒŠ³Š¾Ń€Ń– потрібен лише Š“Š»Ń того, щоб було Го чого піГ'Ń”Š“Š½Š°Ń‚ŠøŃŃ. ŠšŠ»Ń–Ń”Š½Ń‚ — це Šæ'ять виГілених Ń€ŃŠ“ŠŗŃ–Š². +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” **сам об'єкт сервера**. Це транспорт у пам'яті: без ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ, без ŠæŠ¾Ń€Ń‚Ńƒ, без HTTP. Даме так піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ кожен приклаГ на цій сторінці й кожен тест, ŃŠŗŠøŠ¹ ви Š½Š°ŠæŠøŃˆŠµŃ‚е. +Тепер сервер Š“Š¾ŃŃ‚ŃƒŠæŠ½ŠøŠ¹ за Š°Š“Ń€ŠµŃŠ¾ŃŽ `http://localhost:8000/mcp`. ŠšŠ»Ń–Ń”Š½Ń‚ — окрема програма. Š—Š±ŠµŃ€ŠµŠ¶Ń–Ń‚ŃŒ його ŃŠŗ `client.py` і Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `python client.py` у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” **URL**, тож піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ через Streamable HTTP Го сервера, ŃŠŗŠøŠ¹ ви щойно Š·Š°ŠæŃƒŃŃ‚или. * `async with` — це **життєвий цикл**. Š’Ń…Ń–Š“ у блок піГ'Ń”Š“Š½ŃƒŃ” й ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” параметри; вихіГ — віГ'Ń”Š“Š½ŃƒŃ”. ŠŸŠ°Ń€Šø `connect()` / `close()` немає, а `Client` не можна використати повторно ŠæŃ–ŃŠ»Ń Š·Š°Š²ŠµŃ€ŃˆŠµŠ½Š½Ń блоку. * УсереГині блоку віГомості про Š·'Ń”Š“Š½Š°Š½Š½Ń вже Š“Š¾ŃŃ‚ŃƒŠæŠ½Ń– ŃŠŗ звичайні властивості. @@ -25,10 +35,10 @@ translation: `Client` приймає оГин позиційний Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ і визначає транспорт за його типом: -* Š•ŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ `MCPServer` (або Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–вневого `Server`): піГ'Ń”Š“Š½Š°Š½Š½Ń **в межах ŠæŃ€Š¾Ń†ŠµŃŃƒ**. -* Š ŃŠ“Š¾Šŗ Š· URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, ŃˆŠ»ŃŃ… Š“Š»Ń робочого Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń. -* `StdioServerParameters`: команГа, ŃŠŗŃƒ буГе Š·Š°ŠæŃƒŃ‰ŠµŠ½Š¾ ŃŠŗ **піГпроцес**; ŃŠæŃ–Š»ŠŗŃƒŠ²Š°Š½Š½Ń Š· ним іГе через його stdin і stdout. +* Š ŃŠ“Š¾Šŗ Š· URL (`Client("http://localhost:8000/mcp")`): Streamable HTTP, транспорт Š“Š»Ń робочого Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń. +* `StdioServerParameters`: команГа, ŃŠŗŃƒ буГе Š·Š°ŠæŃƒŃ‰ŠµŠ½Š¾ ŃŠŗ локальний **піГпроцес**; ŃŠæŃ–Š»ŠŗŃƒŠ²Š°Š½Š½Ń Š· ним іГе через його stdin і stdout. * **Транспорт**: буГь-що, що можна використати ŃŠŗ `async with ... as (read, write)`, наприклаГ `streamable_http_client(url, http_client=...)` навколо вашого власного HTTP-клієнта. +* Š•ŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ `MCPServer` (або Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–вневого `Server`): піГ'Ń”Š“Š½Š°Š½Š½Ń **в межах ŠæŃ€Š¾Ń†ŠµŃŃƒ**, без ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ й без ŠæŠ¾Ń€Ń‚Ńƒ. Це Š“Š»Ń тестів, і на ньому побуГована сторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)**. Усе Ń–Š½ŃˆŠµ на цій сторінці оГнакове Š“Š»Ń всіх Ń‡Š¾Ń‚ŠøŃ€ŃŒŠ¾Ń…. Заголовки, піГпроцеси, тайм-Š°ŃƒŃ‚Šø та протокол `Transport` Š¼Š°ŃŽŃ‚ŃŒ власну ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ: **[Транспорти клієнта](transports.md)**. @@ -49,11 +59,11 @@ translation: ## ŠŸŠµŃ€ŠµŠ»Ń–Šŗ Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń–Š² {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` повертає `ListToolsResult`; Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š»ŠµŠ¶Š°Ń‚ŃŒ у `.tools`. Кожен Ń–Š· них — повне Š¾Š·Š½Š°Ń‡ŠµŠ½Š½Ń, ŃŠŗŠµ хост переГав би моГелі: +`list_tools()` повертає `ListToolsResult`; Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š»ŠµŠ¶Š°Ń‚ŃŒ у `.tools`. Кожен Ń–Š· них — повне Š¾Š·Š½Š°Ń‡ŠµŠ½Š½Ń, ŃŠŗŠµ хост переГав би моГелі. ŠžŃŃŒ ŠæŠµŃ€ŃˆŠøŠ¹: ```python tool.name # 'search_books' @@ -77,6 +87,8 @@ tool.description # 'Search the catalog by title or author.' Š¦Ń схема — усе, що потрібно UI, щоб показати Ń„Š¾Ń€Š¼Ńƒ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń–Š², і все, що потрібно моГелі, щоб ŃŃ„Š¾Ń€Š¼ŃƒŠ²Š°Ń‚Šø коректні Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Šø. +Š”Ń€ŃƒŠ³ŠøŠ¹ Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, `lookup_book`, зареєстровано без `title=`, тож його `tool.title` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None`. + !!! tip `title` необов'ŃŠ·ŠŗŠ¾Š²ŠøŠ¹, тож UI, що ŠæŠ¾ŠŗŠ°Š·ŃƒŃ” Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š»ŃŽŠ“ŠøŠ½Ń–, має обирати: `title`, ŃŠŗŃ‰Š¾ він є, і `name`, ŃŠŗŃ‰Š¾ немає. `from mcp.shared.metadata_utils import get_display_name` Ń€Š¾Š±ŠøŃ‚ŃŒ саме це — @@ -86,7 +98,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` Š·Š°ŠæŃƒŃŠŗŠ°Ń” Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ і повертає `CallToolResult`. -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -142,7 +154,7 @@ result.is_error # False Дії Š· Ń€ŠµŃŃƒŃ€ŃŠ°Š¼Šø Š¹Š“ŃƒŃ‚ŃŒ парами: Гва способи перелічити, оГин спосіб прочитати. -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -156,7 +168,7 @@ result.is_error # False ## ŠŸŃ€Š¾Š¼ŠæŃ‚Šø {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -181,7 +193,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Дервер Š· обробником Š°Š²Ń‚Š¾Š“Š¾ŠæŠ¾Š²Š½ŠµŠ½Š½Ń може Š“Š¾ŠæŠ¾Š²Š½ŃŽŠ²Š°Ń‚Šø Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Šø промптів і ŃˆŠ°Š±Š»Š¾Š½Ń–Š² Ń€ŠµŃŃƒŃ€ŃŃ–Š², поки ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ Š“Ń€ŃƒŠŗŃƒŃ”. -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -194,21 +206,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from Кожен метоГ `list_*` приймає іменований Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ `cursor=`, а кожен Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ Š¼Ń–ŃŃ‚ŠøŃ‚ŃŒ `next_cursor`. Коли `next_cursor` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None`, у вас є все. -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -Цей цикл коректний Š“Š»Ń буГь-ŃŠŗŠ¾Š³Š¾ сервера. `MCPServer` повертає все Š¾Š“Š½Ń–Ń”ŃŽ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŠ¾ŃŽ, тож `next_cursor` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None` і цикл Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ оГин раз — саме Ń‚Š¾Š¼Ńƒ Š±Ń–Š»ŃŒŃˆŃ–ŃŃ‚ŃŒ коГу його ніколи не пише. Дервери, що справГі Ń€Š¾Š·Š±ŠøŠ²Š°ŃŽŃ‚ŃŒ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Šø на сторінки, і правила, ŃŠŗŠøŠ¼ ŠæŃ–Š“ŠŗŠ¾Ń€ŃŃŽŃ‚ŃŒŃŃ ŠŗŃƒŃ€ŃŠ¾Ń€Šø, — на сторінці **[ŠŸŠ°Š³Ń–Š½Š°Ń†Ń–Ń](../advanced/pagination.md)**. +`list_all_tools` коректна Š“Š»Ń буГь-ŃŠŗŠ¾Š³Š¾ сервера. `MCPServer` повертає все Š¾Š“Š½Ń–Ń”ŃŽ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŠ¾ŃŽ, тож `next_cursor` Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `None` і цикл Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ оГин раз — саме Ń‚Š¾Š¼Ńƒ Š±Ń–Š»ŃŒŃˆŃ–ŃŃ‚ŃŒ коГу його ніколи не пише. Дервери, що справГі Ń€Š¾Š·Š±ŠøŠ²Š°ŃŽŃ‚ŃŒ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Šø на сторінки, і правила, ŃŠŗŠøŠ¼ ŠæŃ–Š“ŠŗŠ¾Ń€ŃŃŽŃ‚ŃŒŃŃ ŠŗŃƒŃ€ŃŠ¾Ń€Šø, — на сторінці **[ŠŸŠ°Š³Ń–Š½Š°Ń†Ń–Ń](../advanced/pagination.md)**. ## Š£ тестах {#in-tests} -`Client(mcp)` без ŠæŃ€Š¾Ń†ŠµŃŃƒ й без ŠæŠ¾Ń€Ń‚Ńƒ — це вже тестова обв'ŃŠ·ŠŗŠ° Š“Š»Ń вашого сервера. +Кожен `client.py` на цій сторінці Š·Š²ŠµŃ€Ń‚Š°Š²ŃŃ Го `server.py` через HTTP. Š£ тесті Š¼ŠµŃ€ŠµŠ¶Ńƒ Š¾Š¼ŠøŠ½Š°ŃŽŃ‚ŃŒ і ŠæŠµŃ€ŠµŠ“Š°ŃŽŃ‚ŃŒ у `Client` сам об'єкт сервера: `from server import mcp`, потім `Client(mcp)`. Без ŠæŃ€Š¾Ń†ŠµŃŃƒ, без ŠæŠ¾Ń€Ń‚Ńƒ, а всі метоГи вище ŠæŃ€Š°Ń†ŃŽŃŽŃ‚ŃŒ так само. -Даме Š“Š»Ń Ń†ŃŒŠ¾Š³Š¾ є оГин ŠæŃ€Š°ŠæŠ¾Ń€ŠµŃ†ŃŒ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°: `Client(mcp, raise_exceptions=True)`. Він Гіє лише на Š·'Ń”Š“Š½Š°Š½Š½ŃŃ… у пам'яті, а ŠæŠ¾ŃŃŠ½ŃŽŃ” його й Š±ŃƒŠ“ŃƒŃ” навколо нього весь піГхіГ сторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)**. +Даме Š“Š»Ń Ń†ŃŒŠ¾Š³Š¾ є оГин ŠæŃ€Š°ŠæŠ¾Ń€ŠµŃ†ŃŒ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°: `Client(mcp, raise_exceptions=True)`. Він Гіє лише на Š·'Ń”Š“Š½Š°Š½Š½ŃŃ… у межах ŠæŃ€Š¾Ń†ŠµŃŃƒ, а ŠæŠ¾ŃŃŠ½ŃŽŃ” його й Š±ŃƒŠ“ŃƒŃ” навколо нього весь піГхіГ сторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)**. ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} -* `Client(x)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в пам'яті Го об'єкта сервера, через Streamable HTTP — Го Ń€ŃŠ“ŠŗŠ° Š· URL і через транспорт — Го всього Ń–Š½ŃˆŠ¾Š³Š¾. +* `Client(x)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ через Streamable HTTP Го Ń€ŃŠ“ŠŗŠ° Š· URL, Š·Š°ŠæŃƒŃŠŗŠ°Ń” піГпроцес Š“Š»Ń `StdioServerParameters`, Š²Ń…Š¾Š“ŠøŃ‚ŃŒ у транспорт Š½Š°ŠæŃ€ŃŠ¼Ńƒ, а в тестах приймає сам об'єкт сервера. * `async with` — це весь життєвий цикл. УсереГині нього `server_capabilities` і `protocol_version` уже заповнені; `server_info` та `instructions` — теж, ŃŠŗŃ‰Š¾ сервер їх наГає. * `list_tools()` Гає `name`, `title`, `description` та `input_schema` кожного Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°. * `call_tool()` повертає `content` Š“Š»Ń моГелі, `structured_content` Š“Š»Ń вашого коГу та `is_error`. Š†Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, що викиГає Š²ŠøŠ½ŃŃ‚Š¾Šŗ, — це Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚, а не Š²ŠøŠ½ŃŃ‚ок. diff --git a/i18n/uk/pages/client/oauth-clients.md b/i18n/uk/pages/client/oauth-clients.md index e39d2184..bf3e8726 100644 --- a/i18n/uk/pages/client/oauth-clients.md +++ b/i18n/uk/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth-клієнти {#oauth-clients} @@ -81,18 +81,20 @@ translation: Коли `Client` ŃƒŠæŠµŃ€ŃˆŠµ наГсилає запит, сервер віГповіГає `401`. Далі ŃŠæŃ€Š°Š²Ńƒ бере на себе провайГер: -1. **Š’ŠøŃŠ²Š»ŠµŠ½Š½Ń.** Він читає заголовок `WWW-Authenticate`, Š·Š°Š²Š°Š½Ń‚Š°Š¶ŃƒŃ” Protected Resource Metadata сервера Š· `/.well-known/oauth-protected-resource`, Š“Ń–Š·Š½Š°Ń”Ń‚ŃŒŃŃ, ŃŠŗŠøŠ¹ сервер авторизації захищає цей Ń€ŠµŃŃƒŃ€Ń, і Š·Š°Š²Š°Š½Ń‚Š°Š¶ŃƒŃ” метаГані *того* сервера. +1. **Š’ŠøŃŠ²Š»ŠµŠ½Š½Ń.** Він читає заголовок `WWW-Authenticate`, Š·Š°Š²Š°Š½Ń‚Š°Š¶ŃƒŃ” Protected Resource Metadata сервера Š· `/.well-known/oauth-protected-resource`, Š“Ń–Š·Š½Š°Ń”Ń‚ŃŒŃŃ, ŃŠŗŠøŠ¹ сервер авторизації захищає цей Ń€ŠµŃŃƒŃ€Ń, і Š·Š°Š²Š°Š½Ń‚Š°Š¶ŃƒŃ” метаГані *того* сервера. (Š”Ń‚Š°Ń€Ń–ŃˆŠøŠ¹ сервер, що не ŠæŃƒŠ±Š»Ń–ŠŗŃƒŃ” метаГаних Ń€ŠµŃŃƒŃ€ŃŃƒ, Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ Š·Š°ŠæŠøŃ‚ŃƒŃŽŃ‚ŃŒ про метаГані сервера авторизації за його власним origin.) Š£ буГь-ŃŠŗŠ¾Š¼Ńƒ разі метаГані Š¼Š°ŃŽŃ‚ŃŒ називати своїм `issuer` той сервер, Š“Š»Ń ŃŠŗŠ¾Š³Š¾ їх завантажено; усе Ń–Š½ŃˆŠµ Š²Ń–Š“Ń…ŠøŠ»ŃŃ”Ń‚ŃŒŃŃ. 2. **Š ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–Ń.** Š£ сховищі ŠæŠ¾Ń€Š¾Š¶Š½ŃŒŠ¾? Він Гинамічно Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŃ” вас Ń–Š· вашими `OAuthClientMetadata` і зберігає Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. 3. **ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń.** Він Š³ŠµŠ½ŠµŃ€ŃƒŃ” ŠæŠ°Ń€Ńƒ PKCE і `state`, Š±ŃƒŠ“ŃƒŃ” URL авторизації, Š¾Ń‡Ń–ŠŗŃƒŃ” ваш `redirect_handler`, а потім — ваш `callback_handler`, щоб отримати коГ. 4. **ŠžŠ±Š¼Ń–Š½.** Він Š¼Ń–Š½ŃŃ” коГ на `OAuthToken`, зберігає його й ŠæŠ¾Š²Ń‚Š¾Ń€ŃŽŃ” ваш початковий запит Ń–Š· `Authorization: Bearer ...`. ŠŸŃ–ŃŠ»Ń Ń†ŃŒŠ¾Š³Š¾ все тихо. Токени Š±ŠµŃ€ŃƒŃ‚ŃŒŃŃ Š·Ń– сховища, прострочений токен Š“Š¾ŃŃ‚ŃƒŠæŃƒ Š¾Š½Š¾Š²Š»ŃŽŃ”Ń‚ŃŒŃŃ за Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ токена Š¾Š½Š¾Š²Š»ŠµŠ½Š½Ń, і лише коли ніщо Š· Ń†ŃŒŠ¾Š³Š¾ не ŃŠæŃ€Š°Ń†ŃŒŠ¾Š²ŃƒŃ”, провайГер Š·Š°ŠæŃƒŃŠŗŠ°Ń” потік знову. +Š”Š»Ń всіх цих запитів Гіє оГне транспортне правило: ŃŠŗ і MCP-запит, ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ń– ŃŠŗŠ¾Š³Š¾ вони Š²ŠøŠŗŠ¾Š½ŃƒŃŽŃ‚ŃŒŃŃ, вони ŠæŠµŃ€ŠµŃ…Š¾Š“ŃŃ‚ŃŒ за ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½ŃŠ¼ лише тоГі, коли воно Š»ŠøŃˆŠ°Ń”Ń‚ŃŒŃŃ на Ń‚Š¾Š¼Ńƒ самому origin і зберігає метоГ (скажімо, 307/308 на ŠŗŃ–Š½Ń†ŠµŠ²Ńƒ ŃŠŗŃ–ŃŠ½Ńƒ Ń€ŠøŃŠŗŃƒ), а буГь-ŃŠŗŠµ Ń–Š½ŃˆŠµ ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń Ń‚Ń€Š°ŠŗŃ‚ŃƒŃŽŃ‚ŃŒ так, ніби цей URL не віГповіГає. + ŠŃ–Ń‡Š¾Š³Š¾ Š· Ń†ŃŒŠ¾Š³Š¾ ви не писали. Š›ŠøŃˆŠ°ŃŽŃ‚ŃŒŃŃ ще Гва іменовані Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Šø (`client_metadata_url` і `validate_resource_url`), і Ń†ŃŒŠ¾Š¼Ńƒ Ń„Š°Š¹Š»Ńƒ не потрібен жоГен. Знати варто про `client_metadata_url`; йому ŠæŃ€ŠøŃŠ²ŃŃ‡ŠµŠ½Š¾ окремий розГіл нижче. ### Š”ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ самі {#try-it} -Š‘Ń–Š»ŃŒŃˆŃ–ŃŃ‚ŃŒ приклаГів у цій Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†Ń–Ń— можна перевірити за Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ `Client(server)` у пам'яті. Цей — ні: уся суть ŠæŠ¾Ń‚Š¾ŠŗŃƒ в HTTP-віГповіГі `401`, а між клієнтом у пам'яті та його сервером HTTP немає. +`Client(server)` у пам'яті, ŃŠŗŠøŠ¼ ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŃŽŃ‚ŃŒŃŃ Š²Š°ŃˆŃ– тести, тут не Гопоможе: уся суть ŠæŠ¾Ń‚Š¾ŠŗŃƒ в HTTP-віГповіГі `401`, а між клієнтом у пам'яті та його сервером HTTP немає. Š£ репозиторії є жива Š²ŠµŃ€ŃŃ–я. `examples/servers/simple-auth/` Š·Š°ŠæŃƒŃŠŗŠ°Ń” окремий сервер авторизації та захищений MCP-сервер; `examples/clients/simple-auth-client/` — це клієнт Ń–Š· цієї сторінки, що виріс у невеликий CLI. Š£ його README є Гві команГи: Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ сервери, Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ клієнт проти них — і спостерігайте, ŃŠŗ ŠæŃ€Š¾Ń…Š¾Š“ŃŃ‚ŃŒ усі чотири кроки. @@ -110,13 +112,14 @@ URL має Š±ŃƒŃ‚Šø HTTPS Ń–Š· некореневим ŃˆŠ»ŃŃ…Š¾Š¼; усе ін `ClientCredentialsOAuthProvider` — це той самий `httpx2.Auth`, Ń‚Ń–Š»ŃŒŠŗŠø без Š»ŃŽŠ“ини: -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` Що Š·Š¼Ń–Š½ŠøŠ»Š¾ŃŃ: * ŠŠµŠ¼Š°Ń” `OAuthClientMetadata`, немає обробників. Š’Šø переГаєте `client_id` і `client_secret`; провайГер Š±ŃƒŠ“ŃƒŃ” навколо них Š¼Ń–Š½Ń–Š¼Š°Š»ŃŒŠ½Ńƒ Ń€ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–ŃŽ `client_credentials` і ŠæŠ¾Š²Š½Ń–ŃŃ‚ŃŽ ŠæŃ€Š¾ŠæŃƒŃŠŗŠ°Ń” Š“ŠøŠ½Š°Š¼Ń–Ń‡Š½Ńƒ Ń€ŠµŃ”ŃŃ‚Ń€Š°Ń†Ń–ŃŽ. +* `issuer` називає сервер авторизації, що виГав ці облікові Гані; Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ Š·Š½Š°Ń‡ŠµŠ½Š½Ń `issuer`, ŃŠŗŠµ повертає його Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ `/.well-known/oauth-authorization-server`. Š’ŠøŃŠ²Š»ŠµŠ½Š½Ń й Галі Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ, ŃŠŗ описано вище, але запити на токен Š±ŃƒŠ“ŃƒŃŽŃ‚ŃŒŃŃ лише Š· метаГаних *Ń†ŃŒŠ¾Š³Š¾* Š²ŠøŠ“Š°Š²Ń†Ń; ŃŠŗŃ‰Š¾ MCP-сервер ŃƒŠŗŠ°Š·ŃƒŃ” куГись інГе, потік Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ Š·ŃƒŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ Š· `OAuthFlowError`. ŠŸŃ€Š¾ŠæŃƒŃŠŗŠ°Ń‚Šø його — застаріла практика, і у версії 3.0 він стане обов'ŃŠ·ŠŗŠ¾Š²ŠøŠ¼ (Гив. **[Застарілі можливості](../deprecated.md#deprecated-sdk-helpers)**); Готи провайГер попереГжає й Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” той сервер авторизації, ŃŠŗŠøŠ¹ знайГе Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń. * `scope` — це Ń€ŃŠ“Š¾Šŗ, розГілений пробілами: такий формат OAuth Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” в переГаних Ганих. * Усе Галі іГентичне: той самий `TokenStorage`, той самий `httpx2.AsyncClient(auth=...)`, той самий `streamable_http_client`. @@ -129,7 +132,7 @@ URL має Š±ŃƒŃ‚Šø HTTPS Ń–Š· некореневим ŃˆŠ»ŃŃ…Š¾Š¼; усе ін Ще оГин провайГер живе в `mcp.client.auth.extensions.client_credentials`: **`PrivateKeyJWTOAuthProvider`** — Š“Š»Ń клієнтів, ŃŠŗŃ– Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃŽŃ‚ŃŒŃŃ за Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ JWT Š·Š°Š¼Ń–ŃŃ‚ŃŒ ŃŠæŃ–Š»ŃŒŠ½Š¾Š³Š¾ ŃŠµŠŗŃ€ŠµŃ‚Ńƒ (`private_key_jwt`, варіант Ń–Š· ŠæŠ°Ń€Š¾ŃŽ ŠŗŠ»ŃŽŃ‡Ń–Š² та workload identity). Він Š“Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ”Ń‚ŃŒŃŃ - тієї ж схеми: ŃŃ‚Š²Š¾Ń€Ń–Ń‚ŃŒ ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ і переГайте в `auth=`. Š£ Ń‚Š¾Š¼Ńƒ ж Š¼Š¾Š“ŃƒŠ»Ń– є + тієї ж схеми: ŃŃ‚Š²Š¾Ń€Ń–Ń‚ŃŒ ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€ (він приймає той самий необов'ŃŠ·ŠŗŠ¾Š²ŠøŠ¹ `issuer`) і переГайте в `auth=`. Š£ Ń‚Š¾Š¼Ńƒ ж Š¼Š¾Š“ŃƒŠ»Ń– є `SignedJWTParameters` і `static_assertion_provider` — Гва Гопоміжні засоби, що Š±ŃƒŠ“ŃƒŃŽŃ‚ŃŒ Š“Š»Ń нього assertion. Š„ ще оГна ŃŠøŃ‚ŃƒŠ°Ń†Ń–Ń без Š»ŃŽŠ“ини: клієнт Š½Š°Š»ŠµŠ¶ŠøŃ‚ŃŒ ŠæŃ–Š“ŠæŃ€ŠøŃ”Š¼ŃŃ‚Š²Ńƒ, Ге провайГер іГентичності, а не ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡, Š²ŠøŃ€Ń–ŃˆŃƒŃ”, Го ŃŠŗŠøŃ… MCP-серверів він може Š·Š²ŠµŃ€Ń‚Š°Ń‚ŠøŃŃ. Це Ń–Š½ŃˆŠøŠ¹ грант Ń–Š· Š²Š»Š°ŃŠ½Š¾ŃŽ Š¼Š¾Š“ŠµŠ»Š»ŃŽ Говіри та Š²Š»Š°ŃŠ½Š¾ŃŽ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŠ¾ŃŽ — **[Š¢Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń про Ń–Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ](identity-assertion.md)**. diff --git a/i18n/uk/pages/client/session-groups.md b/i18n/uk/pages/client/session-groups.md index db219633..4e4f14e9 100644 --- a/i18n/uk/pages/client/session-groups.md +++ b/i18n/uk/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # Š“Ń€ŃƒŠæŠø сесій {#session-groups} @@ -78,7 +78,7 @@ translation: ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} * `ClientSessionGroup` тримає багато Š·'Ń”Š“Š½Š°Š½ŃŒ Ń–Š· серверами й Š·Š²Š¾Š“ŠøŃ‚ŃŒ їхні Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø, Ń€ŠµŃŃƒŃ€ŃŠø й промпти в оГин `dict` кожного виГу. -* `connect_to_server(params)` Š“Š»Ń кожного сервера. Він приймає параметри Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ й ніколи — об'єкт сервера чи URL, ŃŠŗ `Client`. +* `connect_to_server(params)` Š“Š»Ń кожного сервера. Він приймає параметри Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ й ніколи — URL чи `Transport`, ŃŠŗŃ– приймає `Client`. * `group.call_tool(name, arguments)` сам ŃŠæŃ€ŃŠ¼Š¾Š²ŃƒŃ” виклик на сервер-власник. * Імена Š¼Š°ŃŽŃ‚ŃŒ Š±ŃƒŃ‚Šø ŃƒŠ½Ń–ŠŗŠ°Š»ŃŒŠ½ŠøŠ¼Šø в межах усієї Š³Ń€ŃƒŠæŠø; Гва сервери Š· Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š¾Š¼ `search` самі по собі ŃŠæŃ–Š²Ń–ŃŠ½ŃƒŠ²Š°Ń‚Šø не Š¼Š¾Š¶ŃƒŃ‚ŃŒ. * `component_name_hook=` ŠæŠµŃ€ŠµŠæŠøŃŃƒŃ” кожне зареєстроване ім'я. Š—Š¼Ń–Š½ŃŽŃ”Ń‚ŃŒŃŃ ŠŗŠ»ŃŽŃ‡ словника, а не ім'я в переГаних Ганих. diff --git a/i18n/uk/pages/client/transports.md b/i18n/uk/pages/client/transports.md index 97ed0544..17aab3a8 100644 --- a/i18n/uk/pages/client/transports.md +++ b/i18n/uk/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # Транспорти клієнта {#client-transports} @@ -11,30 +11,15 @@ translation: *Дерверний* бік кожного Š· них (що Ń€Š¾Š±ŠøŃ‚ŃŒ `mcp.run()` і що ви розгортаєте) описано на сторінці **[Š—Š°ŠæŃƒŃŠŗ сервера](../run/index.md)**. -## Š£ пам'яті {#in-memory} - -ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ сам об'єкт сервера: - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ЖоГного ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ, жоГного ŠæŠ¾Ń€Ń‚Ńƒ, жоГних байтів у мережі. ŠšŠ»Ń–Ń”Š½Ń‚ і сервер — це Гва об'єкти в оГному процесі, а виклик усе оГно ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ через справжній ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»ŃŒŠ½ŠøŠ¹ Ń€Ń–Š²ŠµŠ½ŃŒ: `search_books` ŠæŠµŃ€ŠµŠ»Ń–Ń‡ŃƒŃ”Ń‚ŃŒŃŃ, Š²Š°Š»Ń–Š“ŃƒŃ”Ń‚ŃŒŃŃ й Š²ŠøŠŗŠ»ŠøŠŗŠ°Ń”Ń‚ŃŒŃŃ Ń‚Š¾Ń‡Š½Ń–ŃŃ–Š½ŃŒŠŗŠ¾ так само, ŃŠŗ це було б через HTTP. - -Тож це Š¾Š“Ń€Š°Š·Ńƒ Гві речі: - -* **Тестовий стенГ.** Кожен приклаГ у цій Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†Ń–Ń— ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ”Ń‚ŃŒŃŃ саме так, а сторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)** Š±ŃƒŠ“ŃƒŃ” Говкола Ń†ŃŒŠ¾Š³Š¾ весь піГхіГ. -* **API Š“Š»Ń Š²Š±ŃƒŠ“Š¾Š²ŃƒŠ²Š°Š½Š½Ń.** Š—Š°ŃŃ‚Š¾ŃŃƒŠ½ŠŗŃƒ, ŃŠŗŠøŠ¹ сам ŃŃ‚Š²Š¾Ń€ŃŽŃ” сервер, не потрібен мережевий перехіГ, щоб викликати його Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø. - ## Streamable HTTP {#streamable-http} -ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ Ń€ŃŠ“Š¾Šŗ Š· URL — і отримаєте **Streamable HTTP**, транспорт, за ŃŠŗŠøŠ¼ Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°ŃŽŃ‚ŃŒ сервер: +ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ Ń€ŃŠ“Š¾Šŗ Š· URL — і отримаєте **Streamable HTTP**, транспорт, за ŃŠŗŠøŠ¼ Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°ŃŽŃ‚ŃŒ сервер і Го ŃŠŗŠ¾Š³Š¾ варто Š·Š²ŠµŃ€Ń‚Š°Ń‚ŠøŃŃ насампереГ: ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -ŠžŃ†Šµ й увесь ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, Š½Š°Š»Š°ŃˆŃ‚Š¾Š²Š°Š½Š¾Š³Š¾ так, ŃŠŗ потрібно MCP: `follow_redirects=True`, 30-секунГний тайм-Š°ŃƒŃ‚ на connect/write/pool і 300-секунГний тайм-Š°ŃƒŃ‚ на Ń‡ŠøŃ‚Š°Š½Š½Ń, бо сервер може тримати потік віГповіГі віГкритим. +ŠžŃ†Šµ й увесь ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½-клієнт. `Client` сам загортає URL у `streamable_http_client(...)` поверх `httpx2.AsyncClient`, Š½Š°Š»Š°ŃˆŃ‚Š¾Š²Š°Š½Š¾Š³Š¾ так, ŃŠŗ потрібно MCP: 30-секунГний тайм-Š°ŃƒŃ‚ на connect/write/pool і 300-секунГний тайм-Š°ŃƒŃ‚ на Ń‡ŠøŃ‚Š°Š½Š½Ń, бо сервер може тримати потік віГповіГі віГкритим. !!! check Дтворений `Client` ще **не** піГ'єГнаний. ŠšŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€ лише обирає транспорт; @@ -50,7 +35,7 @@ translation: Щойно Š·Š½Š°Š“Š¾Š±ŠøŃ‚ŃŒŃŃ заголовок `Authorization`, cookie, проксі, mTLS чи Ń–Š½ŃˆŠøŠ¹ тайм-Š°ŃƒŃ‚ — ŃŃ‚Š²Š¾Ń€Ń–Ń‚ŃŒ `httpx2.AsyncClient` самостійно й переГайте його в `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -80,9 +65,30 @@ translation: !!! info `httpx2` зберігає знайомий API `httpx`, тож ŃŠŗŃ‰Š¾ ви знаєте `httpx`, то вже вмієте робити тут Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŠ°Ń†Ń–ŃŽ, проксі, Ń…ŃƒŠŗŠø поГій, повторні спроби й Š¾Š±Š¼ŠµŠ¶ŠµŠ½Š½Ń Š·'Ń”Š“Š½Š°Š½ŃŒ. SDK нічого не ГоГає Š·Š²ŠµŃ€Ń…Ńƒ й нічого не - забирає. Даме ŃŃŽŠ“Šø піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ й OAuth: + забирає, окрім [обробки ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½ŃŒ](#redirects). Даме ŃŃŽŠ“Šø піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ й OAuth: `httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Увесь цей процес описано на сторінці **[OAuth-клієнти](oauth-clients.md)**. +### ŠŸŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń {#redirects} + +Транспорт піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Го URL, ŃŠŗŠøŠ¹ ви йому переГали, і лише Го Ń†ŃŒŠ¾Š³Š¾ origin. + +* ŠŸŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń `307`/`308`, що Š»ŠøŃˆŠ°Ń”Ń‚ŃŒŃŃ в межах тієї самої схеми, хоста й ŠæŠ¾Ń€Ń‚Ńƒ, Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ; так само й `http://` → `https://` на Ń‚Š¾Š¼Ńƒ самому хості. Це покриває звичне ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń Š· ŠŗŃ–Š½Ń†ŠµŠ²Š¾ŃŽ ŃŠŗŃ–ŃŠ½Š¾ŃŽ Ń€ŠøŃŠŗŠ¾ŃŽ `/mcp` → `/mcp/`. +* ŠŸŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń буГь-куГи інГе **не** Š²ŠøŠŗŠ¾Š½ŃƒŃ”Ń‚ŃŒŃŃ. Виклик Š·Š°Š²ŠµŃ€ŃˆŃƒŃ”Ń‚ŃŒŃŃ ŠæŠ¾Š¼ŠøŠ»ŠŗŠ¾ŃŽ: + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + Якщо цей URL — сервер, ŃŠŗŠøŠ¹ ви мали на ŃƒŠ²Š°Š·Ń–, Š²ŠæŠøŃˆŃ–Ń‚ŃŒ його в ŠŗŠ¾Š½Ń„Ń–Š³ŃƒŃ€Š°Ń†Ń–ŃŽ. Якщо ні — сервер або проксі переГ ним Š½Š°Š»Š°ŃˆŃ‚овано Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾. + +Це ŃŃ‚Š¾ŃŃƒŃ”Ń‚ŃŒŃŃ буГь-ŃŠŗŠ¾Š³Š¾ `httpx2.AsyncClient`, ŃŠŗŠøŠ¹ ви переГаєте: його Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń `follow_redirects` Š“Š»Ń MCP-запитів не Š²Ń€Š°Ń…Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ — в жоГен бік. OAuth-провайГери SDK Š·Š°ŃŃ‚Š¾ŃŠ¾Š²ŃƒŃŽŃ‚ŃŒ те саме правило Го власних запитів. + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` означає, що + сервер ŃŃ‚Š¾Ń—Ń‚ŃŒ за проксі Š· Ń‚ŠµŃ€Š¼Ń–Š½Š°Ń†Ń–Ń”ŃŽ TLS, про ŃŠŗŠøŠ¹ не знає, і виГає ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń на `http://`. + Це Š²ŠøŠæŃ€Š°Š²Š»ŃŃŽŃ‚ŃŒ на сервері (**[Š Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń та Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŠ²Š°Š½Š½Ń](../run/deploy.md#behind-a-tls-terminating-proxy)**) + або Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½ŃŠ¼ точного URL `https://…/`, ŃŠŗŠøŠ¹ ŠæŃ–Š“ŠŗŠ°Š·ŃƒŃ” ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń. + ## stdio {#stdio} Дервер **stdio** — це піГпроцес. ŠšŠ»Ń–Ń”Š½Ń‚ Š·Š°ŠæŃƒŃŠŗŠ°Ń” його, пише JSON-RPC в його stdin і читає JSON-RPC Š· його stdout. Даме так Гесктопний хост Š·Š°ŠæŃƒŃŠŗŠ°Ń” сервер на Š²Š°ŃˆŃ–й Š¼Š°ŃˆŠøŠ½Ń–: хост і *є* цим коГом ŠæŠ»ŃŽŃ UI, а сторінка **[ŠŸŃ–Š“'Ń”Š“Š½Š°Š½Š½Ń Го ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ хоста](../get-started/real-host.md)** ŠæŠ¾ŠŗŠ°Š·ŃƒŃ” ті самі ŃŃ‚Š¾ŃŃƒŠ½ŠŗŠø Š· боку хоста — ŃŠŗ ŠŗŠ¾Š½Ń„Ń–Š³ŃƒŃ€Š°Ń†Ń–Š¹Š½ŠøŠ¹ файл. @@ -105,6 +111,18 @@ stderr Š“Š¾Ń‡Ń–Ń€Š½ŃŒŠ¾Š³Š¾ ŠæŃ€Š¾Ń†ŠµŃŃƒ йГе у ваш. Щоб ŃŠæŃ€ŃŠ¼Ńƒ Дервер, ŃŠŗŠ¾Š¼Ńƒ потрібен ŠŗŠ»ŃŽŃ‡ API, там його не знайГе. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ його ŃŠ²Š½Š¾ через `env=`; ці змінні Š½Š°ŠŗŠ»Š°Š“Š°ŃŽŃ‚ŃŒŃŃ поверх списку Гозволених. Даме це й Ń€Š¾Š±ŠøŃ‚ŃŒ `BOOKSHOP_API_KEY` вище. +## Š£ пам'яті {#in-memory} + +Š£ тесті нема чого розгортати й нема чого Š·Š°ŠæŃƒŃŠŗŠ°Ń‚Šø. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ сам об'єкт сервера: + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ЖоГного ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ, жоГного ŠæŠ¾Ń€Ń‚Ńƒ, жоГних байтів у мережі. ŠšŠ»Ń–Ń”Š½Ń‚ і сервер — це Гва об'єкти в оГному процесі, а виклик усе оГно ŠæŃ€Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ через справжній ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»ŃŒŠ½ŠøŠ¹ Ń€Ń–Š²ŠµŠ½ŃŒ: `search_books` ŠæŠµŃ€ŠµŠ»Ń–Ń‡ŃƒŃ”Ń‚ŃŒŃŃ, Š²Š°Š»Ń–Š“ŃƒŃ”Ń‚ŃŒŃŃ й Š²ŠøŠŗŠ»ŠøŠŗŠ°Ń”Ń‚ŃŒŃŃ Ń‚Š¾Ń‡Š½Ń–ŃŃ–Š½ŃŒŠŗŠ¾ так само, ŃŠŗ це було б через HTTP. Дторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)** Š±ŃƒŠ“ŃƒŃ” Говкола Ń†ŃŒŠ¾Š³Š¾ весь піГхіГ. + +Š¦Ń сама форма воГночас ŃŠ»ŃƒŠ³ŃƒŃ” API Š“Š»Ń Š²Š±ŃƒŠ“Š¾Š²ŃƒŠ²Š°Š½Š½Ń: Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, ŃŠŗŠøŠ¹ сам ŃŃ‚Š²Š¾Ń€ŃŽŃ” сервер, може викликати його Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø без мережевого ŠæŠµŃ€ŠµŃ…Š¾Š“Ńƒ. + ## SSE {#sse} `sse_client(url)` Š· `mcp.client.sse` — це HTTP-транспорт, на Š·Š¼Ń–Š½Ńƒ ŃŠŗŠ¾Š¼Ńƒ ŠæŃ€ŠøŠ¹ŃˆŠ¾Š² Streamable HTTP. Загортайте його так само, `Client(sse_client("http://localhost:8000/sse"))`, щоб говорити Ń–Š· сервером, ŃŠŗŠøŠ¹ Госі ним ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŃ”Ń‚ŃŒŃŃ, — і не Š±ŃƒŠ“ŃƒŠ¹Ń‚Šµ на ньому нічого нового. @@ -113,15 +131,16 @@ stderr Š“Š¾Ń‡Ń–Ń€Š½ŃŒŠ¾Š³Š¾ ŠæŃ€Š¾Ń†ŠµŃŃƒ йГе у ваш. Щоб ŃŠæŃ€ŃŠ¼Ńƒ Š”Š»Ń `Client` усе перелічене вище — оГне й те саме. -**Транспорт** — це буГь-ŃŠŗŠøŠ¹ асинхронний контекстний менеГжер, що повертає ŠæŠ°Ń€Ńƒ потоків ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½ŃŒ `(read, write)`: Ń„Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾ — протокол `Transport` у `mcp.client`. `Client` розв'ŃŠ·ŃƒŃ” свій Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ за типом: об'єкт сервера піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в межах ŠæŃ€Š¾Ń†ŠµŃŃƒ, `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, а в буГь-що Ń–Š½ŃˆŠµ він Š²Ń…Š¾Š“ŠøŃ‚ŃŒ Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ ŃŠŗ у транспорт. Даме Š·Š°Š²Š“ŃŠŗŠø Š¾ŃŃ‚Š°Š½Š½ŃŒŠ¾Š¼Ńƒ ŠæŃ€Š°Š²ŠøŠ»Ńƒ `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` ŃŃ‚Š°ŃŽŃ‚ŃŒ на оГне й те саме місце — і саме Ń‚Š¾Š¼Ńƒ можна написати власний. +**Транспорт** — це буГь-ŃŠŗŠøŠ¹ асинхронний контекстний менеГжер, що повертає ŠæŠ°Ń€Ńƒ потоків ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½ŃŒ `(read, write)`: Ń„Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾ — протокол `Transport` у `mcp.client`. `Client` розв'ŃŠ·ŃƒŃ” свій Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ за типом: `str` стає `streamable_http_client(url)`, `StdioServerParameters` стає `stdio_client(params)`, об'єкт сервера піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в межах ŠæŃ€Š¾Ń†ŠµŃŃƒ, а в буГь-що Ń–Š½ŃˆŠµ він Š²Ń…Š¾Š“ŠøŃ‚ŃŒ Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ ŃŠŗ у транспорт. Даме Š·Š°Š²Š“ŃŠŗŠø Š¾ŃŃ‚Š°Š½Š½ŃŒŠ¾Š¼Ńƒ ŠæŃ€Š°Š²ŠøŠ»Ńƒ `stdio_client(...)`, `streamable_http_client(...)` і `sse_client(...)` ŃŃ‚Š°ŃŽŃ‚ŃŒ на оГне й те саме місце — і саме Ń‚Š¾Š¼Ńƒ можна написати власний. ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} -* `Client(mcp)` (об'єкт сервера) піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в пам'яті. Š’ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ Š“Š»Ń тестів і Š“Š»Ń Š²Š±ŃƒŠ“Š¾Š²ŃƒŠ²Š°Š½Š½Ń. * `Client("http://.../mcp")` (URL) піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ через Streamable HTTP, ŠæŃ€Š¾Š“Š°ŠŗŃˆŠµŠ½-транспорт. * Заголовки, Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŠ°Ń†Ń–Ń, проксі й тайм-Š°ŃƒŃ‚Šø Š½Š°Š»ŠµŠ¶Š°Ń‚ŃŒ `httpx2.AsyncClient`, ŃŠŗŠøŠ¹ ви переГаєте в `streamable_http_client(url, http_client=...)`. Іменованого Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ńƒ `headers=` немає. +* ŠŸŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń Š²ŠøŠŗŠ¾Š½ŃƒŃŽŃ‚ŃŒŃŃ лише в межах власного origin Ń†ŃŒŠ¾Š³Š¾ URL (`307`/`308` Ń–Š· ŠŗŃ–Š½Ń†ŠµŠ²Š¾ŃŽ ŃŠŗŃ–ŃŠ½Š¾ŃŽ Ń€ŠøŃŠŗŠ¾ŃŽ), ŠæŠ»ŃŽŃ `http`→`https` на Ń‚Š¾Š¼Ńƒ самому хості. Усе Ń–Š½ŃˆŠµ Š·Š°Š²ŠµŃ€ŃˆŃƒŃ”Ń‚ŃŒŃŃ ŠæŠ¾Š¼ŠøŠ»ŠŗŠ¾ŃŽ `Redirect to … not followed`; ŠæŃ€Š¾ŠæŠøŃˆŃ–Ń‚ŃŒ у ŠŗŠ¾Š½Ń„Ń–Š³ŃƒŃ€Š°Ń†Ń–Ń— кінцевий URL. * stdio — це `Client(StdioServerParameters(...))`. Загортайте його в `stdio_client(...)` самостійно лише Š“Š»Ń того, щоб перенаправити stderr Š“Š¾Ń‡Ń–Ń€Š½ŃŒŠ¾Š³Š¾ ŠæŃ€Š¾Ń†ŠµŃŃƒ. * ŠŸŃ–Š“ŠæŃ€Š¾Ń†ŠµŃ Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” сереГовище Š·Ń– списку Гозволених, а не ваше; `env=` його Š“Š¾ŠæŠ¾Š²Š½ŃŽŃ”. +* `Client(mcp)` (об'єкт сервера) піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в пам'яті. Š’ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ в тестах або щоб Š²Š±ŃƒŠ“ŃƒŠ²Š°Ń‚Šø сервер у Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, ŃŠŗŠøŠ¹ його створив. * Транспорт — це буГь-що, Š· чим можна зробити `async with x as (read, write)`. Усе, що не є об'єктом сервера, URL чи `StdioServerParameters`, `Client` переГає ŠæŃ€ŃŠ¼Š¾ Ń†ŃŒŠ¾Š¼Ńƒ ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. * Š”Ń‚Š²Š¾Ń€ŠµŠ½Š½Ń `Client` обирає транспорт. `async with` його віГкриває. diff --git a/i18n/uk/pages/deprecated.md b/i18n/uk/pages/deprecated.md index c079c516..08e50718 100644 --- a/i18n/uk/pages/deprecated.md +++ b/i18n/uk/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # Застарілі можливості {#deprecated-features} -Š”ŠæŠµŃ†ŠøŃ„Ń–ŠŗŠ°Ń†Ń–Ń 2026-07-28 Š²ŠøŠ²Š¾Š“ŠøŃ‚ŃŒ Š· ŃƒŠ¶ŠøŃ‚ŠŗŃƒ Šæ'ять речей. SDK і Галі Ń€ŠµŠ°Š»Ń–Š·ŃƒŃ” кожну Š· них, і кожна тепер ŃŃƒŠæŃ€Š¾Š²Š¾Š“Š¶ŃƒŃ”Ń‚ŃŒŃŃ **ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶ŠµŠ½Š½ŃŠ¼ про Š·Š°ŃŃ‚Š°Ń€Ń–Š»Ń–ŃŃ‚ŃŒ**. ŠžŠ“ŠøŠ½ Гопоміжний метоГ SDK оголошено застарілим окремо, і його навеГено [наприкінці сторінки](#deprecated-sdk-helpers). +Š”ŠæŠµŃ†ŠøŃ„Ń–ŠŗŠ°Ń†Ń–Ń 2026-07-28 Š²ŠøŠ²Š¾Š“ŠøŃ‚ŃŒ Š· ŃƒŠ¶ŠøŃ‚ŠŗŃƒ Šæ'ять речей. SDK і Галі Ń€ŠµŠ°Š»Ń–Š·ŃƒŃ” кожну Š· них, і кожна тепер ŃŃƒŠæŃ€Š¾Š²Š¾Š“Š¶ŃƒŃ”Ń‚ŃŒŃŃ **ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶ŠµŠ½Š½ŃŠ¼ про Š·Š°ŃŃ‚Š°Ń€Ń–Š»Ń–ŃŃ‚ŃŒ**. ŠšŃ–Š»ŃŒŠŗŠ° речей оголошено застарілими на рівні самого SDK, незалежно віГ специфікації; їх навеГено [наприкінці сторінки](#deprecated-sdk-helpers). Š¢Š°Š±Š»ŠøŃ†Ń нижче називає кожну Š·Š°ŃŃ‚Š°Ń€Ń–Š»Ńƒ Š¼Š¾Š¶Š»ŠøŠ²Ń–ŃŃ‚ŃŒ, ŠæŠ¾ŃŃŠ½ŃŽŃ”, Ń‡Š¾Š¼Ńƒ вона зникає, і Š²ŠŗŠ°Š·ŃƒŃ” Š·Š°Š¼Ń–Š½Ńƒ, на ŃŠŗŃƒ варто ŃŠæŠøŃ€Š°Ń‚ŠøŃŃ. @@ -136,13 +136,15 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ŠžŠ“ŠøŠ½ Ń€ŃŠ“Š¾Šŗ ŠŗŠ¾Š½Ń„Ń–Š³ŃƒŃ€Š°Ń†Ń–Ń— pytest — і застарілий виклик Š±Ń–Š»ŃŒŃˆŠµ ніколи не ŠæŃ€Š¾ŠŗŃ€Š°Š“ŠµŃ‚ŃŒŃŃ назаГ у вашу коГову базу, не ŠæŃ€Š¾Š²Š°Š»ŠøŠ²ŃˆŠø тест. -## Застарілі Гопоміжні метоГи SDK {#deprecated-sdk-helpers} +## Застарілі Гопоміжні засоби SDK {#deprecated-sdk-helpers} -Це не зміни специфікації, а лише Š²Š½ŃƒŃ‚Ń€Ń–ŃˆŠ½Ń– частини SDK, Š“Š»Ń ŃŠŗŠøŃ… є краща заміна. Вони ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶Š°ŃŽŃ‚ŃŒ тим самим `MCPDeprecationWarning` і Š±ŃƒŠ“ŃƒŃ‚ŃŒ Š²ŠøŠ»ŃƒŃ‡ŠµŠ½Ń– у версії 3.0. +Це не зміни специфікації, а лише способи Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń SDK, Š“Š»Ń ŃŠŗŠøŃ… є краща заміна. Вони ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶Š°ŃŽŃ‚ŃŒ тим самим `MCPDeprecationWarning`, а Š²ŠµŃ€ŃŃ–я 3.0 Š²ŠøŠ»ŃƒŃ‡Š°Ń” ŃŃ‚Š°Ń€Ńƒ Ń„Š¾Ń€Š¼Ńƒ. | Застаріле | Що робити Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | `FuncMetadata.validate_arguments()`, а потім `FuncMetadata.call_fn()`. Його викликав лише коГ, що ŠæŃ€Š°Ń†ŃŽŃ” Š· `FuncMetadata` Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ (скажімо, власний піГклас `Tool`). | +| `AuthSettings(resource_server_url=...)` без `validate_token_resource=` | ЗаГайте його: Š· `True` сервер Š²Ń–Š“Ń…ŠøŠ»ŃŃ” bearer-токени, про ŃŠŗŃ– ваш верифікатор не ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŃŃ”, що їх виГано Š“Š»Ń `resource_server_url`; `False` означає, що верифікатор сам ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” Š°ŃƒŠ“ŠøŃ‚Š¾Ń€Ń–ŃŽ токена (Гив. **[ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń](run/authorization.md#a-token-verifier)**). ŠŠµŠ·Š°Š“Š°Š½Šµ Š·Š½Š°Ń‡ŠµŠ½Š½Ń ŠæŠ¾Š²Š¾Š“ŠøŃ‚ŃŒŃŃ ŃŠŗ `False`; у версії 3.0 `True` стане типовим Š·Š½Š°Ń‡ŠµŠ½Š½ŃŠ¼ Ń‰Š¾Ń€Š°Š·Ńƒ, коли заГано `resource_server_url`. | +| `ClientCredentialsOAuthProvider(...)` або `PrivateKeyJWTOAuthProvider(...)` без `issuer=` | ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ `issuer=` Š· Š°Š“Ń€ŠµŃŠ¾ŃŽ сервера авторизації, ŃŠŗŠøŠ¹ виГав облікові Гані (Гив. **[ŠŠ°ŠæŠøŃŠ°Š½Š½Ń OAuth-клієнтів](client/oauth-clients.md#machine-to-machine)**). Без нього MCP-сервер Š²ŠøŃ€Ń–ŃˆŃƒŃ”, ŃŠŗŠøŠ¹ сервер авторизації їх отримає; у версії 3.0 цей іменований Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ стане обов'ŃŠ·ŠŗŠ¾Š²ŠøŠ¼. | ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} @@ -151,7 +153,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * Š—Š°ŃŃ‚Š°Ń€Ń–Š»Ń–ŃŃ‚ŃŒ має рекоменГаційний характер: жоГних змін у переГаних Ганих, усе й Галі ŠæŃ€Š°Ń†ŃŽŃ” Ń–Š· ŃŠµŃŃ–ŃŠ¼Šø Го 2026, а ви Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ”Ń‚Šµ помітне ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶ŠµŠ½Š½Ń `MCPDeprecationWarning` (це `UserWarning`, тож воно ввімкнене за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼). * Š”ŠµŠ¼ŠæŠ»ŃŽŠ²Š°Š½Š½Ń й кореневі каталоги ГоГатково ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃŽŃ‚ŃŒ зворотного каналу, ŃŠŗŠ¾Š³Š¾ ŃŠµŃŃ–Ń 2026-07-28 не має. ŠŠ° ŃŃƒŃ‡Š°ŃŠ½Š¾Š¼Ńƒ Š·'єГнанні вони ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶Š°ŃŽŃ‚ŃŒ, а потім Š²ŠøŠŗŠøŠ“Š°ŃŽŃ‚ŃŒ Š²ŠøŠ½ŃŃ‚Š¾Šŗ. * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` ŠæŃ€ŠøŠ³Š»ŃƒŃˆŃƒŃ” Š²ŃŃŽ ŠŗŠ°Ń‚ŠµŠ³Š¾Ń€Ń–ŃŽ; `"error::mcp.MCPDeprecationWarning"` у pytest ŠæŠµŃ€ŠµŃ‚Š²Š¾Ń€ŃŽŃ” її на провал Ń‚ŠµŃŃ‚Ńƒ. -* ŠžŠ“ŠøŠ½ Гопоміжний метоГ SDK, `FuncMetadata.call_fn_with_arg_validation()`, оголошено застарілим окремо — його Š²ŠøŠ»ŃƒŃ‡Š°Ń‚ŃŒ у версії 3.0. +* [Застарілі засоби на рівні SDK](#deprecated-sdk-helpers) ŠæŃ–Š“ŠæŠ¾Ń€ŃŠ“ŠŗŠ¾Š²ŃƒŃŽŃ‚ŃŒŃŃ Ń‚Š¾Š¼Ńƒ самому ŠæŃ€Š°Š²ŠøŠ»Ńƒ: зараз вони ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶Š°ŃŽŃ‚ŃŒ, а Š²ŠµŃ€ŃŃ–я 3.0 віГкиГає ŃŃ‚Š°Ń€Ńƒ Ń„Š¾Ń€Š¼Ńƒ. * ŠŠ¾Š²ŠøŠ¹ коГ не варто Š±ŃƒŠ“ŃƒŠ²Š°Ń‚Šø на жоГній Ń–Š· цих можливостей. Усі Ń–Š½ŃˆŃ– сторінки цієї Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†Ń–Ń— Š½Š°Š²Ń‡Š°ŃŽŃ‚ŃŒ чинного API. diff --git a/i18n/uk/pages/get-started/first-steps.md b/i18n/uk/pages/get-started/first-steps.md index 2b711b43..4c4fb6fc 100644 --- a/i18n/uk/pages/get-started/first-steps.md +++ b/i18n/uk/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # ŠŸŠµŃ€ŃˆŃ– кроки {#first-steps} @@ -17,7 +17,7 @@ translation: * **ŠšŠ»Ń–Ń”Š½Ń‚** живе всереГині хоста й Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ Š¼Š¾Š²Š¾ŃŽ MCP. Єост Š·Š°ŠæŃƒŃŠŗŠ°Ń” по оГному ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ на кожен сервер, Го ŃŠŗŠ¾Š³Š¾ піГ'єГнаний. * **Дервер** — це те, що ви ŃŃ‚Š²Š¾Ń€ŃŽŃ”Ń‚Šµ за Š“Š¾ŠæŠ¾Š¼Š¾Š³Š¾ŃŽ Ń†ŃŒŠ¾Š³Š¾ SDK. Він наГає речі клієнтам. Він ніколи не Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ Ń–Š· Š¼Š¾Š“ŠµŠ»Š»ŃŽ Š½Š°ŠæŃ€ŃŠ¼Ńƒ. -Š’Šø ŠæŠøŃˆŠµŃ‚Šµ сервер. Єости — це Ń‡ŠøŠ¹ŃŃŒ Ń–Š½ŃˆŠøŠ¹ ŠæŃ€Š¾Š“ŃƒŠŗŃ‚. SDK також Гає вам `Client`. ŠŠøŠ¼ ви Ń‚ŠµŃŃ‚ŃƒŠ²Š°Ń‚ŠøŠ¼ŠµŃ‚Šµ свої сервери, і він Š·'ŃŠ²ŠøŃ‚ŃŒŃŃ Галі на цій сторінці. +Š’Šø ŠæŠøŃˆŠµŃ‚Šµ сервер. Єости — це Ń‡ŠøŠ¹ŃŃŒ Ń–Š½ŃˆŠøŠ¹ ŠæŃ€Š¾Š“ŃƒŠŗŃ‚. SDK також Гає вам `Client` — той самий клас, ŃŠŗŠøŠ¼ хост Š·Š²ŠµŃ€Ń‚Š°Š²ŃŃ б Го сервера за URL або запускав би його ŃŠŗ піГпроцес. Він Š·'ŃŠ²ŠøŃ‚ŃŒŃŃ Галі на цій сторінці, і саме ним ви Ń‚ŠµŃŃ‚ŃƒŠ²Š°Ń‚ŠøŠ¼ŠµŃ‚Šµ свої сервери. ## Три примітиви {#the-three-primitives} @@ -83,22 +83,20 @@ Inspector Š·Š°ŠæŃƒŃŃ‚ŠøŠ² ваш сервер через **stdio**, оГин і Коли клієнт піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ, сервер Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” свої **можливості**: на ŃŠŗŃ– сімейства запитів він віГповіГатиме. ŠšŠ»Ń–Ń”Š½Ń‚ Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” це Š¾Š³Š¾Š»Š¾ŃˆŠµŠ½Š½Ń, щоб Š²ŠøŃ€Ń–ŃˆŠøŃ‚Šø, про що взагалі просити. Š’Šø його не писали; `MCPServer` Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” його за вас. -ŠŸŠ¾Š“ŠøŠ²Ń–Ń‚ŃŒŃŃ самі. `Client` Š· SDK приймає об'єкт сервера Š½Š°ŠæŃ€ŃŠ¼Ńƒ й піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Го нього **в пам'яті** (без ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ, без ŠæŠ¾Ń€Ń‚Ńƒ): +ŠŸŠ¾Š“ŠøŠ²Ń–Ń‚ŃŒŃŃ самі. Š—Š°Š»ŠøŃˆŃ‚Šµ `server.py` ŠæŃ€Š°Ń†ŃŽŠ²Š°Ń‚Šø через HTTP в оГному терміналі: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +а Š· Ń–Š½ŃˆŠ¾Š³Š¾ ŃŠæŃ€ŃŠ¼ŃƒŠ¹Ń‚Šµ на нього клієнта: -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -118,8 +116,9 @@ asyncio.run(main()) Š—Š²ŠµŃ€Š½Ń–Ń‚ŃŒ увагу на те, чого там немає. `completions` (Š°Š²Ń‚Š¾Š“Š¾ŠæŠ¾Š²Š½ŠµŠ½Š½Ń Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Ń–Š² Š“Š»Ń ŃˆŠ°Š±Š»Š¾Š½Ń–Š² Ń€ŠµŃŃƒŃ€ŃŃ–Š² і промптів) ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃ” обробника, ŃŠŗŠøŠ¹ ŠæŠøŃˆŠµŃ‚Šµ ви; у Ń†ŃŒŠ¾Š³Š¾ сервера його немає, тож Š¼Š¾Š¶Š»ŠøŠ²Ń–ŃŃ‚ŃŒ Š²Ń–Š“ŃŃƒŃ‚Š½Ń, і коректний клієнт про неї не проситиме. Це правило Š“Š»Ń всього необов'ŃŠ·ŠŗŠ¾Š²Š¾Š³Š¾: Š·Š°Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ¹Ń‚Šµ річ — і Š¼Š¾Š¶Š»ŠøŠ²Ń–ŃŃ‚ŃŒ Š·'ŃŠ²ŠøŃ‚ŃŒŃŃ; **[ŠŠ²Ń‚Š¾Š“Š¾ŠæŠ¾Š²Š½ŠµŠ½Š½Ń](../servers/completions.md)** це Š“Š¾Š²Š¾Š“ŠøŃ‚ŃŒ. !!! info - `Client(mcp)` — той самий клієнт у пам'яті, ŃŠŗŠøŠ¼ протестовано кожен приклаГ у цій Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Š°Ń†Ń–Ń—, і - саме ним ви Ń‚ŠµŃŃ‚ŃƒŠ²Š°Ń‚ŠøŠ¼ŠµŃ‚Šµ свої. Š™Š¾Š¼Ńƒ ŠæŃ€ŠøŃŠ²ŃŃ‡ŠµŠ½Š¾ Ń†Ń–Š»Ńƒ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ: **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](testing.md)**. + Цей `client.py` — уже повноцінний MCP-клієнт, і йому ŠæŃ€ŠøŃŠ²ŃŃ‡ŠµŠ½Š¾ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ **[ŠšŠ»Ń–Ń”Š½Ń‚](../client/index.md)**. + Š£ тесті термінал і порт не потрібні: переГайте `Client` сам об'єкт сервера — + `Client(mcp)`. Цьому теж ŠæŃ€ŠøŃŠ²ŃŃ‡ŠµŠ½Š¾ Ń†Ń–Š»Ńƒ ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ: **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](testing.md)**. ## Чого ви не писали {#what-you-did-not-write} @@ -128,7 +127,7 @@ asyncio.run(main()) * JSON Schema. `a: int, b: int` *і є* схема Š“Š»Ń `add`. * ŠžŠ±Ń€Š¾Š±Š½ŠøŠŗ запитів. `tools/list`, `resources/read`, `prompts/get`: усе Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ”Ń‚ŃŒŃŃ за вас. * ŠžŠ³Š¾Š»Š¾ŃˆŠµŠ½Š½Ń можливостей. `MCPServer` зробив його за вас. -* ЖоГного Ń€ŃŠ“ŠŗŠ° ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. Š£Š·Š³Š¾Š“Š¶ŠµŠ½Š½Ń версії, Š¾Š±Ń€Š°Š¼Š»ŠµŠ½Š½Ń JSON-RPC, обмін Š¼Š¾Š¶Š»ŠøŠ²Š¾ŃŃ‚ŃŠ¼Šø: усе це Š²Ń–Š“Š±ŃƒŠ»Š¾ŃŃ всереГині `mcp dev` і `Client(mcp)`, і ви Ń†ŃŒŠ¾Š³Š¾ не бачили. +* ЖоГного Ń€ŃŠ“ŠŗŠ° ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. Š£Š·Š³Š¾Š“Š¶ŠµŠ½Š½Ń версії, Š¾Š±Ń€Š°Š¼Š»ŠµŠ½Š½Ń JSON-RPC, обмін Š¼Š¾Š¶Š»ŠøŠ²Š¾ŃŃ‚ŃŠ¼Šø: усе це Š²Ń–Š“Š±ŃƒŠ»Š¾ŃŃ всереГині `mcp dev` і `client.py`, і ви Ń†ŃŒŠ¾Š³Š¾ не бачили. Š£ Ń†ŃŒŠ¾Š¼Ńƒ ŃŠæŃ–Š²Š²Ń–Š“Š½Š¾ŃˆŠµŠ½Š½Ń– й увесь сенс SDK. @@ -139,6 +138,6 @@ asyncio.run(main()) * ŠžŠ“ŠøŠ½ Гекоратор на примітив: `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. ŠŠ°Š·Š²Š°, опис і схема Š±ŠµŃ€ŃƒŃ‚ŃŒŃŃ Š· Ń„ŃƒŠ½ŠŗŃ†Ń–Ń—. * URI Š· `{param}` ŃŃ‚Š²Š¾Ń€ŃŽŃ” **шаблон** Ń€ŠµŃŃƒŃ€ŃŃƒ, ŃŠŗŠøŠ¹ ŠæŠ¾ŠŗŠ°Š·ŃƒŃ”Ń‚ŃŒŃŃ окремо віГ конкретних Ń€ŠµŃŃƒŃ€ŃŃ–Š². * **ŠœŠ¾Š¶Š»ŠøŠ²Š¾ŃŃ‚Ń–** сервера Š¾Š³Š¾Š»Š¾ŃˆŃƒŃŽŃ‚ŃŒŃŃ за вас, а клієнт ŠæŃ€Š¾ŃŠøŃ‚ŃŒ лише те, що сервер оголосив. -* `Client(mcp)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Го об'єкта сервера в пам'яті: ваш тестовий стенГ Ń–Š· ŠæŠµŃ€ŃˆŠ¾Š³Š¾ Š“Š½Ń. +* `Client("http://localhost:8000/mcp")` Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ Ń–Š· вашим Š·Š°ŠæŃƒŃ‰ŠµŠ½ŠøŠ¼ сервером. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ йому Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ об'єкт сервера — `Client(mcp)` — і це ваш тестовий стенГ Ń–Š· ŠæŠµŃ€ŃˆŠ¾Š³Š¾ Š“Š½Ń. Далі — **[ŠŸŃ–Š“'Ń”Š“Š½Š°Š½Š½Ń Го ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ хоста](real-host.md)**: цей сервер ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ń– Claude Desktop або IDE, по-ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š¼Ńƒ. ŠŸŠ¾Ń‚Ń–Š¼ **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](testing.md)**: оГна сторінка, оГин клієнт у пам'яті — і Š±Ń–Š»ŃŒŃˆŠµ ніколи не Š“Š¾Š²ŠµŠ“ŠµŃ‚ŃŒŃŃ гаГати, чи воно ŠæŃ€Š°Ń†ŃŽŃ”. ŠŸŃ–ŃŠ»Ń Ń†ŃŒŠ¾Š³Š¾ кожен примітив Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” власну ŃŃ‚Š¾Ń€Ń–Š½ŠŗŃƒ, ŠæŠ¾Ń‡ŠøŠ½Š°ŃŽŃ‡Šø Š· того, ŃŠŗŠøŠ¼ ŠŗŠµŃ€ŃƒŃ” моГель: **[Š†Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø](../servers/tools.md)**. diff --git a/i18n/uk/pages/get-started/testing.md b/i18n/uk/pages/get-started/testing.md index f384dcde..3e82e7bd 100644 --- a/i18n/uk/pages/get-started/testing.md +++ b/i18n/uk/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń {#testing} -Python SDK Š¼Ń–ŃŃ‚ŠøŃ‚ŃŒ клас `Client` Ń–Š· **транспортом у пам'яті**: переГайте йому об'єкт сервера — і він піГ'Ń”Š“Š½Š°Ń”Ń‚ŃŒŃŃ Го нього Š½Š°ŠæŃ€ŃŠ¼Ńƒ. +Клас `Client` Ń–Š· SDK — той самий, що піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ за URL чи Š·Š°ŠæŃƒŃŠŗŠ°Ń” піГпроцес, — ŃƒŠ¼Ń–Ń” піГ'Ń”Š“Š½ŃƒŠ²Š°Ń‚ŠøŃŃ й **у пам'яті**: переГайте йому об'єкт сервера, і він говоритиме Š· ним Š½Š°ŠæŃ€ŃŠ¼Ńƒ. -ЖоГного ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ. ЖоГного ŠæŠ¾Ń€Ń‚Ńƒ. Узагалі жоГного Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ. Та сама Ń–Š“ŠµŃ, що й `TestClient` у FastAPI. +ЖоГного ŠæŃ–Š“ŠæŃ€Š¾Ń†ŠµŃŃƒ. ЖоГного ŠæŠ¾Ń€Ń‚Ńƒ. ŠŃ–Ń‡Š¾Š³Š¾ не ŠæŠµŃ€ŠµŠ“Š°Ń”Ń‚ŃŒŃŃ Š¼ŠµŃ€ŠµŠ¶ŠµŃŽ. Та сама Ń–Š“ŠµŃ, що й `TestClient` у FastAPI. ## Базове Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š°Š½Š½Ń {#basic-usage} @@ -96,12 +96,12 @@ async def test_call_add_tool(client: Client): Š—Š°Š»ŠøŃˆŠ°Š¹Ń‚Šµ його ввімкненим у тестах. Š£ Ń€Š¾Š±Š¾Ń‡Š¾Š¼Ńƒ коГі він не має сенсу. -## Š£ Ń‚Š¾Š¼Ńƒ самому процесі за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ {#in-process-by-default} +## ŠŠµŠ¹Ń‚Ń€Š°Š»ŃŒŠ½Ń–ŃŃ‚ŃŒ щоГо ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ {#era-neutral-by-default} !!! note `Client(mcp)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ в межах ŠæŃ€Š¾Ń†ŠµŃŃƒ й за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ **Š½ŠµŠ¹Ń‚Ń€Š°Š»ŃŒŠ½ŠøŠ¹ щоГо ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń**: він Š·Š¾Š½Š“ŃƒŃ” сервер і - обирає віГповіГний ŃˆŠ»ŃŃ… ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. Š—Š°Ń„Ń–ŠŗŃŃƒŠ¹Ń‚Šµ `mode="legacy"`, ŃŠŗŃ‰Š¾ тест ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” ŃŠµŠ¼Š°Š½Ń‚ŠøŠŗŃƒ, ŃŠæŠµŃ†ŠøŃ„Ń–Ń‡Š½Ńƒ Š“Š»Ń - старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń — push ŃŠµŠ¼ŠæŠ»ŃŽŠ²Š°Š½Š½Ń (sampling) чи еліцитації (elicitation), `message_handler`, — і ŠæŃ€ŠøŠ±ŠµŃ€Ń–Ń‚ŃŒ там `raise_exceptions=True`: + обирає віГповіГний ŃˆŠ»ŃŃ… ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. Š—Š°Ń„Ń–ŠŗŃŃƒŠ¹Ń‚Šµ `mode="legacy"`, ŃŠŗŃ‰Š¾ тест ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” ŃŠµŠ¼Š°Š½Ń‚ŠøŠŗŃƒ, Š²Š»Š°ŃŃ‚ŠøŠ²Ńƒ саме + ŃŃ‚Š°Ń€Š¾Š¼Ńƒ ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½ŃŽ — push ŃŠµŠ¼ŠæŠ»ŃŽŠ²Š°Š½Š½Ń (sampling) чи еліцитації (elicitation), `message_handler`, — і ŠæŃ€ŠøŠ±ŠµŃ€Ń–Ń‚ŃŒ там `raise_exceptions=True`: Š·'Ń”Š“Š½Š°Š½Š½Ń старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń взагалі нічого не ŃƒŠ·Š°Š³Š°Š»ŃŒŠ½ŃŽŃ”, а ŠæŃ€Š°ŠæŠ¾Ń€ŠµŃ†ŃŒ повторно викиГає збій ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ń– Š·Š°Š²Š“Š°Š½Š½Ń сервера, а не у вашому тесті. diff --git a/i18n/uk/pages/handlers/multi-round-trip.md b/i18n/uk/pages/handlers/multi-round-trip.md index 68c6ba20..d8a0cd07 100644 --- a/i18n/uk/pages/handlers/multi-round-trip.md +++ b/i18n/uk/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # Š‘Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń– запити {#multi-round-trip-requests} @@ -164,7 +164,7 @@ TTL, прив'ŃŠ·ŠŗŠ° Го принципала й прив'ŃŠ·ŠŗŠ° Го за ## Š ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ версії 2026-07-28 {#a-2026-07-28-result} -`InputRequiredResult` Ń–ŃŠ½ŃƒŃ” лише у версії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ **2026-07-28**. `Client(server)` у пам'яті ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” її за вас; Š¼ŠµŃ€ŠµŠ¶ŠµŃŽ її Š²ŠøŃŠ²Š»ŃŃ” `mode="auto"`. ŠŸŃ–ŃŠ»Ń піГ'Ń”Š“Š½Š°Š½Š½Ń `client.protocol_version` покаже, що саме ви отримали. +`InputRequiredResult` Ń–ŃŠ½ŃƒŃ” лише у версії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ **2026-07-28**. Режим `mode="auto"`, типовий Š“Š»Ń `Client`, Š²ŠøŃŠ²Š»ŃŃ” її на буГь-ŃŠŗŠ¾Š¼Ńƒ Š·'єГнанні. ŠŸŃ–ŃŠ»Ń піГ'Ń”Š“Š½Š°Š½Š½Ń `client.protocol_version` покаже, що саме ви отримали. !!! warning Š£ сесії, ŃŃ‚Š°Ń€ŃˆŃ–Š¹ за 2026, `InputRequiredResult` просто Š½Ń–ŠŗŃƒŠ“Šø покласти. ŠŸŠ¾Š²ŠµŃ€Š½Ń–Ń‚ŃŒ його Š· обробника на diff --git a/i18n/uk/pages/handlers/progress.md b/i18n/uk/pages/handlers/progress.md index e6d9f4cb..7e4a22db 100644 --- a/i18n/uk/pages/handlers/progress.md +++ b/i18n/uk/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # ŠŸŠµŃ€ŠµŠ±Ń–Š³ Š²ŠøŠŗŠ¾Š½Š°Š½Š½Ń {#progress} @@ -29,19 +29,17 @@ translation: ŠšŠ»Ń–Ń”Š½Ń‚ ŠæŃ–Š“ŠæŠøŃŃƒŃ”Ń‚ŃŒŃŃ **окремо Š“Š»Ń кожного виклику**, ŠæŠµŃ€ŠµŠ“Š°ŃŽŃ‡Šø `progress_callback=` у `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,28 +54,31 @@ anyio.run(main) Колбек — це `async`-Ń„ŃƒŠ½ŠŗŃ†Ń–Ń, ŃŠŗŠ° приймає рівно те, що повіГомив сервер: `progress`, `total`, `message`. !!! info - `Client(mcp)` піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ Го об'єкта сервера, у пам'яті, — це той самий клієнт, на ŃŠŗŠ¾Š¼Ńƒ - побуГована сторінка **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](../get-started/testing.md)**. ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€ `progress_callback` оГнаковий - незалежно віГ Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ, ŃŠŗŠøŠ¹ Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” `Client`; а от *хронометраж*, ŃŠŗŠøŠ¹ ви зараз побачите, - властивий саме Š·'Ń”Š“Š½Š°Š½Š½ŃŽ в пам'яті. Воно Š·Š°ŠæŃƒŃŠŗŠ°Ń” колбек Š¾Š“Ń€Š°Š·Ńƒ на місці, тож кожен звіт Š½Š°Š“Ń…Š¾Š“ŠøŃ‚ŃŒ Го - того, ŃŠŗ `call_tool` поверне Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. ŠŠ° ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š¼Ńƒ транспорті ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń Š·Š¼Š°Š³Š°ŃŽŃ‚ŃŒŃŃ Š· Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Š¾Š¼, - і ŠæŠ¾Š²Ń–Š»ŃŒŠ½ŠøŠ¹ колбек може ще Š²ŠøŠŗŠ¾Š½ŃƒŠ²Š°Ń‚ŠøŃŃ ŠæŃ–ŃŠ»Ń того, ŃŠŗ `call_tool` уже ŠæŠ¾Š²ŠµŃ€Š½ŃƒŠ² Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. + ŠŸŠ°Ń€Š°Š¼ŠµŃ‚Ń€ `progress_callback` оГнаковий незалежно віГ того, що ви переГали в `Client`: URL, ŃŠŗ тут, + `StdioServerParameters` чи об'єкт сервера в тесті. ŠŸŃ€Š¾Ń‚Šµ зважайте на хронометраж на ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š¼Ńƒ + транспорті. Кожне ŃŠæŠ¾Š²Ń–Ń‰ŠµŠ½Š½Ń Š“Š¾ŃŃ‚Š°Š²Š»ŃŃ”Ń‚ŃŒŃŃ окремо, ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· Š²Ń–Š“ŠæŠ¾Š²Ń–Š“Š“ŃŽ, тож ŠæŠ¾Š²Ń–Š»ŃŒŠ½ŠøŠ¹ колбек може + ще Š²ŠøŠŗŠ¾Š½ŃƒŠ²Š°Ń‚ŠøŃŃ ŠæŃ–ŃŠ»Ń того, ŃŠŗ `call_tool` уже ŠæŠ¾Š²ŠµŃ€Š½ŃƒŠ² Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. Š›ŠøŃˆŠµ тестове Š·'Ń”Š“Š½Š°Š½Š½Ń в межах + ŠæŃ€Š¾Ń†ŠµŃŃƒ Š·Š°ŠæŃƒŃŠŗŠ°Ń” колбек Š¾Š“Ń€Š°Š·Ńƒ на місці й Š³Š°Ń€Š°Š½Ń‚ŃƒŃ”, що кожен звіт наГійГе Ń€Š°Š½Ń–ŃˆŠµ. ### Š”ŠæŃ€Š¾Š±ŃƒŠ¹Ń‚Šµ самі {#try-it} -ŠŸŠ¾ŠŗŠ»Š°Š“Ń–Ń‚ŃŒ `client.py` ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· `server.py` і Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ: +Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `server.py` через HTTP, а потім Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ клієнт у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі: + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -Кожен `await ctx.report_progress(...)` на сервері ŠæŠµŃ€ŠµŃ‚Š²Š¾Ń€ŠøŠ²ŃŃ на оГин виклик `show` на клієнті, у Ń‚Š¾Š¼Ńƒ самому ŠæŠ¾Ń€ŃŠ“ŠŗŃƒ, і обиГва Ń€ŃŠ“ŠŗŠø Š½Š°Š“Ń€ŃƒŠŗŃƒŠ²Š°Š»ŠøŃŃ **Го** того, ŃŠŗ `call_tool` ŠæŠ¾Š²ŠµŃ€Š½ŃƒŠ² Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. ŠŸŠµŃ€ŠµŠ±Ń–Š³ не ŠæŠ°ŠŗŃƒŃ”Ń‚ŃŒŃŃ в Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ — він Š½Š°Š“Ń…Š¾Š“ŠøŃ‚ŃŒ потоком, поки Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ іще ŠæŃ€Š°Ń†ŃŽŃ”. +Кожен `await ctx.report_progress(...)` на сервері ŠæŠµŃ€ŠµŃ‚Š²Š¾Ń€ŠøŠ²ŃŃ на оГин виклик `show` на клієнті, у Ń‚Š¾Š¼Ńƒ самому ŠæŠ¾Ń€ŃŠ“ŠŗŃƒ. ŠŸŠµŃ€ŠµŠ±Ń–Š³ не ŠæŠ°ŠŗŃƒŃ”Ń‚ŃŒŃŃ в Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚. Він Š½Š°Š“Ń…Š¾Š“ŠøŃ‚ŃŒ потоком, поки Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ іще ŠæŃ€Š°Ń†ŃŽŃ”. !!! warning `progress_callback` Š½Š°Š»ŠµŠ¶ŠøŃ‚ŃŒ **виклику**, а не `Client`. ŠŃ€Š³ŃƒŠ¼ŠµŠ½Ń‚Ńƒ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š° Š“Š»Ń нього немає, diff --git a/i18n/uk/pages/protocol-versions.md b/i18n/uk/pages/protocol-versions.md index fcbb2347..bd5202a2 100644 --- a/i18n/uk/pages/protocol-versions.md +++ b/i18n/uk/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # Версії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ {#protocol-versions} @@ -11,9 +11,17 @@ translation: ŠŸŠµŃ€ŠµŠ¹Š¼Š°Ń‚ŠøŃŃ цим майже ніколи не Š“Š¾Š²Š¾Š“ŠøŃ‚ŃŒŃŃ, бо `Client` Š“Š¾Š¼Š¾Š²Š»ŃŃ”Ń‚ŃŒŃŃ за вас. Š¦Ń сторінка — про єГиний Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, ŃŠŗŠøŠ¹ цим ŠŗŠµŃ€ŃƒŃ”, `mode=`, і про три випаГки, коли його Š·Š¼Ń–Š½ŃŽŃŽŃ‚ŃŒ. +Кожен фрагмент на цій сторінці — це `client.py`, що Ń€Š¾Š·Š¼Š¾Š²Š»ŃŃ” Š· `server.py` книгарні Bookshop Š·Ń– сторінки **[ŠšŠ»Ń–Ń”Š½Ń‚](client/index.md)**. Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ цей сервер в оГному терміналі: + +```console +uv run mcp run server.py --transport streamable-http +``` + +ŠŸŠ¾Ń‚Ń–Š¼ Š·Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ кожен фрагмент у Š“Ń€ŃƒŠ³Š¾Š¼Ńƒ терміналі ŠŗŠ¾Š¼Š°Š½Š“Š¾ŃŽ `python client.py`. + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,13 +39,14 @@ translation: ŠžŃ†Šµ й уся Š¼Š¾Š¶Š»ŠøŠ²Ń–ŃŃ‚ŃŒ. ŠžŠ“ŠøŠ½ `Client`, сервер буГь-ŃŠŗŠ¾Š³Š¾ ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń, жоГних Ń€Š¾Š·Š³Š°Š»ŃƒŠ¶ŠµŠ½ŃŒ у вашому коГі. !!! info - `MCPServer` віГповіГає на `server/discover` на кожному транспорті — in-memory, stdio, streamable - HTTP — тож Ń–Š· власним сервером `auto` завжГи Š·ŃƒŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ на `2026-07-28`. Запасний ŃˆŠ»ŃŃ… - ŃŠæŃ€Š°Ń†ŃŒŠ¾Š²ŃƒŃ” лише проти ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ сервера, Š²ŠøŠæŃƒŃ‰ŠµŠ½Š¾Š³Š¾ Го 2026, — саме тоГі, коли він і потрібен. + `MCPServer` віГповіГає на `server/discover` на кожному транспорті — Streamable HTTP, stdio та + Š²Š½ŃƒŃ‚Ń€Ń–ŃˆŠ½ŃŒŠ¾ŠæŃ€Š¾Ń†ŠµŃŠ½Š¾Š¼Ńƒ Š·'єГнанні, ŃŠŗŠøŠ¼ ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŃŽŃ‚ŃŒŃŃ Š²Š°ŃˆŃ– тести, — тож Ń–Š· власним сервером `auto` + завжГи Š·ŃƒŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ на `2026-07-28`. Запасний ŃˆŠ»ŃŃ… ŃŠæŃ€Š°Ń†ŃŒŠ¾Š²ŃƒŃ” лише проти ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ сервера, + Š²ŠøŠæŃƒŃ‰ŠµŠ½Š¾Š³Š¾ Го 2026, — саме тоГі, коли він і потрібен. ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -61,7 +70,7 @@ translation: `mode` також приймає Ń€ŃŠ“Š¾Šŗ ŃŃƒŃ‡Š°ŃŠ½Š¾Ń— версії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ. Š”ŃŒŠ¾Š³Š¾Š“Š½Ń– ця множина — рівно `["2026-07-28"]`. -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -94,7 +103,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 Тож Š·Š±ŠµŃ€ŠµŠ¶Ń–Ń‚ŃŒ її. ŠŸŃ–ŃŠ»Ń Š·'Ń”Š“Š½Š°Š½Š½Ń в режимі `auto` `client.session.discover_result` Š¼Ń–ŃŃ‚ŠøŃ‚ŃŒ точний `DiscoverResult`, ŃŠŗŠøŠ¹ наГіслав сервер: його `supported_versions`, `capabilities`, `instructions` і віГомості про себе, ŃŠŗŃ– сервер записав у `_meta` Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ńƒ. ŠŠ°ŃŃ‚ŃƒŠæŠ½Š¾Š³Š¾ Ń€Š°Š·Ńƒ переГайте його назаГ ŃŠŗ `prior_discover=`: -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/uk/pages/run/asgi.md b/i18n/uk/pages/run/asgi.md index 8525330f..84e57d87 100644 --- a/i18n/uk/pages/run/asgi.md +++ b/i18n/uk/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # Š”Š¾Š“Š°Š²Š°Š½Š½Ń Го Š½Š°ŃŠ²Š½Š¾Š³Š¾ Š·Š°ŃŃ‚Š¾ŃŃƒŠ½ŠŗŃƒ {#add-to-an-existing-app} @@ -99,7 +99,7 @@ uvicorn server:app --8<-- "docs_src/asgi/tutorial004.py" ``` -Тепер клієнти піГ'Ń”Š“Š½ŃƒŃŽŃ‚ŃŒŃŃ Го `/notes`, а не Го `/notes/mcp`. +Тепер клієнти піГ'Ń”Š“Š½ŃƒŃŽŃ‚ŃŒŃŃ Го `/notes/`, а не Го `/notes/mcp`. ## CORS Š“Š»Ń Š±Ń€Š°ŃƒŠ·ŠµŃ€Š½ŠøŃ… клієнтів {#cors-for-browser-clients} @@ -142,4 +142,4 @@ uvicorn server:app * Š‘Ń€Š°ŃƒŠ·ŠµŃ€Š½ŠøŠ¼ клієнтам потрібен CORS: `allow_headers` Š“Š»Ń заголовків Š·Š°ŠæŠøŃ‚Ńƒ `Mcp-*`, `expose_headers=["Mcp-Session-Id"]` Š“Š»Ń віГповіГі. * `@mcp.custom_route()` ГоГає звичайні HTTP-кінцеві точки без автентифікації ŠæŠ¾Ń€ŃƒŃ‡ Ń–Š· `/mcp`. -Щойно сервер стане Š“Š¾ŃŃŠ¶Š½ŠøŠ¼ за ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾ŃŽ URL-Š°Š“Ń€ŠµŃŠ¾ŃŽ, **[ŠšŠ»Ń–Ń”Š½Ń‚](../client/index.md)** піГ'Ń”Š“Š½Š°Ń”Ń‚ŃŒŃŃ Го нього за цією URL-Š°Š“Ń€ŠµŃŠ¾ŃŽ, а не через об'єкт сервера. +Щойно сервер стане Š“Š¾ŃŃŠ¶Š½ŠøŠ¼ за ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾ŃŽ URL-Š°Š“Ń€ŠµŃŠ¾ŃŽ, **[ŠšŠ»Ń–Ń”Š½Ń‚](../client/index.md)** піГ'Ń”Š“Š½Š°Ń”Ń‚ŃŒŃŃ Го нього за цією URL-Š°Š“Ń€ŠµŃŠ¾ŃŽ. diff --git a/i18n/uk/pages/run/authorization.md b/i18n/uk/pages/run/authorization.md index d293bd32..250704df 100644 --- a/i18n/uk/pages/run/authorization.md +++ b/i18n/uk/pages/run/authorization.md @@ -1,60 +1,64 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń {#authorization} -Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його сліГ так само, ŃŠŗ буГь-ŃŠŗŠøŠ¹ Ń–Š½ŃˆŠøŠ¹ вебсервіс: bearer-токенами OAuth 2.1. +Через Streamable HTTP ваш MCP-сервер — це звичайний вебсервіс, і захищати його треба так само, ŃŠŗ буГь-ŃŠŗŠøŠ¹ вебсервіс: bearer-токенами OAuth 2.1. -Š£ термінах OAuth ваш сервер — це **сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š²** (resource server). Він ніколи нікого не Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” й ніколи не виГає токенів. Він Ń€Š¾Š±ŠøŃ‚ŃŒ оГне: Š“ŠøŠ²ŠøŃ‚ŃŒŃŃ на заголовок `Authorization` у кожному запиті й Š²ŠøŃ€Ń–ŃˆŃƒŃ”, чи приГатний токен у ньому. +Š£ термінах OAuth ваш сервер — це **сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š²**. Він нікого не Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” і ніколи не виГає токенів. Він Ń€Š¾Š±ŠøŃ‚ŃŒ оГне: Š“ŠøŠ²ŠøŃ‚ŃŒŃŃ на заголовок `Authorization` кожного Š·Š°ŠæŠøŃ‚Ńƒ й Š²ŠøŃ€Ń–ŃˆŃƒŃ”, чи Гійсний токен у ньому. -Š¦Ń сторінка — про серверний бік. ŠšŠ»Ń–Ń”Š½Ń‚, ŃŠŗŠøŠ¹ Š·Š½Š°Ń…Š¾Š“ŠøŃ‚ŃŒ ваш сервер авторизації й Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” токен, описано на сторінці **[ŠšŠ»Ń–Ń”Š½Ń‚Šø OAuth](../client/oauth-clients.md)**. +Š¦Ń сторінка — про ŃŠµŃ€Š²ŠµŃ€Š½Ńƒ Ń‡Š°ŃŃ‚ŠøŠ½Ńƒ. ŠšŠ»Ń–Ń”Š½Ń‚, ŃŠŗŠøŠ¹ Š·Š½Š°Ń…Š¾Š“ŠøŃ‚ŃŒ ваш сервер авторизації й Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” токен, описаний на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**. ## Три сторони {#the-three-parties} -* **Дервер авторизації** Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” Š»ŃŽŠ“ŠµŠ¹ і виГає токени Š“Š¾ŃŃ‚ŃƒŠæŃƒ. Š’Šø його не ŠæŠøŃˆŠµŃ‚Šµ. Це ваш ŠæŠ¾ŃŃ‚Š°Ń‡Š°Š»ŃŒŠ½ŠøŠŗ іГентичності (Auth0, Keycloak, Entra, ваш власний). +* **Дервер авторизації** Š°Š²Ń‚ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” Š»ŃŽŠ“ŠµŠ¹ і виГає токени Š“Š¾ŃŃ‚ŃƒŠæŃƒ. Його ви не ŠæŠøŃˆŠµŃ‚Šµ. Це ваш ŠæŠ¾ŃŃ‚Š°Ń‡Š°Š»ŃŒŠ½ŠøŠŗ іГентичності (Auth0, Keycloak, Entra, власний). * **Дервер Ń€ŠµŃŃƒŃ€ŃŃ–Š²** — це ваш MCP-сервер. Він ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” токен у кожному запиті. * **ŠšŠ»Ń–Ń”Š½Ń‚** Š·'ŃŃŠ¾Š²ŃƒŃ”, ŃŠŗŠ¾Š¼Ńƒ ŃŠµŃ€Š²ŠµŃ€Ńƒ авторизації ви Š“Š¾Š²Ń–Ń€ŃŃ”Ń‚Šµ, Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” віГ нього токен і наГсилає його вам ŃŠŗ `Authorization: Bearer `. -ŠžŃ†Šµ й увесь Ń‚Ń€ŠøŠŗŃƒŃ‚Š½ŠøŠŗ. Усе на цій сторінці ŃŃ‚Š¾ŃŃƒŃ”Ń‚ŃŒŃŃ ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾Š³Š¾ ŠæŃƒŠ½ŠŗŃ‚Ńƒ. +ŠžŃ†Šµ й увесь Ń‚Ń€ŠøŠŗŃƒŃ‚Š½ŠøŠŗ. Усе на цій сторінці — про сереГній ŠæŃƒŠ½ŠŗŃ‚. ## Верифікатор токенів {#a-token-verifier} -SDK не має власної Гумки про те, ŃŠŗŠøŠ¹ Š²ŠøŠ³Š»ŃŠ“ має Гійсний токен. Це визначаєте ви, Ń€ŠµŠ°Š»Ń–Š·ŃƒŠ²Š°Š²ŃˆŠø **`TokenVerifier`**: +SDK не має власної Гумки про те, ŃŠŗŠøŠ¹ токен вважати Гійсним. Це визначаєте ви, Ń€ŠµŠ°Š»Ń–Š·ŃƒŃŽŃ‡Šø **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` -* `TokenVerifier` — це протокол Š· оГним асинхронним метоГом. `verify_token` Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” сирий токен Ń–Š· заголовка `Authorization` і повертає **`AccessToken`**, ŃŠŗŃ‰Š¾ він Гійсний, або `None`, ŃŠŗŃ‰Š¾ ні. Š‘Ń–Š»ŃŒŃˆŠµ нічого Ń€ŠµŠ°Š»Ń–Š·Š¾Š²ŃƒŠ²Š°Ń‚Šø не треба. -* Цей верифікатор ŃˆŃƒŠŗŠ°Ń” токен у таблиці. Дправжній ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” піГпис JWT або Š·Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ Го кінцевої точки інтроспекції токенів сервера авторизації. Цей коГ — ваш; SDK лише викликає його. -* `token_verifier=` і `auth=` завжГи Š¹Š“ŃƒŃ‚ŃŒ разом. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ оГне без Ń–Š½ŃˆŠ¾Š³Š¾ — і `MCPServer(...)` викине `ValueError` ще Го того, ŃŠŗ Š¾Š±ŃŠ»ŃƒŠ¶ŠøŃ‚ŃŒ хоч оГин запит. +* `TokenVerifier` — це протокол Š· оГним асинхронним метоГом. `verify_token` Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” сирий токен Ń–Š· заголовка `Authorization` і повертає **`AccessToken`**, ŃŠŗŃ‰Š¾ токен Гійсний, або `None`, ŃŠŗŃ‰Š¾ ні. Š‘Ń–Š»ŃŒŃˆŠµ нічого Ń€ŠµŠ°Š»Ń–Š·Š¾Š²ŃƒŠ²Š°Ń‚Šø не потрібно. +* Цей ŃˆŃƒŠŗŠ°Ń” токен у таблиці; кожен запис Ń„Ń–ŠŗŃŃƒŃ” Ń€ŠµŃŃƒŃ€Ń, Š“Š»Ń ŃŠŗŠ¾Š³Š¾ токен виГано. Дправжній ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” піГпис JWT або викликає ŠŗŃ–Š½Ń†ŠµŠ²Ńƒ Ń‚Š¾Ń‡ŠŗŃƒ інтроспекції токенів сервера авторизації і ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŃŃ”, Š“Š»Ń кого виГано токен (його `aud`), у `AccessToken.resource`. Цей коГ — ваш; SDK його лише викликає. +* `token_verifier=` і `auth=` завжГи Š¹Š“ŃƒŃ‚ŃŒ у парі. ŠŸŠµŃ€ŠµŠ“Š°Š¹Ń‚Šµ оГне без Ń–Š½ŃˆŠ¾Š³Š¾ — і `MCPServer(...)` викине `ValueError`, ще не обслуживши жоГного Š·Š°ŠæŠøŃ‚Ńƒ. -`AuthSettings` — це ŠæŃƒŠ±Š»Ń–чне Š¾Š±Š»ŠøŃ‡Ń‡Ń вашого сервера Ń€ŠµŃŃƒŃ€ŃŃ–Š²: +`AuthSettings` — ŠæŃƒŠ±Š»Ń–Ń‡Š½Šµ Š¾Š±Š»ŠøŃ‡Ń‡Ń вашого сервера Ń€ŠµŃŃƒŃ€ŃŃ–Š²: * `issuer_url`: сервер авторизації, що виГає Š²Š°ŃˆŃ– токени. -* `resource_server_url`: ŠæŃƒŠ±Š»Ń–Ń‡Š½ŠøŠ¹ URL цієї MCP-кінцевої точки. Він ŃƒŠŗŠ°Š·ŃƒŃ”, Š“Š»Ń *ŃŠŗŠ¾Š³Š¾* Ń€ŠµŃŃƒŃ€ŃŃƒ призначено токен, і саме тут розміщено Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń. +* `resource_server_url`: ŠæŃƒŠ±Š»Ń–Ń‡Š½Š° URL-аГреса цієї кінцевої точки MCP. Вона Š²ŠŗŠ°Š·ŃƒŃ”, Š“Š»Ń *ŃŠŗŠ¾Š³Š¾* Ń€ŠµŃŃƒŃ€ŃŃƒ призначено токен, і саме за Š½ŠµŃŽ Š»ŠµŠ¶ŠøŃ‚ŃŒ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń. * `required_scopes`: кожен токен Š¼ŃƒŃŠøŃ‚ŃŒ містити їх усі. +* `validate_token_resource`: Š²Ń–Š“Ń…ŠøŠ»ŃŃ‚Šø буГь-ŃŠŗŠøŠ¹ токен, чий `AccessToken.resource` не Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `resource_server_url`. Якщо Š·Š°Š»ŠøŃˆŠøŃ‚Šø його незаГаним, коли `resource_server_url` заГано, Š·'ŃŠ²Š»ŃŃ”Ń‚ŃŒŃŃ ŠæŠ¾ŠæŠµŃ€ŠµŠ“Š¶ŠµŠ½Š½Ń (`MCPDeprecationWarning`), а повеГінка віГповіГає `False`; у 3.0 типовим Š·Š½Š°Ń‡ŠµŠ½Š½ŃŠ¼ Š“Š»Ń серверів Ń€ŠµŃŃƒŃ€ŃŃ–Š² стане `True`. + * Š£Š²Ń–Š¼ŠŗŠ½Ń–Ń‚ŃŒ його, ŃŠŗŃ‰Š¾ ваш сервер авторизації прив'ŃŠ·ŃƒŃ” токени Го `resource`, ŃŠŗŠøŠ¹ запросив клієнт, — а MCP-клієнти завжГи його Š½Š°Š“ŃŠøŠ»Š°ŃŽŃ‚ŃŒ. `resource_server_url` має Š±ŃƒŃ‚Šø точно тією URL-Š°Š“Ń€ŠµŃŠ¾ŃŽ, Го ŃŠŗŠ¾Ń— піГ'Ń”Š“Š½ŃƒŃŽŃ‚ŃŒŃŃ клієнти. + * Š—Š°Š»ŠøŃˆŃ‚Šµ вимкненим, ŃŠŗŃ‰Š¾ ваш сервер авторизації Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ” власні іГентифікатори Š°ŃƒŠ“иторії (іГентифікатор API в Auth0, іГентифікатор Š·Š°ŃŃ‚Š¾ŃŃƒŠ½ŠŗŃƒ в Entra), і Š½Š°Ń‚Š¾Š¼Ń–ŃŃ‚ŃŒ ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŠ¹Ń‚Šµ `aud` у ŃŠ²Š¾Ń”Š¼Ńƒ верифікаторі, ŠæŠ¾Š²ŠµŃ€Ń‚Š°ŃŽŃ‡Šø `None` Š“Š»Ń токена, що не призначений Ń†ŃŒŠ¾Š¼Ńƒ ŃŠµŃ€Š²ŠµŃ€Ńƒ. + * Якщо `aud` — список, ŠæŠ¾ŠŗŠ»Š°Š“Ń–Ń‚ŃŒ у `resource` той елемент, що Š“Š¾Ń€Ń–Š²Š½ŃŽŃ” `resource_server_url`. !!! tip - `examples/servers/simple-auth/` у репозиторії SDK Š¼Ń–ŃŃ‚ŠøŃ‚ŃŒ `IntrospectionTokenVerifier`, ŃŠŗŠøŠ¹ Š·Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ - Го кінцевої точки [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ сервера авторизації. Даме Ń‚Š°ŠŗŃƒ Ń„Š¾Ń€Š¼Ńƒ Š¼Š°ŃŽŃ‚ŃŒ Š±Ń–Š»ŃŒŃˆŃ–ŃŃ‚ŃŒ ŠæŃ€Š¾Š“Š°ŠŗŃˆŠ½-верифікаторів. + Š£ `examples/servers/simple-auth/` у репозиторії SDK є `IntrospectionTokenVerifier`, ŃŠŗŠøŠ¹ викликає + ŠŗŃ–Š½Ń†ŠµŠ²Ńƒ Ń‚Š¾Ń‡ŠŗŃƒ [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) ŃŠæŃ€Š°Š²Š¶Š½ŃŒŠ¾Š³Š¾ сервера авторизації. Даме Ń‚Š°ŠŗŃƒ Ń„Š¾Ń€Š¼Ńƒ має Š±Ń–Š»ŃŒŃˆŃ–ŃŃ‚ŃŒ верифікаторів у Ń€ŠµŠ°Š»ŃŒŠ½ŠøŃ… Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½ŃŃ…. -## Що Š·'ŃŠ²Š»ŃŃ”Ń‚ŃŒŃŃ через HTTP {#what-you-get-over-http} +## Що ви Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ”Ń‚Šµ через HTTP {#what-you-get-over-http} -ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń живе в HTTP-заголовках, тож Ń–ŃŠ½ŃƒŃ” лише на HTTP-транспортах. Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ сервер на Ń‚Š¾Š¼Ńƒ, ŃŠŗŠøŠ¹ розгортаєте: `mcp.run(transport="streamable-http")` Ń€Š¾Š·Š¼Ń–Ń‰ŃƒŃ” його на `http://127.0.0.1:8000/mcp`, а Ń€ŠµŃˆŃ‚Š° — на сторінці **[Š—Š°ŠæŃƒŃŠŗ сервера](index.md)**. Тепер Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ має Гва Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚Šø: +ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń живе в HTTP-заголовках, тож Ń–ŃŠ½ŃƒŃ” лише на HTTP-транспортах. Š—Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ її на Ń‚Š¾Š¼Ńƒ, ŃŠŗŠøŠ¹ розгортаєте: `mcp.run(transport="streamable-http")` піГнімає сервер на `http://127.0.0.1:8000/mcp`, а Ń€ŠµŃˆŃ‚Ńƒ описано на сторінці **[Š—Š°ŠæŃƒŃŠŗ сервера](index.md)**. Тепер Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ має Гва Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚Šø: ```text /mcp /.well-known/oauth-protected-resource/mcp ``` -Š’Šø Š·Š°Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ²Š°Š»Šø оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚. Š”Ń€ŃƒŠ³ŠøŠ¹ Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ Š½Š°Š»ŠµŠ¶ŠøŃ‚ŃŒ SDK. +Š’Šø Š·Š°Ń€ŠµŃ”ŃŃ‚Ń€ŃƒŠ²Š°Š»Šø оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚. Š”Ń€ŃƒŠ³ŠøŠ¹ Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ — віГ SDK. ### Š’ŠøŃŠ²Š»ŠµŠ½Š½Ń {#discovery} -Š—Ń€Š¾Š±Ń–Ń‚ŃŒ `GET` на цей well-known-ŃˆŠ»ŃŃ… — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, ŠæŠ¾Š±ŃƒŠ“Š¾Š²Š°Š½Ń– Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ Š· вашого `AuthSettings`: +Š—Ń€Š¾Š±Ń–Ń‚ŃŒ `GET` на цей well-known ŃˆŠ»ŃŃ… — і отримаєте **[RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata**, ŠæŠ¾Š±ŃƒŠ“Š¾Š²Š°Š½Ń– ŠæŃ€ŃŠ¼Š¾ Š· Š²Š°ŃˆŠøŃ… `AuthSettings`: ```json { @@ -65,7 +69,7 @@ SDK не має власної Гумки про те, ŃŠŗŠøŠ¹ Š²ŠøŠ³Š»ŃŠ“ м } ``` -Даме Š·Š°Š²Š“ŃŠŗŠø Ń†ŃŒŠ¾Š¼Ńƒ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Ńƒ клієнт, ŃŠŗŠøŠ¹ ніколи не Ń‡ŃƒŠ² про ваш сервер, Š·Š½Š°Ń…Š¾Š“ŠøŃ‚ŃŒ Š“Š¾Ń€Š¾Š³Ńƒ: він читає `authorization_servers` і йГе Ń‚ŃƒŠ“Šø по токен. Š’Šø не написали Š· нього жоГного Ń€ŃŠ“ŠŗŠ°. +Даме Š·Š°Š²Š“ŃŠŗŠø Ń†ŃŒŠ¾Š¼Ńƒ Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚Ńƒ клієнт, ŃŠŗŠøŠ¹ ніколи не Ń‡ŃƒŠ² про ваш сервер, Š·Š½Š°Ń…Š¾Š“ŠøŃ‚ŃŒ ŃˆŠ»ŃŃ… ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ńƒ: він читає `authorization_servers` і йГе Ń‚ŃƒŠ“Šø по токен. Š’Šø не написали Š· нього жоГного Ń€ŃŠ“ŠŗŠ°. !!! check Š—Š²ŠµŃ€Š½Ń–Ń‚ŃŒŃŃ Го `/mcp` без токена (або Š· таким, Š“Š»Ń ŃŠŗŠ¾Š³Š¾ ваш верифікатор ŠæŠ¾Š²ŠµŃ€Š½ŃƒŠ² `None`) — і запит @@ -78,26 +82,26 @@ SDK не має власної Гумки про те, ŃŠŗŠøŠ¹ Š²ŠøŠ³Š»ŃŠ“ м {"error": "invalid_token", "error_description": "Authentication required"} ``` - ŠŃ–Ń‡Š¾Š³Š¾ не було розібрано, і жоГен Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ не Š²ŠøŠŗŠ¾Š½Š°Š²ŃŃ. А вказівник `resource_metadata` у `WWW-Authenticate` — - саме те, що Ń€Š¾Š±ŠøŃ‚ŃŒ Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń автоматичним: 401 -> Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ метаГаних -> сервер авторизації -> токен -> повторна спроба. + ŠŃ–Ń‡Š¾Š³Š¾ не розібрано, жоГен Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ не Š²ŠøŠŗŠ¾Š½ŃƒŠ²Š°Š²ŃŃ. А вказівник `resource_metadata` у `WWW-Authenticate` — + це те, що Ń€Š¾Š±ŠøŃ‚ŃŒ Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń автоматичним: 401 -> Š“Š¾ŠŗŃƒŠ¼ŠµŠ½Ń‚ метаГаних -> сервер авторизації -> токен -> повторна спроба. !!! warning - ŠŃ–Ń‰Š¾ Š· Ń†ŃŒŠ¾Š³Š¾ не захищає `stdio`. Канал не має заголовка `Authorization`, тож `token_verifier` там ніколи - не Š²ŠøŠŗŠ»ŠøŠŗŠ°Ń”Ń‚ŃŒŃŃ. Межа безпеки `stdio`-сервера — це процес, ŃŠŗŠøŠ¹ його Š·Š°ŠæŃƒŃŃ‚ив. Те саме - ŃŃ‚Š¾ŃŃƒŃ”Ń‚ŃŒŃŃ `Client(mcp)` у пам'яті, ŃŠŗŠøŠ¹ ви Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃ”Ń‚Šµ в тестах: він піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ Го об'єкта сервера - й оминає HTTP-Ń€Ń–Š²ŠµŠ½ŃŒ, разом Š· Š°Š²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń”ŃŽ. + ŠŃ–Ń‡Š¾Š³Š¾ Š· Ń†ŃŒŠ¾Š³Š¾ не захищає `stdio`. Канал (pipe) не має заголовка `Authorization`, тож `token_verifier` там ніколи + не Š²ŠøŠŗŠ»ŠøŠŗŠ°Ń”Ń‚ŃŒŃŃ. Межа безпеки `stdio`-сервера — процес, ŃŠŗŠøŠ¹ його Š·Š°ŠæŃƒŃŃ‚ив. Те саме + ŃŃ‚Š¾ŃŃƒŃ”Ń‚ŃŒŃŃ `Client(mcp)` у пам'яті, ŃŠŗŠøŠ¹ Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŃŽŃ‚ŃŒ у тестах: він піГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Š±ŠµŠ·ŠæŠ¾ŃŠµŃ€ŠµŠ“Š½ŃŒŠ¾ Го об'єкта сервера + й оминає HTTP-Ń€Ń–Š²ŠµŠ½ŃŒ разом Š· Š°Š²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń”ŃŽ. ## Š†Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ того, хто викликає {#the-callers-identity} УсереГині буГь-ŃŠŗŠ¾Š³Š¾ обробника **`get_access_token()`** — це `AccessToken`, ŃŠŗŠøŠ¹ ваш верифікатор ŠæŠ¾Š²ŠµŃ€Š½ŃƒŠ² Š“Š»Ń поточного Š·Š°ŠæŠøŃ‚Ńƒ: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` -* Це ŠæŃ€Š°Ń†ŃŽŃ” в Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°Ń…, Ń€ŠµŃŃƒŃ€ŃŠ°Ń… і промптах, і нічого Š½Ń–ŠŗŃƒŠ“Šø переГавати не треба: middleware авторизації зберігає його в контекстній змінній Š“Š»Ń кожного Š·Š°ŠæŠøŃ‚Ńƒ. -* ŠŸŠ¾Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ **той самий об'єкт, ŃŠŗŠøŠ¹ побуГував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і буГь-ŃŠŗŃ– ГоГаткові `claims`, ŃŠŗŃ– ви ГоГали. Це й є зачіпка Š“Š»Ń правил на рівні окремих Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń–Š²: прочитайте scopes і віГмовте. -* Поза автентифікованим HTTP-запитом він повертає `None`. Š£ пам'яті й через `stdio` це завжГи `None`. +* Це ŠæŃ€Š°Ń†ŃŽŃ” в Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°Ń…, Ń€ŠµŃŃƒŃ€ŃŠ°Ń… і промптах, і нічого не треба переГавати Š²Ń€ŃƒŃ‡Š½Ńƒ: middleware авторизації зберігає його в контекстній змінній Š“Š»Ń кожного Š·Š°ŠæŠøŃ‚Ńƒ. +* ŠŸŠ¾Š²ŠµŃ€Ń‚Š°Ń”Ń‚ŃŒŃŃ **той самий об'єкт, що його побуГував ваш верифікатор**: `client_id`, `scopes`, `subject`, `expires_at` і буГь-ŃŠŗŃ– ГоГаткові `claims`, ŃŠŗŃ– ви ГоГали. Це і є зачіпка Š“Š»Ń правил на рівні окремих Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Ń–Š²: прочитайте scopes і віГмовте. +* Поза автентифікованим HTTP-запитом Ń„ŃƒŠ½ŠŗŃ†Ń–Ń повертає `None`. Š£ пам'яті та через `stdio` це завжГи `None`. Викличте `whoami` Š· `Authorization: Bearer alice-token` — і моГель прочитає: @@ -109,22 +113,22 @@ alice (scopes: notes:read) SDK Гає вам половину сервера Ń€ŠµŃŃƒŃ€ŃŃ–Š²: перевірити, оголосити, віГмовити. Він не Гає сторінки Š²Ń…Š¾Š“Ńƒ, екрана згоГи чи токена. -Щоб побачити всі три сторони в русі, Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `examples/servers/simple-auth/` Š· Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€Ń–ŃŽ SDK (невеликий сервер авторизації та сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š², Š½Š°Š»Š°ŃˆŃ‚Š¾Š²Š°Š½ŠøŠ¹ точно ŃŠŗ на цій сторінці), а потім ŃŠæŃ€ŃŠ¼ŃƒŠ¹Ń‚Šµ на нього `examples/clients/simple-auth-client/`, щоб пройти повний ŃˆŠ»ŃŃ… Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń й Š¾Ń‚Ń€ŠøŠ¼Š°Š½Š½Ń токена. +Щоб побачити всі три сторони в русі, Š·Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `examples/servers/simple-auth/` Š· Ń€ŠµŠæŠ¾Š·ŠøŃ‚Š¾Ń€Ń–ŃŽ SDK (невеликий сервер авторизації та сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š², Š½Š°Š»Š°ŃˆŃ‚Š¾Š²Š°Š½ŠøŠ¹ точно ŃŠŗ на цій сторінці), а потім ŃŠæŃ€ŃŠ¼ŃƒŠ¹Ń‚Šµ на нього `examples/clients/simple-auth-client/` — і пройГете весь ŃˆŠ»ŃŃ… віГ Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń Го токена. !!! info - Š„ Š“Ń€ŃƒŠ³ŠøŠ¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, `auth_server_provider=`, ŃŠŗŠøŠ¹ Š²Š±ŃƒŠ“Š¾Š²ŃƒŃ” повноцінний сервер - авторизації Š²ŃŠµŃ€ŠµŠ“ŠøŠ½Ńƒ вашого MCP-сервера. Він Š·'ŃŠ²ŠøŠ²ŃŃ ще Го Ń€Š¾Š·Š“Ń–Š»ŠµŠ½Š½Ń AS/RS, навколо ŃŠŗŠ¾Š³Š¾ - побуГовано ŃŠæŠµŃ†ŠøŃ„Ń–ŠŗŠ°Ń†Ń–ŃŽ авторизації MCP. ŠŠ¾Š²ŠøŠ¼ серверам не сліГ Го нього Š²Š“Š°Š²Š°Ń‚ŠøŃŃ. + Š„ ще Š“Ń€ŃƒŠ³ŠøŠ¹ Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚ ŠŗŠ¾Š½ŃŃ‚Ń€ŃƒŠŗŃ‚Š¾Ń€Š°, `auth_server_provider=`, ŃŠŗŠøŠ¹ Š²Š±ŃƒŠ“Š¾Š²ŃƒŃ” повноцінний сервер авторизації + Š²ŃŠµŃ€ŠµŠ“ŠøŠ½Ńƒ вашого MCP-сервера. Він Š·'ŃŠ²ŠøŠ²ŃŃ Ń€Š°Š½Ń–ŃˆŠµ за Ń€Š¾Š·Š“Ń–Š»ŠµŠ½Š½Ń AS/RS, навколо ŃŠŗŠ¾Š³Š¾ побуГована ŃŠæŠµŃ†ŠøŃ„Ń–ŠŗŠ°Ń†Ń–Ń + авторизації MCP. Š£ нових серверах його Š²ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ²Š°Ń‚Šø не сліГ. -Дервер авторизації також може ŠæŃ€ŠøŠ¹Š½ŃŃ‚Šø піГписане Ń‚Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń корпоративного ŠæŠ¾ŃŃ‚Š°Ń‡Š°Š»ŃŒŠ½ŠøŠŗŠ° іГентичності Š·Š°Š¼Ń–ŃŃ‚ŃŒ того, щоб ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ прохоГив екран згоГи, і SDK ŠæŃ–Š“Ń‚Ń€ŠøŠ¼ŃƒŃ” обиГва боки Ń†ŃŒŠ¾Š³Š¾ Š¾Š±Š¼Ń–Š½Ńƒ. ŠŸŃ€Š¾ цей grant і клієнта, що його преГ'ŃŠ²Š»ŃŃ”, — на сторінці **[Š¢Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń іГентичності](../client/identity-assertion.md)**. +Дервер авторизації може також ŠæŃ€ŠøŠ¹Š½ŃŃ‚Šø піГписане Ń‚Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń віГ корпоративного ŠæŠ¾ŃŃ‚Š°Ń‡Š°Š»ŃŒŠ½ŠøŠŗŠ° іГентичності Š·Š°Š¼Ń–ŃŃ‚ŃŒ того, щоб ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡ ŠæŃ€Š¾ŠŗŠ»Š°Ń†ŃƒŠ²Š°Š² екран згоГи, і SDK ŠæŃ–Š“Ń‚Ń€ŠøŠ¼ŃƒŃ” обиГві сторони Ń†ŃŒŠ¾Š³Š¾ Š¾Š±Š¼Ń–Š½Ńƒ. Цей грант і клієнт, що його преГ'ŃŠ²Š»ŃŃ”, описані на сторінці **[Š¢Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń іГентичності](../client/identity-assertion.md)**. ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} -* Через Streamable HTTP ваш сервер — це **сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š²** OAuth 2.1: він ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” токени й ніколи їх не виГає. -* `TokenVerifier` — це Š²ŃŃ ŠæŠ¾Š²ŠµŃ€Ń…Š½Ń інтеграції: оГин асинхронний метоГ, на вхоГі токен, на вихоГі `AccessToken | None`. -* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжГи Š¹Š“ŃƒŃ‚ŃŒ разом. +* Через Streamable HTTP ваш сервер — **сервер Ń€ŠµŃŃƒŃ€ŃŃ–Š²** OAuth 2.1: він ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” токени й ніколи їх не виГає. +* `TokenVerifier` — уся ŠæŠ¾Š²ŠµŃ€Ń…Š½Ń інтеграції: оГин асинхронний метоГ, токен на вхоГі, `AccessToken | None` на вихоГі. +* `token_verifier=` і `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` завжГи Š¹Š“ŃƒŃ‚ŃŒ у парі. * SDK ŠæŃƒŠ±Š»Ń–ŠŗŃƒŃ” [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata за Š°Š“Ń€ŠµŃŠ¾ŃŽ `/.well-known/oauth-protected-resource/...` і віГповіГає на неавтентифіковані запити коГом 401, чий заголовок `WWW-Authenticate` Š²ŠŗŠ°Š·ŃƒŃ” на них. ŠžŃ†Šµ й уся Ń–ŃŃ‚Š¾Ń€Ń–Ń Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń. * `get_access_token()` у буГь-ŃŠŗŠ¾Š¼Ńƒ Š¾Š±Ń€Š¾Š±Š½ŠøŠŗŃƒ — це той, хто викликає. -* ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń — справа HTTP. `stdio` та клієнт у пам'яті ніколи її не Š±Š°Ń‡Š°Ń‚ŃŒ. +* ŠŠ²Ń‚Š¾Ń€ŠøŠ·Š°Ń†Ń–Ń — справа HTTP. `stdio` і тестовий клієнт у пам'яті ніколи її не Š±Š°Ń‡Š°Ń‚ŃŒ. -ŠšŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ° половина (Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń вашого сервера авторизації й Š¾Ń‚Ń€ŠøŠ¼Š°Š½Š½Ń токена за вас) — на сторінці **[ŠšŠ»Ń–Ń”Š½Ń‚Šø OAuth](../client/oauth-clients.md)**. А клієнт, ŃŠŗŠøŠ¹ *ŃŃ‚Š²ŠµŃ€Š“Š¶ŃƒŃ”* Ń–Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ Š·Š°Š¼Ń–ŃŃ‚ŃŒ того, щоб Š·Š°ŠæŠøŃ‚ŃƒŠ²Š°Ń‚Šø її в ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°, — на сторінці **[Š¢Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń іГентичності](../client/identity-assertion.md)**. +ŠšŠ»Ń–Ń”Š½Ń‚ŃŃŒŠŗŠ° половина (Š²ŠøŃŠ²Š»ŠµŠ½Š½Ń сервера авторизації й Š¾Ń‚Ń€ŠøŠ¼Š°Š½Š½Ń токена за вас) — на сторінці **[OAuth-клієнти](../client/oauth-clients.md)**. А клієнт, ŃŠŗŠøŠ¹ *ŃŃ‚Š²ŠµŃ€Š“Š¶ŃƒŃ”* Ń–Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ Š·Š°Š¼Ń–ŃŃ‚ŃŒ того, щоб просити її в ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°, — на сторінці **[Š¢Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń іГентичності](../client/identity-assertion.md)**. diff --git a/i18n/uk/pages/run/deploy.md b/i18n/uk/pages/run/deploy.md index fe83a33e..a09c4928 100644 --- a/i18n/uk/pages/run/deploy.md +++ b/i18n/uk/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # Š Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń та Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŠ²Š°Š½Š½Ń {#deploy-scale} @@ -47,6 +47,22 @@ translation: Ń€Š¾Š·Š³Š¾Ń€Š½ŃƒŃ‚ŠøŠ¹ сервер, ŃŠŗŠøŠ¹ Š²Ń–Š“Ń…ŠøŠ»ŃŃ” кожне Š·'Ń”Š“Š½Š°Š½Š½Ń, — це список Гозволених Host, Гоки не ГовеГено протилежне. **[Š£ŃŃƒŠ½ŠµŠ½Š½Ń несправностей](../troubleshooting.md)** теж ŠæŠ¾Ń‡ŠøŠ½Š°Ń”Ń‚ŃŒŃŃ звіГси. +## За проксі, що Š·Š°Š²ŠµŃ€ŃˆŃƒŃ” TLS {#behind-a-tls-terminating-proxy} + +Якщо TLS Š·Š°Š²ŠµŃ€ŃˆŃƒŃ”Ń‚ŃŒŃŃ на проксі (ingress, балансувальник Š½Š°Š²Š°Š½Ń‚Š°Š¶ŠµŠ½Š½Ń, Caddy, nginx), а uvicorn за ним Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” звичайний HTTP, ŃŠŗŠ°Š¶Ń–Ń‚ŃŒ uvicorn Š“Š¾Š²Ń–Ń€ŃŃ‚Šø заголовкам `X-Forwarded-*` віГ проксі: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +Без Ń†ŃŒŠ¾Š³Š¾ Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ вважає, що його Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃŽŃ‚ŃŒ через `http://`, і буГь-ŃŠŗŠµ ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń, ŃŠŗŠµ він виГає (зазвичай це `/mcp` → `/mcp/`), веГе на `http://…`. Python-клієнт Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ”Ń‚ŃŒŃŃ перехоГити Š· HTTPS-енГпоінта на звичайний HTTP і ŠæŃ€ŃŠ¼Š¾ про це каже: + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +Тимчасовий обхіГ на боці клієнта — вказати точний URL, ŃŠŗŠøŠ¹ Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” сервер (`https://mcp.example.com/mcp/`, Š·Ń– ŃŠŗŃ–ŃŠ½Š¾ŃŽ Ń€ŠøŃŠŗŠ¾ŃŽ наприкінці), щоб ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń не було взагалі. Дправжнє Š²ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń — ŠæŃ€Š°ŠæŠ¾Ń€ŠµŃ†ŃŒ вище. `FORWARDED_ALLOW_IPS` — той самий параметр у Š²ŠøŠ³Š»ŃŠ“Ń– змінної сереГовища; `*` Š“Š¾Š²Ń–Ń€ŃŃ” кожному ŠæŃ€Š¾Š¼Ń–Š¶Š½Š¾Š¼Ńƒ вузлу, що ŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾ лише тоГі, коли Го uvicorn не може Š“Ń–ŃŃ‚Š°Ń‚ŠøŃŃ ніхто, крім проксі. + ## Робочі процеси, і Š“Š»Ń кого потрібні липкі сесії {#workers-and-who-has-to-be-sticky} Щойно ім'я хоста віГповіГає, поставте за ним Š±Ń–Š»ŃŒŃˆŠµ ніж оГин робочий процес. Š”Š»Ń Ń†ŃŒŠ¾Š³Š¾ в SDK немає жоГного перемикача; Starlette-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŃŽŃ‚ŃŒ так само, ŃŠŗ і буГь-ŃŠŗŠøŠ¹ ASGI-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, — ŠæŠµŃ€ŠµŠ“Š°ŃŽŃ‚ŃŒ об'єкт Ń‡Š¾Š¼ŃƒŃŃŒ, що вміє ŃŃ‚Š²Š¾Ń€ŃŽŠ²Š°Ń‚Šø Гочірні процеси: @@ -170,6 +186,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## ŠŸŃ–Š“ŃŃƒŠ¼ŠŗŠø {#recap} * За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ віГповіГає лише на запити, аГресовані localhost. `transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` — це ворота Š²ŠøŃ…Š¾Š“Ńƒ в світ: Гоки його не переГати, кожен запит за справжнім іменем хоста — це `421`, а причина є лише в лозі сервера. +* За проксі, що Š·Š°Š²ŠµŃ€ŃˆŃƒŃ” TLS, Š·Š°ŠæŃƒŃŠŗŠ°Š¹Ń‚Šµ uvicorn Š· `--proxy-headers --forwarded-allow-ips=...`, Ń–Š½Š°ŠŗŃˆŠµ його ŠæŠµŃ€ŠµŠ½Š°ŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń Š²ŠµŠ“ŃƒŃ‚ŃŒ на `http://`, і клієнт їх Š²Ń–Š“Ń…ŠøŠ»ŃŃ”. * ŠŠ° 2026-07-28 немає сесії і нічого, Го чого балансувальник Š½Š°Š²Š°Š½Ń‚Š°Š¶ŠµŠ½Š½Ń мав би прив'ŃŠ·ŃƒŠ²Š°Ń‚ŠøŃŃ. `stateless_http=True` — перемикач лише Š“Š»Ń старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń, бо ŃŃƒŃ‡Š°ŃŠ½ŠøŠ¹ запит Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŃƒŃ”Ń‚ŃŒŃŃ й Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ ще Го того, ŃŠŗ цей ŠæŃ€Š°ŠæŠ¾Ń€ŠµŃ†ŃŒ ŃƒŠ·Š°Š³Š°Š»Ń– ŠæŃ€Š¾Ń‡ŠøŃ‚Š°ŃŽŃ‚ŃŒ. * Типовий ŠŗŠ»ŃŽŃ‡ `requestState` — це `os.urandom(32)`, Š²ŠøŠæŃƒŃ‰ŠµŠ½ŠøŠ¹ окремо в кожному процесі. Š‘Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Š° повторна спроба, що потрапила Го Ń–Š½ŃˆŠ¾Š³Š¾ робочого ŠæŃ€Š¾Ń†ŠµŃŃƒ, Š·Š±Š¾Ń—Ń‚ŃŒ Š· `-32602` *Ā«Invalid or expired requestStateĀ»*. * Š’ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń — `RequestStateSecurity(keys=[...])` **і** те саме ім'я сервера на кожному ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€Ń–. Ім'я — типове Ń‚Š²ŠµŃ€Š“Š¶ŠµŠ½Š½Ń про Š°ŃƒŠ“ŠøŃ‚Š¾Ń€Ń–ŃŽ токена. Ті самі ŠŗŠ»ŃŽŃ‡Ń–, те саме ім'я. diff --git a/i18n/uk/pages/run/index.md b/i18n/uk/pages/run/index.md index d5f388f3..5d4f90c8 100644 --- a/i18n/uk/pages/run/index.md +++ b/i18n/uk/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # Š—Š°ŠæŃƒŃŠŗ сервера {#running-your-server} @@ -75,6 +75,11 @@ Inspector Ń€Š¾Š±ŠøŃ‚ŃŒ рівно те саме, що й справжній хо * `max_request_body_size`: Š½Š°Š¹Š±Ń–Š»ŃŒŃˆŠøŠ¹ ŠæŃ€ŠøŠ¹Š½ŃŃ‚Š½ŠøŠ¹ розмір тіла Š·Š°ŠæŠøŃ‚Ńƒ в байтах. За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ 4 ŠœŃ–Š‘; Š±Ń–Š»ŃŒŃˆŃ– запити Š¾Ń‚Ń€ŠøŠ¼ŃƒŃŽŃ‚ŃŒ HTTP 413 ще Го Ń€Š¾Š·Š±Š¾Ń€Ńƒ чи ŃŃ‚Š²Š¾Ń€ŠµŠ½Š½Ń сесії. Š—Š±Ń–Š»ŃŒŃˆŃƒŠ¹Ń‚Šµ його лише тоГі, коли легітимні MCP-ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń ŠæŠµŃ€ŠµŠ²ŠøŃ‰ŃƒŃŽŃ‚ŃŒ цей розмір. +* `session_idle_timeout`: ŃŠŗŃ–Š»ŃŒŠŗŠø секунГ ŃŠµŃŃ–Ń старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń може ŠæŃ€Š¾ŃŃ‚Š¾ŃŽŠ²Š°Ń‚Šø без жоГного Š·Š°ŠæŠøŃ‚Ńƒ в роботі, + ŠæŠµŃ€Ńˆ ніж сервер її закриє. За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ 1800. `None` вимикає це. Див. + [Час Š¶ŠøŃ‚Ń‚Ń сесії та Š¾Š±Š¼ŠµŠ¶ŠµŠ½Š½Ń](legacy-clients.md#session-lifetime-and-limits). +* `max_sessions`: ŃŠŗŃ–Š»ŃŒŠŗŠø сесій старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń оГин процес ŃƒŃ‚Ń€ŠøŠ¼ŃƒŃ” оГночасно. За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ 10 000. `None` + знімає Š¾Š±Š¼ŠµŠ¶ŠµŠ½Š½Ń. ŠžŠæŠøŃŠ°Š½Š¾ в Ń‚Š¾Š¼Ńƒ самому розГілі. * `event_store`, `retry_interval`, `transport_security`: Š²Ń–Š“Š½Š¾Š²Š»ŃŽŠ²Š°Š½Ń–ŃŃ‚ŃŒ і захист віГ DNS-rebinding. Вони Š¼Š¾Š¶ŃƒŃ‚ŃŒ зачекати, Гоки ви не розгорнете сервер ГеінГе, крім localhost; `transport_security` описано на сторінці **[Š Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń та Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŠ²Š°Š½Š½Ń](deploy.md)**. !!! warning diff --git a/i18n/uk/pages/run/legacy-clients.md b/i18n/uk/pages/run/legacy-clients.md index 099aa9d3..3f8c9661 100644 --- a/i18n/uk/pages/run/legacy-clients.md +++ b/i18n/uk/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń клієнтів старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń {#serving-legacy-clients} @@ -21,15 +21,25 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŃƒŃ” кожен запит за його заголовк ## ŠžŠ“ŠøŠ½ обробник, обиГва ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń {#one-handler-both-eras} -ŠžŃŃŒ Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŃŠŗŠ¾Š¼Ńƒ треба Гещо запитати в ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°, і клієнти обох ŠæŠ¾ŠŗŠ¾Š»Ń–Š½ŃŒ, що його Š²ŠøŠŗŠ»ŠøŠŗŠ°ŃŽŃ‚ŃŒ: +ŠžŃŃŒ Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŃŠŗŠ¾Š¼Ńƒ треба Гещо запитати в ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°: -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃ” оГнієї речі, ŃŠŗŠ¾Ń— моГель не наГала: ŃŠŗŃ–Š»ŃŒŠŗŠø примірників. Через `Annotated[..., Resolve(ask_quantity)]` Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ це й Š¾Š³Š¾Š»Š¾ŃˆŃƒŃ” (Š“Š¾ŠŗŠ»Š°Š“Š½Ń–ŃˆŠµ — на сторінці **[Залежності](../handlers/dependencies.md)**). ŠŃ–Ń‰Š¾ в `reserve` не називає Š²ŠµŃ€ŃŃ–ŃŽ, не ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” Š¼Š¾Š¶Š»ŠøŠ²Ń–ŃŃ‚ŃŒ і не Ń€Š¾Š·Š³Š°Š»ŃƒŠ¶ŃƒŃ”Ń‚ŃŒŃŃ. -ŠžŠ±ŠøŠ“Š²Š° клієнти віГкриті **оГночасно**, на Ń‚Š¾Š¼Ńƒ самому об'єкті `mcp`. `mode="legacy"` Š²ŠøŠŗŠ¾Š½ŃƒŃ” Ń€ŃƒŠŗŠ¾ŃŃ‚ŠøŃŠŗŠ°Š½Š½Ń `initialize` — саме те Š·'Ń”Š“Š½Š°Š½Š½Ń, ŃŠŗŠµ віГкриває клієнт Го 2026 Ń€Š¾ŠŗŃƒ. Š”Ń€ŃƒŠ³ŠøŠ¹ клієнт бере Š·Š½Š°Ń‡ŠµŠ½Š½Ń за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ і ŠæŠ¾Ń‚Ń€Š°ŠæŠ»ŃŃ” на `2026-07-28`. +Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ його через HTTP — і ось клієнти обох ŠæŠ¾ŠŗŠ¾Š»Ń–Š½ŃŒ, що його Š²ŠøŠŗŠ»ŠøŠŗŠ°ŃŽŃ‚ŃŒ: + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +ŠžŠ±ŠøŠ“Š²Š° клієнти віГкриті **оГночасно**, Го того самого сервера, що ŠæŃ€Š°Ń†ŃŽŃ”. `mode="legacy"` Š²ŠøŠŗŠ¾Š½ŃƒŃ” Ń€ŃƒŠŗŠ¾ŃŃ‚ŠøŃŠŗŠ°Š½Š½Ń `initialize` — саме те Š·'Ń”Š“Š½Š°Š½Š½Ń, ŃŠŗŠµ віГкриває клієнт Го 2026 Ń€Š¾ŠŗŃƒ. Š”Ń€ŃƒŠ³ŠøŠ¹ клієнт бере Š·Š½Š°Ń‡ŠµŠ½Š½Ń за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ і ŠæŠ¾Ń‚Ń€Š°ŠæŠ»ŃŃ” на `2026-07-28`. Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ `python client.py` Š· Š“Ń€ŃƒŠ³Š¾Š³Š¾ термінала: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -62,6 +72,41 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŃƒŃ” кожен запит за його заголовк Š½Š°Š“ŃŠøŠ»Š°Š½Š½Ń ŠæŃ€Š¾ŠæŃƒŃ‰ŠµŠ½ŠøŃ… SSE-поГій ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ, що перепіГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ Го *тієї самої* сесії), а не сховище сесій. Він ніколи не Ń€Š¾Š±ŠøŃ‚ŃŒ ŃŠµŃŃ–ŃŽ Š“Š¾ŃŃŠ¶Š½Š¾ŃŽ Š· Ń–Š½ŃˆŠ¾Š³Š¾ ŠæŃ€Š¾Ń†ŠµŃŃƒ. +## Час Š¶ŠøŃ‚Ń‚Ń сесій і Š¾Š±Š¼ŠµŠ¶ŠµŠ½Š½Ń {#session-lifetime-and-limits} + +Š”ŠµŃŃ–Ń старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń не живе вічно, і оГин процес не тримає їх необмежену ŠŗŃ–Š»ŃŒŠŗŃ–ŃŃ‚ŃŒ. +Цим ŠŗŠµŃ€ŃƒŃŽŃ‚ŃŒ Гва Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń. ŠžŠ±ŠøŠ“Š²Š° — іменовані Š°Ń€Š³ŃƒŠ¼ŠµŠ½Ń‚Šø `run()`, `streamable_http_app()` +і `Server.streamable_http_app()`. Š”ŃƒŃ‡Š°ŃŠ½Ń– Š·'Ń”Š“Š½Š°Š½Š½Ń (`2026-07-28`) і `stateless_http=True` +сесій не Š¼Š°ŃŽŃ‚ŃŒ, тож жоГне Š· Š½Š°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½ŃŒ їх не ŃŃ‚Š¾ŃŃƒŃ”Ń‚ŃŒŃŃ. + +| ŠŠ°Š»Š°ŃˆŃ‚ŃƒŠ²Š°Š½Š½Ń | За Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ | Що Ń€Š¾Š±ŠøŃ‚ŃŒ | Що Š±Š°Ń‡ŠøŃ‚ŃŒ клієнт | ŠÆŠŗ Š²ŠøŠ¼ŠŗŠ½ŃƒŃ‚Šø | +|---|---|---|---|---| +| `session_idle_timeout` | `1800` (30 хв) | Закриває ŃŠµŃŃ–ŃŽ, у ŃŠŗŃ–Š¹ ŃŃ‚Ń–Š»ŃŒŠŗŠø Ń‡Š°ŃŃƒ нічого не було в роботі. | `404 Session not found`. Š”Š¾Š²ŠµŠ“ŠµŃ‚ŃŒŃŃ знову виконати `initialize`. | `None` | +| `max_sessions` | `10_000` | Š’Ń–Š“Š¼Š¾Š²Š»ŃŃ”Ń‚ŃŒŃŃ віГкривати ŃŠµŃŃ–ŃŽ понаГ цю ŠŗŃ–Š»ŃŒŠŗŃ–ŃŃ‚ŃŒ. ŠŠ°ŃŠ²Š½Ń– сесії не Š·Š°Ń‡Ń–ŠæŠ°ŃŽŃ‚ŃŒŃŃ, і нічого не Š²ŠøŃ‚Ń–ŃŠ½ŃŃ”Ń‚ŃŒŃŃ. | `503 Too many open sessions` Š· коГом JSON-RPC `-32603`. | `None` | + +Що Š²Š²Š°Š¶Š°Ń”Ń‚ŃŒŃŃ «в роботі»: + +* ВіГкритий потік `GET`. ŠšŠ»Ń–Ń”Š½Ń‚Šø SDK Ń‚Ń€ŠøŠ¼Š°ŃŽŃ‚ŃŒ оГин віГкритим, тож ŃŠµŃŃ–Ń піГ'єГнаного + клієнта ніколи не спливає. +* Запит, на ŃŠŗŠøŠ¹ Госі Š²Ń–Š“ŠæŠ¾Š²Ń–Š“Š°ŃŽŃ‚ŃŒ. Виклик Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°, що триває Говше за тайм-Š°ŃƒŃ‚, не + ŠæŠµŃ€ŠµŃ€ŠøŠ²Š°Ń”Ń‚ŃŒŃŃ, а віГлік ŠæŠ¾Ń‡ŠøŠ½Š°Ń”Ń‚ŃŒŃŃ лише ŠæŃ–ŃŠ»Ń його Š·Š°Š²ŠµŃ€ŃˆŠµŠ½Š½Ń. +* Š‘Ń–Š»ŃŒŃˆŠµ нічого. ŠœŃ–Š¶ запитами гоГинник іГе. Š‘ŃƒŠ“ŃŒ-ŃŠŗŠøŠ¹ запит у сесії ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŠŗŠ°Ń” його, + зокрема й `ping`. Щойно ŃŠµŃŃ–Ń спливла, ніщо її не Š¾Š¶ŠøŠ²ŠøŃ‚ŃŒ. + +ŠšŠ»Ń–Ń”Š½Ń‚, що Š·Š°Š²ŠµŃ€ŃˆŃƒŃ” ŃŠ²Š¾ŃŽ ŃŠµŃŃ–ŃŽ через `DELETE`, Š·Š²Ń–Š»ŃŒŠ½ŃŃ” її негайно. Так само й клієнт, чий +початковий запит віГхилено. + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +ŠžŠ±ŠøŠ“Š²Ń– поГії ŠæŠ¾Ń‚Ń€Š°ŠæŠ»ŃŃŽŃ‚ŃŒ у лог сервера. Š”ŠæŠ»ŠøŠ²Š°Š½Š½Ń — це `Session idle timeout` на рівні +`INFO`. ВіГмова у віГкритті — `Refusing to open a new session: sessions are already open` +на рівні `WARNING`. + +ŠžŠ±Š¼ŠµŠ¶ŠµŠ½Š½Ń Š“Ń–ŃŽŃ‚ŃŒ на процес. Із чотирма робочими процесами ŃŃ‚ŠµŠ»Ń — ŃƒŃ‡ŠµŃ‚Š²ŠµŃ€Š¾ `max_sessions`, +і кожен робочий процес сам Š·Š°Š²ŠµŃ€ŃˆŃƒŃ” свої сесії за тайм-Š°ŃƒŃ‚Š¾Š¼. + ## ЄГиний перемикач: `stateless_http` {#the-one-knob-stateless_http} Якщо Š»ŠøŠæŠŗŃ–ŃŃ‚ŃŒ — ціна, ŃŠŗŃƒ ви платити не готові, змінити можна рівно оГну річ. @@ -87,8 +132,8 @@ SDK Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·ŃƒŃ” кожен запит за його заголовк !!! check Š—Ń€Š¾Š±Ń–Ń‚ŃŒ Š½ŠµŠæŃ€Š°Š²ŠøŠ»ŃŒŠ½Š¾. `reserve` — той самий Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, що щойно обслужив обох клієнтів. - Š Š¾Š·Š³Š¾Ń€Š½Ń–Ń‚ŃŒ його Š·Ń– `stateless_http=True`, піГ'єГнайте тих самих Гвох клієнтів через HTTP - і викличте його Š· кожного. + Š Š¾Š·Š³Š¾Ń€Š½Ń–Ń‚ŃŒ його Š·Ń– `stateless_http=True`, піГ'єГнайте тих самих Гвох клієнтів і викличте + його Š· кожного. Š”ŃƒŃ‡Š°ŃŠ½ŠøŠ¹ клієнт, ŃŠŗ і Ń€Š°Š½Ń–ŃˆŠµ, Š¾Ń‚Ń€ŠøŠ¼ŃƒŃ” `Reserved 2 of 'Dune'.` Š”ŃƒŃ‡Š°ŃŠ½Š° гілка не Š·Š¼Ń–Š½ŠøŠ»Š°ŃŃ. diff --git a/i18n/uk/pages/troubleshooting.md b/i18n/uk/pages/troubleshooting.md index 2f5df7a2..1b631ed4 100644 --- a/i18n/uk/pages/troubleshooting.md +++ b/i18n/uk/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # Š£ŃŃƒŠ½ŠµŠ½Š½Ń несправностей {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +Ці ŠæŃƒŠ½ŠŗŃ‚Šø Š·Š²ŠµŃ€Ń‚Š°ŃŽŃ‚ŃŒŃŃ Го нього за Š°Š“Ń€ŠµŃŠ¾ŃŽ `http://localhost:8000/mcp`, тож Š·Š°Š»ŠøŃˆŃ‚е його ŠæŃ€Š°Ń†ŃŽŠ²Š°Ń‚Šø через HTTP: + +```console +uv run mcp run server.py --transport streamable-http +``` + Помилки, ŃŠŗŃ– Ń†ŠøŃ‚ŃƒŃ” ця сторінка, справжні: власний набір тестів SDK Š²Ń–Š“Ń‚Š²Š¾Ń€ŃŽŃ” кожну Š· них. ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -67,7 +73,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -75,7 +81,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -252,7 +258,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -Дервер не впізнає `Mcp-Session-Id`, ŃŠŗŠøŠ¹ наГіслав ваш клієнт, майже завжГи Ń‚Š¾Š¼Ńƒ, що сервер **ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ²ŃŃ** (або вас ŃŠæŃ€ŃŠ¼ŃƒŠ²Š°Š»Šø на Ń–Š½ŃˆŠøŠ¹ ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€). Десії Š¶ŠøŠ²ŃƒŃ‚ŃŒ у пам'яті того оГного ŠæŃ€Š¾Ń†ŠµŃŃƒ. +Дервер не впізнає `Mcp-Session-Id`, ŃŠŗŠøŠ¹ наГіслав ваш клієнт. Або сервер **ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ²ŃŃ** (чи вас ŃŠæŃ€ŃŠ¼ŃƒŠ²Š°Š»Šø на Ń–Š½ŃˆŠøŠ¹ ŠµŠŗŠ·ŠµŠ¼ŠæŠ»ŃŃ€), або **сплив строк** сесії, бо ŠæŃ€Š¾Ń‚ŃŠ³Š¾Š¼ `session_idle_timeout` (за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ це 30 хвилин) у ній не було жоГного Š·Š°ŠæŠøŃ‚Ńƒ в роботі. Див. [Час Š¶ŠøŃ‚Ń‚Ń сесії та Š¾Š±Š¼ŠµŠ¶ŠµŠ½Š½Ń](run/legacy-clients.md#session-lifetime-and-limits). Десії Š¶ŠøŠ²ŃƒŃ‚ŃŒ у пам'яті того оГного ŠæŃ€Š¾Ń†ŠµŃŃƒ. Помилки в сервері тут немає. HTTP-Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ — це `404`, тіло ŃŠŗŠ¾Š³Š¾ *є* JSON-RPC, тож, на Š²Ń–Š“Š¼Ń–Š½Ńƒ віГ `421` вище, python `Client` ŠæŠ¾ŠŗŠ°Š·ŃƒŃ” це ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń Гослівно: @@ -262,9 +268,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Š’ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń — перепіГ'Ń”Š“Š½Š°Ń‚ŠøŃŃ: Š²ŠøŠ¹Š“Ń–Ń‚ŃŒ Ń–Š· блоку `async with Client(...)` і Š²Š²Ń–Š¹Š“Ń–Ń‚ŃŒ у новий, ŃŠŗŠøŠ¹ ŃƒŠ·Š³Š¾Š“ŠøŃ‚ŃŒ ŃŠ²Ń–Š¶Ńƒ ŃŠµŃŃ–ŃŽ. Š”Š»Ń Š“Š¾Š²Š³Š¾Š¶ŠøŠ²ŃƒŃ‡Š¾Š³Š¾ клієнта це означає ŠæŠµŃ€ŠµŃ…Š¾ŠæŠ»ŃŽŠ²Š°Ń‚Šø `MCPError` навколо викликів і перепіГ'Ń”Š“Š½ŃƒŠ²Š°Ń‚ŠøŃŃ на це ŠæŠ¾Š²Ń–Š“Š¾Š¼Š»ŠµŠ½Š½Ń, а не ŠæŠ¾Š²Ń‚Š¾Ń€ŃŽŠ²Š°Ń‚Šø спроби всереГині мертвої сесії. -Якщо це Ń‚Ń€Š°ŠæŠ»ŃŃ”Ń‚ŃŒŃŃ *без* ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŠŗŃƒ, у вас ŠæŃ€Š°Ń†ŃŽŃ” Š±Ń–Š»ŃŒŃˆŠµ оГного робочого ŠæŃ€Š¾Ń†ŠµŃŃƒ без липких сесій: кожен робочий процес тримає власну Ń‚Š°Š±Š»ŠøŃ†ŃŽ сесій, тож запит, ŃŠæŃ€ŃŠ¼Š¾Š²Š°Š½ŠøŠ¹ не на той, Š¾ŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ тут. ŠŸŃ€Š¾ це та про Гва Š²ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń (липка Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·Š°Ń†Ń–Ń або `stateless_http=True`) — сторінки **[Š Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń й Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŠ²Š°Š½Š½Ń](run/deploy.md)** і **[ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń клієнтів старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń](run/legacy-clients.md)**. +Якщо це Ń‚Ń€Š°ŠæŠ»ŃŃ”Ń‚ŃŒŃŃ *без* ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŠŗŃƒ і клієнт не мовчав так Говго, у вас ŠæŃ€Š°Ń†ŃŽŃ” Š±Ń–Š»ŃŒŃˆŠµ оГного робочого ŠæŃ€Š¾Ń†ŠµŃŃƒ без липких сесій: кожен робочий процес тримає власну Ń‚Š°Š±Š»ŠøŃ†ŃŽ сесій, тож запит, ŃŠæŃ€ŃŠ¼Š¾Š²Š°Š½ŠøŠ¹ не на той, Š¾ŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ тут. ŠŸŃ€Š¾ це та про Гва Š²ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń (липка Š¼Š°Ń€ŃˆŃ€ŃƒŃ‚ŠøŠ·Š°Ń†Ń–Ń або `stateless_http=True`) — сторінки **[Š Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń й Š¼Š°ŃŃˆŃ‚Š°Š±ŃƒŠ²Š°Š½Š½Ń](run/deploy.md)** і **[ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń клієнтів старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń](run/legacy-clients.md)**. -Š”Š»Ń оператора сервера віГповіГний Ń€ŃŠ“Š¾Šŗ у лозі — `Rejected request with unknown or expired session ID: `. Він ŠæŠøŃˆŠµŃ‚ŃŒŃŃ на рівні `INFO`, тож за звичного порога `WARNING` його не виГно. Бачити його сплесками Š¾Š“Ń€Š°Š·Ńƒ ŠæŃ–ŃŠ»Ń Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń — Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾: кожен піГ'єГнаний клієнт перепіГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ. +Š”Š»Ń оператора сервера віГповіГний Ń€ŃŠ“Š¾Šŗ у лозі — `Rejected request with unknown or expired session ID: `. Він ŠæŠøŃˆŠµŃ‚ŃŒŃŃ на рівні `INFO`, тож за звичного порога `WARNING` його не виГно. Бачити його сплесками Š¾Š“Ń€Š°Š·Ńƒ ŠæŃ–ŃŠ»Ń Ń€Š¾Š·Š³Š¾Ń€Ń‚Š°Š½Š½Ń — Š½Š¾Ń€Š¼Š°Š»ŃŒŠ½Š¾: кожен піГ'єГнаний клієнт перепіГ'Ń”Š“Š½ŃƒŃ”Ń‚ŃŒŃŃ. Якщо ж сплив строк сесії, переГ цим Ń€ŃŠ“ŠŗŠ¾Š¼ іГе `Session idle timeout`, теж на рівні `INFO`. ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -276,7 +282,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif Š’Š°Ńˆ сервер хоче про Ń‰Š¾ŃŃŒ запитати ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°, а цей клієнт ніколи не казав, що його можна питати. -Резолвер еліцитації (elicitation) Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ” Š¾Š“Ń€Š°Š·Ńƒ, ŃŠŗŃ‰Š¾ піГ'єГнаний клієнт не оголосив ŠµŠ»Ń–Ń†ŠøŃ‚Š°Ń†Ń–ŃŽ через Ń„Š¾Ń€Š¼Ńƒ, а `e.error.data` називає, чого саме Š±Ń€Š°ŠŗŃƒŃ”: +Це Bistro питає, ŠæŠµŃ€Ńˆ ніж Š·Š°Š±Ń€Š¾Š½ŃŽŠ²Š°Ń‚Šø, — через резолвер: + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +Š—Š°ŠæŃƒŃŃ‚Ń–Ń‚ŃŒ його Š·Š°Š¼Ń–ŃŃ‚ŃŒ сервера Weather і викличте `book_table` Ń–Š· клієнта, ŃŠŗŠ¾Š¼Ńƒ не переГали `elicitation_callback`. Резолвер Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ” Š¾Š“Ń€Š°Š·Ńƒ, бо піГ'єГнаний клієнт так і не оголосив ŠµŠ»Ń–Ń†ŠøŃ‚Š°Ń†Ń–ŃŽ (elicitation) через Ń„Š¾Ń€Š¼Ńƒ, а `e.error.data` називає, чого саме Š±Ń€Š°ŠŗŃƒŃ”: ```json { @@ -290,7 +302,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -315,14 +327,14 @@ async def main() -> None: Š’Š°Ńˆ обробник ŃŠæŃ€Š¾Š±ŃƒŠ²Š°Š² Š·Š²ŠµŃ€Š½ŃƒŃ‚ŠøŃŃ Го клієнта посереГ Š·Š°ŠæŠøŃ‚Ńƒ на Š·'єГнанні, Ге виклик не має каналу, зГатного нести запит віГ сервера. Š„ три ŠŗŠ¾Š½Ń„Ń–Š³ŃƒŃ€Š°Ń†Ń–Ń— сервера, за ŃŠŗŠøŃ… виклик Š¾ŠæŠøŠ½ŃŃ”Ń‚ŃŒŃŃ в Ń‚Š°ŠŗŠ¾Š¼Ńƒ становищі. -**Š—'Ń”Š“Š½Š°Š½Š½Ń `2026-07-28`: буГь-ŃŠŗŠøŠ¹ транспорт, завжГи.** Š”ŃƒŃ‡Š°ŃŠ½ŠøŠ¹ протокол ŃƒŠ·Š°Š³Š°Š»Ń– не має запитів, ініційованих сервером, тож сервер Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ” ще Го того, ŃŠŗ Ń‰Š¾ŃŃŒ наГіслано. `ctx.elicit()` ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ń– Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° — класичний спосіб на це натрапити (у Š½Š°Š¹ŠæŠµŃ€ŃˆŠ¾Š¼Ńƒ ж тесті в пам'яті, бо `Client(server)` ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” `2026-07-28`, Š½Š°Š²Ń–Ń‚ŃŒ ŃŠŗŃ‰Š¾ його про це не просили), і ŠæŠµŃ€ŠµŠ“Š°Š²Š°Š½Š½Ń `elicitation_callback=` нічого не Š·Š¼Ń–Š½ŃŽŃ”, бо Го клієнта ніколи не Š“Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ запит, на ŃŠŗŠøŠ¹ він міг би віГповісти: +**Š—'Ń”Š“Š½Š°Š½Š½Ń `2026-07-28`: буГь-ŃŠŗŠøŠ¹ транспорт, завжГи.** Š”ŃƒŃ‡Š°ŃŠ½ŠøŠ¹ протокол ŃƒŠ·Š°Š³Š°Š»Ń– не має запитів, ініційованих сервером, тож сервер Š²Ń–Š“Š¼Š¾Š²Š»ŃŃ” ще Го того, ŃŠŗ Ń‰Š¾ŃŃŒ наГіслано. `ctx.elicit()` ŃƒŃŠµŃ€ŠµŠ“ŠøŠ½Ń– Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° — класичний спосіб на це натрапити, зазвичай у Š½Š°Š¹ŠæŠµŃ€ŃˆŠ¾Š¼Ńƒ ж **[тесті](get-started/testing.md)** Ń†ŃŒŠ¾Š³Š¾ Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š° в пам'яті, бо `Client(mcp)` ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” `2026-07-28`, Š½Š°Š²Ń–Ń‚ŃŒ ŃŠŗŃ‰Š¾ його про це не просили. ŠŸŠµŃ€ŠµŠ“Š°Š²Š°Š½Š½Ń `elicitation_callback=` нічого не Š·Š¼Ń–Š½ŃŽŃ”, бо Го клієнта ніколи не Š“Š¾Ń…Š¾Š“ŠøŃ‚ŃŒ запит, на ŃŠŗŠøŠ¹ він міг би віГповісти: ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -364,7 +376,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -417,7 +429,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * `Tool already exists:` у лозі сервера — єГина ознака того, що Гва оГнойменні Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Šø Š·Š»ŠøŠ»ŠøŃŃ в оГин. * ŠžŠ“ŠøŠ½ 421, три Š½Š°ŠæŠøŃŠ°Š½Š½Ń: `Server returned an error response` (python `Client`), `421 Misdirected Request` / `Invalid Host header` (усе Ń–Š½ŃˆŠµ), `Invalid Host header: ` (лог сервера). Š’ŠøŠæŃ€Š°Š²Š»ŠµŠ½Š½Ń: `transport_security=TransportSecuritySettings(allowed_hosts=[...])`. * `Task group is not initialized` -> змонтований Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ, чий хост-Š·Š°ŃŃ‚Š¾ŃŃƒŠ½Š¾Šŗ у ŃŠ²Š¾Ń”Š¼Ńƒ Š¶ŠøŃ‚Ń‚Ń”Š²Š¾Š¼Ńƒ циклі так і не Š²Š²Ń–Š¹ŃˆŠ¾Š² у `mcp.session_manager.run()`. -* `Session not found` -> сервер ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ²ŃŃ; перепіГ'Ń”Š“Š½Š°Š¹Ń‚ŠµŃŃ. +* `Session not found` -> сервер ŠæŠµŃ€ŠµŠ·Š°ŠæŃƒŃŃ‚ŠøŠ²ŃŃ або сплив строк сесії (`session_idle_timeout`); перепіГ'Ń”Š“Š½Š°Š¹Ń‚ŠµŃŃ. * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` ŠæŠ¾Ń‚Ń€ŠµŠ±ŃƒŃ” каналу віГ сервера Го клієнта: Š·'Ń”Š“Š½Š°Š½Š½Ń `2026-07-28` його ніколи не має, `stateless_http=True` забирає канал старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń, а `json_response=True` — канал у межах Š·Š°ŠæŠøŃ‚Ńƒ. Š’ŠøŠŗŠ¾Ń€ŠøŃŃ‚Š¾Š²ŃƒŠ¹Ń‚Šµ резолвер (ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń також потрібен сервер, що зберігає канал). Š”ŃƒŃŃ–Š“Š½Ń помилка `Method not found` — це запит Š¼ŠµŃ‚Š¾Š“Ńƒ, ŃŠŗŠ¾Š³Š¾ немає в ревізії ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ Ń–Š½ŃˆŠ¾Ń— сторони. * `Client did not declare the form elicitation capability ...` і `Elicitation not supported` -> ŠŗŠ»Ń–Ń”Š½Ń‚Ńƒ Š±Ń€Š°ŠŗŃƒŃ” `elicitation_callback=`. * `Invalid or expired requestState` ніколи не ŠæŠ¾ŃŃŠ½ŃŽŃ” ŠæŃ€ŠøŃ‡ŠøŠ½Ńƒ в переГаних Ганих. Лог сервера — ŠæŠ¾ŃŃŠ½ŃŽŃ”; `unknown key` означає, що треба зробити `RequestStateSecurity(keys=[...])` ŃŠæŃ–Š»ŃŒŠ½ŠøŠ¼ Š“Š»Ń всіх робочих процесів. diff --git a/i18n/uk/pages/whats-new.md b/i18n/uk/pages/whats-new.md index 02d1a39e..13296722 100644 --- a/i18n/uk/pages/whats-new.md +++ b/i18n/uk/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # Що нового у v2 {#whats-new-in-v2} @@ -42,11 +42,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 Гавав три вклаГені ŃˆŠ°Ń€Šø: контекстний менеГжер Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ, що виГає сирі потоки, Š¾Š±Š³Š¾Ń€Š½ŃƒŃ‚Ńƒ навколо них `ClientSession` і викликаний Š²Ń€ŃƒŃ‡Š½Ńƒ `await session.initialize()`. Š£ v2 є оГин об'єкт: -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` приймає об'єкт сервера (у пам'яті, без Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ: це сценарій Ń‚ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń), URL (Streamable HTTP), `StdioServerParameters` (піГпроцес stdio) або буГь-ŃŠŗŠøŠ¹ Ń–Š½ŃˆŠøŠ¹ контекстний менеГжер Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ, ŃŠŗ-от `sse_client(...)`. Š’Ń…Ń–Š“ в `async with` піГ'Ń”Š“Š½ŃƒŃ” та ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” Š²ŠµŃ€ŃŃ–ŃŽ ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ, хай ŃŠŗŠøŠ¼ ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½ŃŠ¼ Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ сервер; ŠæŃ–ŃŠ»Ń Ń†ŃŒŠ¾Š³Š¾ `client.server_capabilities` і `client.protocol_version` просто є, ŃŠŗ і `client.server_info`, коли сервер себе Ń–Š“ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” (тепер це `Implementation | None`, бо Ń–Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ у поколінні 2026 необов'ŃŠ·ŠŗŠ¾Š²Š°). Колбеки ŃŠµŠ¼ŠæŠ»ŃŽŠ²Š°Š½Š½Ń й еліцитації, зареєстровані у v1, і Галі ŠæŃ€Š°Ń†ŃŽŃŽŃ‚ŃŒ (їхні тіла Š·Š°Š·Š½Š°ŃŽŃ‚ŃŒ того самого ŠæŠµŃ€ŠµŠ¹Š¼ŠµŠ½ŃƒŠ²Š°Š½Š½Ń Š°Ń‚Ń€ŠøŠ±ŃƒŃ‚Ń–Š² у snake_case, що й усе Ń–Š½ŃˆŠµ на цій сторінці), тепер вони також Š²Ń–Š“ŠæŠ¾Š²Ń–Š“Š°ŃŽŃ‚ŃŒ на запити всереГині Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń–Š² у стилі 2026 (нижче) і Š²ŠøŠŗŠ¾Š½ŃƒŃŽŃ‚ŃŒŃŃ ŠæŠ°Ń€Š°Š»ŠµŠ»ŃŒŠ½Š¾, а не по оГному. `ClientSession` Госі Š»ŠµŠ¶ŠøŃ‚ŃŒ піГ споГом Š“Š»Ń тих, кому потрібна Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–внева ŠæŠ¾Š²ŠµŃ€Ń…Š½Ń, і `client.session` її віГГає; вона теж Š·Š¼Ń–Š½ŠøŠ»Š°ŃŃ (ŠæŃ€Š°Ń†ŃŽŃ” на новому рушії Гиспетчера, і Š“ŠµŃŠŗŃ– її власні ŃŠøŠ³Š½Š°Ń‚ŃƒŃ€Šø Š·Š¼Ń–Š½ŠøŠ»ŠøŃŃ), тож прочитайте **[ŠŸŠ¾ŃŃ–Š±Š½ŠøŠŗ Š· міграції](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, ŠæŠµŃ€Ńˆ ніж ŃŠæŃƒŃŠŗŠ°Ń‚ŠøŃŃ нижче. +`Client` приймає URL (Streamable HTTP), `StdioServerParameters` (піГпроцес stdio), буГь-ŃŠŗŠøŠ¹ Ń–Š½ŃˆŠøŠ¹ контекстний менеГжер Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ, ŃŠŗ-от `sse_client(...)`, або, у тестах, сам об'єкт сервера (у пам'яті, без Ń‚Ń€Š°Š½ŃŠæŠ¾Ń€Ń‚Ńƒ). Š’Ń…Ń–Š“ в `async with` піГ'Ń”Š“Š½ŃƒŃ” та ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” Š²ŠµŃ€ŃŃ–ŃŽ ŠæŃ€Š¾Ń‚Š¾ŠŗŠ¾Š»Ńƒ, хай ŃŠŗŠøŠ¼ ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½ŃŠ¼ Š³Š¾Š²Š¾Ń€ŠøŃ‚ŃŒ сервер; ŠæŃ–ŃŠ»Ń Ń†ŃŒŠ¾Š³Š¾ `client.server_capabilities` і `client.protocol_version` просто є, ŃŠŗ і `client.server_info`, коли сервер себе Ń–Š“ŠµŠ½Ń‚ŠøŃ„Ń–ŠŗŃƒŃ” (тепер це `Implementation | None`, бо Ń–Š“ŠµŠ½Ń‚ŠøŃ‡Š½Ń–ŃŃ‚ŃŒ у поколінні 2026 необов'ŃŠ·ŠŗŠ¾Š²Š°). Колбеки ŃŠµŠ¼ŠæŠ»ŃŽŠ²Š°Š½Š½Ń й еліцитації, зареєстровані у v1, і Галі ŠæŃ€Š°Ń†ŃŽŃŽŃ‚ŃŒ (їхні тіла Š·Š°Š·Š½Š°ŃŽŃ‚ŃŒ того самого ŠæŠµŃ€ŠµŠ¹Š¼ŠµŠ½ŃƒŠ²Š°Š½Š½Ń Š°Ń‚Ń€ŠøŠ±ŃƒŃ‚Ń–Š² у snake_case, що й усе Ń–Š½ŃˆŠµ на цій сторінці), тепер вони також Š²Ń–Š“ŠæŠ¾Š²Ń–Š“Š°ŃŽŃ‚ŃŒ на запити всереГині Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚Ń–Š² у стилі 2026 (нижче) і Š²ŠøŠŗŠ¾Š½ŃƒŃŽŃ‚ŃŒŃŃ ŠæŠ°Ń€Š°Š»ŠµŠ»ŃŒŠ½Š¾, а не по оГному. `ClientSession` Госі Š»ŠµŠ¶ŠøŃ‚ŃŒ піГ споГом Š“Š»Ń тих, кому потрібна Š½ŠøŠ·ŃŒŠŗŠ¾Ń€Ń–внева ŠæŠ¾Š²ŠµŃ€Ń…Š½Ń, і `client.session` її віГГає; вона теж Š·Š¼Ń–Š½ŠøŠ»Š°ŃŃ (ŠæŃ€Š°Ń†ŃŽŃ” на новому рушії Гиспетчера, і Š“ŠµŃŠŗŃ– її власні ŃŠøŠ³Š½Š°Ń‚ŃƒŃ€Šø Š·Š¼Ń–Š½ŠøŠ»ŠøŃŃ), тож прочитайте **[ŠŸŠ¾ŃŃ–Š±Š½ŠøŠŗ Š· міграції](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**, ŠæŠµŃ€Ńˆ ніж ŃŠæŃƒŃŠŗŠ°Ń‚ŠøŃŃ нижче. **[ŠšŠ»Ń–Ń”Š½Ń‚](client/index.md)** Š·Š½Š°Š¹Š¾Š¼ŠøŃ‚ŃŒ Ń–Š· ним, **[Транспорти клієнта](client/transports.md)** Š¾ŠæŠøŃŃƒŃ” чотири форми піГ'Ń”Š“Š½Š°Š½Š½Ń, **[Колбеки клієнта](client/callbacks.md)** — самі колбеки, а **[Š¢ŠµŃŃ‚ŃƒŠ²Š°Š½Š½Ń](get-started/testing.md)** ŠæŠ¾ŠŗŠ°Š·ŃƒŃ” шаблон роботи в пам'яті, що Š·Š°Š¼Ń–Š½ŃŽŃ” Š“Š¾ŠæŠ¾Š¼Ń–Š¶Š½Ńƒ Ń„ŃƒŠ½ŠŗŃ†Ń–ŃŽ `create_connected_server_and_client_session()` Š· v1. @@ -171,11 +171,15 @@ v2 Ń€ŠµŠ°Š»Ń–Š·ŃƒŃ” Ń€ŠµŠ“Š°ŠŗŃ†Ń–ŃŽ 2026-07-28 і Š¾Š±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŃ” **оби Заміна розвертає виклик у зворотний бік. Š†Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚, ŃŠŗŠ¾Š¼Ńƒ Ń‰Š¾ŃŃŒ потрібно віГ ŠŗŠ¾Ń€ŠøŃŃ‚ŃƒŠ²Š°Ń‡Š°, *повертає* Š·Š°ŠæŠøŃ‚Š°Š½Š½Ń (`InputRequiredResult`), клієнт віГповіГає на нього тими самими колбеками, ŃŠŗŃ– мав завжГи, і виклик ŠæŠ¾Š²Ń‚Š¾Ń€ŃŽŃ”Ń‚ŃŒŃŃ Š· прикріпленими Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŠ¼Šø. `Client` веГе цей цикл за вас. ŠŠ° сервері ви ріГко Š±ŃƒŠ“ŃƒŃ”Ń‚Šµ Ń€ŠµŠ·ŃƒŠ»ŃŒŃ‚Š°Ń‚ самі, бо це Ń€Š¾Š±ŠøŃ‚ŃŒ **[Š·Š°Š»ŠµŠ¶Š½Ń–ŃŃ‚ŃŒ](handlers/dependencies.md)**: Š°Š½Š¾Ń‚ŃƒŠ¹Ń‚Šµ параметр `Resolve(ask_quantity)`, Ге `ask_quantity` — звичайна Ń„ŃƒŠ½ŠŗŃ†Ń–Ń, ŃŠŗŃƒ ви ŠæŠøŃˆŠµŃ‚Šµ, і SDK запитає тим механізмом, ŃŠŗŠøŠ¹ ŠæŃ–Š“Ń‚Ń€ŠøŠ¼ŃƒŃ” Š·'Ń”Š“Š½Š°Š½Š½Ń: живим запитом еліцитації на сесії старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń або Š±Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²ŠøŠ¼ обміном на 2026. ŠžŠ“Š½Šµ тіло Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚Š°, обиГва ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń: -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -Цей файл — уся Ń–Š“ŠµŃ в оГному місці: оГин сервер, оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ на основі `Resolve`, і клієнт старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń разом Ń–Š· ŃŃƒŃ‡Š°ŃŠ½ŠøŠ¼ клієнтом, ŃŠŗŃ– обиГва Š¾Ń‚Ń€ŠøŠ¼ŃƒŃŽŃ‚ŃŒ ŃŠ²Š¾ŃŽ Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ, у пам'яті. **[Š‘Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń– запити](handlers/multi-round-trip.md)** ŠæŠ¾ŃŃŠ½ŃŽŃ” механізм (зокрема `request_state`, ŃŠŗŠøŠ¹ SDK Š·Š°ŠæŠµŃ‡Š°Ń‚ŃƒŃ” й ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” за вас); **[Š•Š»Ń–Ń†ŠøŃ‚Š°Ń†Ń–Ń](handlers/elicitation.md)** — саме Š·Š°ŠæŠøŃ‚ŃƒŠ²Š°Š½Š½Ń. +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +Ці Гва файли — уся Ń–Š“ŠµŃ в оГному місці: оГин сервер, оГин Ń–Š½ŃŃ‚Ń€ŃƒŠ¼ŠµŠ½Ń‚ на основі `Resolve`, і клієнт старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń разом Ń–Š· ŃŃƒŃ‡Š°ŃŠ½ŠøŠ¼ клієнтом, ŃŠŗŃ– обиГва Š¾Ń‚Ń€ŠøŠ¼ŃƒŃŽŃ‚ŃŒ ŃŠ²Š¾ŃŽ Š²Ń–Š“ŠæŠ¾Š²Ń–Š“ŃŒ віГ того самого Š·Š°ŠæŃƒŃ‰ŠµŠ½Š¾Š³Š¾ сервера (**[ŠžŠ±ŃŠ»ŃƒŠ³Š¾Š²ŃƒŠ²Š°Š½Š½Ń клієнтів старого ŠæŠ¾ŠŗŠ¾Š»Ń–Š½Š½Ń](run/legacy-clients.md)** розбирає їх покроково). **[Š‘Š°Š³Š°Ń‚Š¾Ń€Š°ŃƒŠ½Š“Š¾Š²Ń– запити](handlers/multi-round-trip.md)** ŠæŠ¾ŃŃŠ½ŃŽŃ” механізм (зокрема `request_state`, ŃŠŗŠøŠ¹ SDK Š·Š°ŠæŠµŃ‡Š°Ń‚ŃƒŃ” й ŠæŠµŃ€ŠµŠ²Ń–Ń€ŃŃ” за вас); **[Š•Š»Ń–Ń†ŠøŃ‚Š°Ń†Ń–Ń](handlers/elicitation.md)** — саме Š·Š°ŠæŠøŃ‚ŃƒŠ²Š°Š½Š½Ń. !!! warning "Це єГине місце, Ге перенесений сервер v1 Š·Š¼Ń–Š½ŃŽŃ” ŠæŠ¾Š²ŠµŠ“Ń–Š½ŠŗŃƒ" ŠŸŠµŃ€ŃˆŠøŠ¼Šø на це Š½Š°Ń‚Ń€Š°ŠæŠ»ŃŃŽŃ‚ŃŒ Š²Š°ŃˆŃ– власні тести: `Client(mcp)` за Š·Š°Š¼Š¾Š²Ń‡ŃƒŠ²Š°Š½Š½ŃŠ¼ ŃƒŠ·Š³Š¾Š“Š¶ŃƒŃ” Š· вашим сервером v2 diff --git a/i18n/zh-hant/pages/advanced/apps.md b/i18n/zh-hant/pages/advanced/apps.md index 42fe76af..73884b17 100644 --- a/i18n/zh-hant/pages/advanced/apps.md +++ b/i18n/zh-hant/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK ä»„å…§å»ŗēš„ `Apps` ę““å……åŠŸčƒ½ļ¼ˆ`io.modelcontextprotocol/ui`ļ¼‰ęä¾›é€™ ## ęœ‰éŒ¶é¢ēš„ę™‚é˜ {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ HTML ęœ¬čŗ«ęœƒē›£č½äø»ę©Ÿēš„ `postMessage` äø¦é”Æē¤ŗēµęžœć€‚ēœŸę­£ēš„ app č«‹ ęØ”åž‹č®€ēš„ę˜Æ `content`ļ¼›iframe ę˜Æēµ¦äŗŗēœ‹ēš„ć€‚ę”Æę“ UI ēš„äø»ę©Ÿē…§ęØ£ęœƒęŠŠę–‡å­—ēµęžœé¤µēµ¦ęØ”åž‹ļ¼Œč€Œē“”ę–‡å­—ē”Øęˆ¶ē«Æ**åŖ**ęœƒę‹æåˆ°é‚£å€‹ć€‚ę‰€ä»„ęØ™ęŗ–åšę³•ę˜Æäø€å€‹å·„å…·ļ¼Œå…©ēØ®ē­”ę”ˆć€‚å†ēœ‹äø€ę¬” `get_time`: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -åŖęœ‰ē•¶ē”Øęˆ¶ē«Æå®£å‘Šäŗ† `io.modelcontextprotocol/ui` ę““å……åŠŸčƒ½ļ¼Œ**č€Œäø”**åœØå®ƒēš„ `mimeTypes` čØ­å®šč£”åˆ—å‡ŗ `text/html;profile=mcp-app` ę™‚ļ¼Œ`client_supports_apps(ctx)` ę‰ęœƒę˜Æ `True`ć€‚é€™å€‹ę¬„ä½ę˜Æåæ…å”«ēš„ļ¼Œę‰€ä»„ēœē•„å®ƒēš„ē”Øęˆ¶ē«Æäøē®—ę•øć€‚åŒäø€å€‹ęŖ”ę”ˆč£”ēš„ `main()` å®£å‘Šēš„ę­£ę˜Æé€™äŗ›ļ¼šå”å•†ēš„ē”Øęˆ¶ē«Æé‚£äø€åŠļ¼Œę–¼ę˜Æå›žä¾†ēš„ę˜Æč±åÆŒē‰ˆēš„ē­”ę”ˆć€‚ +åŖęœ‰ē•¶ē”Øęˆ¶ē«Æå®£å‘Šäŗ† `io.modelcontextprotocol/ui` ę““å……åŠŸčƒ½ļ¼Œ**č€Œäø”**åœØå®ƒēš„ `mimeTypes` čØ­å®šč£”åˆ—å‡ŗ `text/html;profile=mcp-app` ę™‚ļ¼Œ`client_supports_apps(ctx)` ę‰ęœƒę˜Æ `True`ć€‚é€™å€‹ę¬„ä½ę˜Æåæ…å”«ēš„ļ¼Œę‰€ä»„ēœē•„å®ƒēš„ē”Øęˆ¶ē«Æäøē®—ę•øć€‚ä»„äø‹ę˜Æå”å•†äø­ē”Øęˆ¶ē«Æēš„é‚£äø€åŠļ¼š + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +透過 HTTP ęä¾› `server.py`ļ¼Œē„¶å¾ŒåœØē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œē”Øęˆ¶ē«Æļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +å›žä¾†ēš„ę˜Æč±åÆŒē‰ˆēš„ē­”ę”ˆć€‚ęŠŠ `extensions=[APPS_SUPPORT]` 從 `Client` å‘¼å«č£”ę‹æęŽ‰ļ¼ŒåŒäø€å€‹ēØ‹å¼å°±ęœƒę”¹å°å‡ŗ `The time is 2026-06-26T12:00:00Z.`ļ¼Œč€Œé€™å°±ę˜Æē“”ę–‡å­—ē”Øęˆ¶ē«Æę‰€čƒ½ēœ‹åˆ°ēš„å…ØéƒØć€‚ !!! warning ēµ•å°äøč¦ęŠŠ `"[Rendered UI]"` é€™é”žä½”ä½ę–‡å­—ē•¶ęˆå”Æäø€ēš„å…§å®¹å›žå‚³ć€‚å¦‚ęžœå¾Œå‚™ę–‡å­—ę²’ęœ‰ē”Øļ¼Œé€™å€‹å·„å…·å°ęÆå€‹ē“”ę–‡å­—ē”Øęˆ¶ē«Æć€å°ęØ”åž‹ęœ¬čŗ«å°±éƒ½ę²’ęœ‰ē”Øć€‚å„½å„½åÆ«é‚£å„č©±ć€‚ diff --git a/i18n/zh-hant/pages/advanced/extensions.md b/i18n/zh-hant/pages/advanced/extensions.md index d90e61c2..37c6e5fa 100644 --- a/i18n/zh-hant/pages/advanced/extensions.md +++ b/i18n/zh-hant/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # ę““å……åŠŸčƒ½ {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ęœ€å°ēš„ęœ‰ē”Øę““å……åŠŸčƒ½ę˜Æäø€å€‹å·„å…·åŠ äøŠäø€å€‹čØ­å®šå°ę˜ č”Øļ¼š -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` 是在 `capabilities.extensions["com.example/stamps"]` å®£å‘Šēš„å€¼ć€‚å›žå‚³ `{}`ļ¼ˆé čØ­å€¼ļ¼‰č”Øē¤ŗå®£å‘Šé€™å€‹ę““å……åŠŸčƒ½ä½†äøåø¶ä»»ä½•čØ­å®šć€‚ * ę““å……åŠŸčƒ½ę°øé äøęœƒę‹æåˆ°ä¼ŗęœå™Øć€‚å®ƒä»„č³‡ę–™ēš„å½¢å¼å®£å‘Šč²¢ē»ļ¼Œē”± `MCPServer` å–ē”Øć€‚ę²’ęœ‰ `self.server` åÆä»„äæ®ę”¹ć€‚ -而 `main()` å°±ę˜Æč­‰ę˜Žļ¼Œäø€å€‹čØ˜ę†¶é«”å…§ē”Øęˆ¶ē«Æē›“ęŽ„é€£äøŠ `mcp`: +透過 HTTP ęä¾›ęœå‹™ļ¼Œē”Øęˆ¶ē«Æå°±ę˜Æč­‰ę˜Žļ¼š -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +ęœ¬é ęÆå€‹ `server.py` éƒ½ē”Øé€™å€‹ęŒ‡ä»¤ęä¾›ęœå‹™ļ¼ŒęÆå€‹ `client.py` å‰‡åœØē¬¬äŗŒå€‹ēµ‚ē«Æę©Ÿē”Ø `python client.py` čˆ‡å®ƒäø¦č”ŒåŸ·č”Œć€‚ + ### ęä¾›č‡Ŗå·±ēš„ę–¹ę³• {#serving-your-own-methods} ę““å……åŠŸčƒ½åÆä»„čØ»å†Š**ę–°ēš„č«‹ę±‚ę–¹ę³•**ļ¼šå±¬ę–¼å®ƒč‡Ŗå·±ēš„å‹•č©žļ¼Œå’Œč¦ę ¼å®šē¾©ēš„ę–¹ę³•äø¦åˆ—ęä¾›ļ¼š -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### ē”Øęˆ¶ē«Æé€™äø€å“ {#the-client-side} -åŒäø€å€‹ęŖ”ę”ˆēš„ `main()` å°±ę˜Æå®Œę•“ēš„ē”Øęˆ¶ē«Æę•…äŗ‹ļ¼Œå…©åŠéƒ½åœØč£”é¢ļ¼š +ē”Øęˆ¶ē«Æę˜ÆēØē«‹ēš„ēØ‹å¼ļ¼Œē”Øęˆ¶ē«Æę•…äŗ‹ēš„å…©åŠéƒ½åœØč£”é¢ļ¼š -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` å®£å‘Šé€™å€‹ę““å……åŠŸčƒ½ć€‚é€™äŗ›å®£å‘Šęœƒč®Šęˆ `ClientCapabilities.extensions`:在 2026-07-28 é€£ē·šäøŠļ¼Œé€™å€‹å°ę˜ č”ØéšØč‘—ęÆå€‹č«‹ę±‚ēš„ `_meta` äæ”å°å‚³é€ļ¼Œę‰€ä»„ä¼ŗęœå™ØåœØ**ęÆäø€å€‹**č«‹ę±‚äøŠéƒ½ēœ‹å¾—åˆ°å®ƒļ¼›åœØčˆŠē‰ˆé€£ē·šäøŠļ¼Œå®ƒå‰‡ę­č‘— `initialize` äŗ¤ę”å‚³é€ć€‚ä¼ŗęœå™ØēØ‹å¼ē¢¼äøē”ØåœØę„ę˜Æå“Ŗäø€ēØ®ļ¼š`require_client_extension(ctx, ...)` 和 `ctx.session.check_client_capability(...)` åœØå…©ę¢č·Æå¾‘äøŠéƒ½ęœƒč®€å–ę­£ē¢ŗēš„ä¾†ęŗć€‚ * 廠商方法要往下一層用 `client.session.send_request(...)`ļ¼›`Client` åŖęœƒē‚ŗč¦ę ¼å‹•č©žé•·å‡ŗäø€ē“šę–¹ę³•ć€‚`send_request` ęŽ„å—ä»»ä½• `Request` å­é”žåˆ„ļ¼Œę‰€ä»„å» å•†č«‹ę±‚åŽŸęØ£å‚³éžå³åÆć€‚ +* `SearchRequest` å’Œå®ƒę”œåø¶ēš„å…©å€‹ęØ”åž‹ę˜Æé€™å€‹ę““å……åŠŸčƒ½ēš„ē·šč·Æå„‘ē“„ļ¼Œę‰€ä»„ē”Øęˆ¶ē«Æč‡Ŗå·±å®£å‘Šäø€ä»½ć€‚ę­£å¼ē™¼åøƒēš„ę““å……åŠŸčƒ½ęœƒęŠŠå®ƒå€‘ę”¾é€²äø€å€‹å„—ä»¶ļ¼Œč®“å…©ē«Æéƒ½åŒÆå…„ć€‚ ### ę””ęˆŖ `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## ä½æē”Øē”Øęˆ¶ē«Æę““å……åŠŸčƒ½ {#using-a-client-extension} -**ē”Øęˆ¶ē«Æę““å……åŠŸčƒ½**ę˜Æå¾žä½æē”Øē«Æēœ‹ēš„åŒäø€ä»½å„‘ē“„ļ¼šęŽ›åœØå–®äø€č­˜åˆ„ē¢¼ä¹‹äø‹ēš„äø€ēµ„ē”Øęˆ¶ē«Æč”Œē‚ŗć€‚ęŠŠåÆ¦ä¾‹å‚³ēµ¦ `Client(extensions=[...])`ļ¼Œē„¶å¾Œē…§åøøå‘¼å«å·„å…·ļ¼š +**ē”Øęˆ¶ē«Æę““å……åŠŸčƒ½**ę˜Æå¾žä½æē”Øē«Æēœ‹ēš„åŒäø€ä»½å„‘ē“„ļ¼šęŽ›åœØå–®äø€č­˜åˆ„ē¢¼ä¹‹äø‹ēš„äø€ēµ„ē”Øęˆ¶ē«Æč”Œē‚ŗć€‚é€™č£”ēš„ä¼ŗęœå™Øå›žę‡‰ `buy` ę™‚ēµ¦ēš„äøę˜Æč²Øå“ļ¼Œč€Œę˜Æäø€å¼µå¾…å…Œę›ēš„ę”¶ę“šļ¼Œč€Œäø”åŖå°å®£å‘Šäŗ†é€™å€‹ę““å……åŠŸčƒ½ēš„ē”Øęˆ¶ē«Æé€™ęØ£åšļ¼š -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +åœØē”Øęˆ¶ē«Æļ¼ŒęŠŠåÆ¦ä¾‹å‚³ēµ¦ `Client(extensions=[...])`ļ¼Œē„¶å¾Œē…§åøøå‘¼å«å·„å…·ļ¼š + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` å›žå‚³ę™®é€šēš„ `CallToolResult`ļ¼Œå’Œå…¶ä»–ę‰€ęœ‰å‘¼å«äø€ęØ£ć€‚ę““å……åŠŸčƒ½ę”¹č®Šēš„ę˜Æļ¼šä¼ŗęœå™Øē¾åœØåÆä»„ē”Ø `receipt` **ēµęžœå½¢ē‹€**ä¾†å›žę‡‰ `buy`ļ¼Œč€Œäøę˜Æęœ€ēµ‚ēµęžœļ¼Œč€Œ `Receipts` 會在 `call_tool` å›žå‚³ä¹‹å‰ęŠŠå®ƒå®Œęˆļ¼ˆé€™č£”ę˜Æé€éŽå¾ŒēŗŒå‘¼å«å…Œę›ę”¶ę“šļ¼‰ć€‚å‘¼å«ē«Æēš„ēØ‹å¼ē¢¼å®Œå…Øäøē”Øå‹•ć€‚ ę‹æęŽ‰é€™å€‹ę““å……åŠŸčƒ½ļ¼Œé€™äø€åˆ‡å°±äøå­˜åœØļ¼šä¼ŗęœå™Øēš„é—œå”ęœƒę‹’ēµ•ę²’ęœ‰å®£å‘Šå®ƒēš„ē”Øęˆ¶ē«Æļ¼ˆéŒÆčŖ¤ -32021ļ¼‰ļ¼Œč€Œä¾†č‡Ŗč·³éŽé—œå”ēš„ä¼ŗęœå™Øēš„čŖé ˜å½¢ē‹€ęœƒé©—č­‰å¤±ę•—ļ¼Œå®Œå…Øē¬¦åˆč¦ę ¼å°ē„”ę³•č¾Øč­˜ēš„ `resultType` ēš„č¦ę±‚ć€‚é čØ­é—œé–‰ļ¼Œē·šč·Æēš„å…©ē«Æéƒ½ę˜Æć€‚ @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ę’°åÆ«ē”Øęˆ¶ē«Æę““å……åŠŸčƒ½ {#writing-a-client-extension} 繼承 `ClientExtension`ļ¼ŒåŖč¦†åÆ«éœ€č¦ēš„éƒØåˆ†ć€‚äø‰ēØ®č²¢ē»é”žåž‹ļ¼Œå„ęœ‰é čØ­åÆ¦ä½œļ¼š`settings()`态`claims()` 和 `notifications()`怂 -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * č­˜åˆ„ē¢¼éµå¾Ŗå’Œä¼ŗęœå™Øē›øåŒēš„čŖžę³•ļ¼ŒåœØé”žåˆ„å®šē¾©ę™‚é©—č­‰ć€‚ @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### ę““å……åŠŸčƒ½å‹•č©ž {#extension-verbs} -ę““å……åŠŸčƒ½č‡Ŗå·±ēš„č«‹ę±‚ę–¹ę³•äøéœ€č¦ē”Øęˆ¶ē«ÆčØ»å†Šć€‚å» å•†č«‹ę±‚åž‹åˆ„ē¹¼ę‰æ `mcp.types.Request`ļ¼Œäø¦é€éŽ `client.session.send_request` é€å‡ŗļ¼Œå¦‚[ęä¾›č‡Ŗå·±ēš„ę–¹ę³•](#serving-your-own-methods)ę‰€ē¤ŗć€‚å¤šäø€ä»¶äŗ‹ļ¼šē•¶ęŸå€‹åƒę•øéµåæ…é ˆę­äøŠ `Mcp-Name` ęØ™é ­ę™‚ļ¼ˆåƒ tasks é€™é”žę““å……åŠŸčƒ½č¦ę ¼å°å®ƒå€‘ēš„å‹•č©žęœ‰ę­¤č¦ę±‚ļ¼‰ļ¼Œč«‹ę±‚åž‹åˆ„č¦å®£å‘Š `name_param`: +ę““å……åŠŸčƒ½č‡Ŗå·±ēš„č«‹ę±‚ę–¹ę³•äøéœ€č¦ē”Øęˆ¶ē«ÆčØ»å†Šć€‚å» å•†č«‹ę±‚åž‹åˆ„ē¹¼ę‰æ `mcp.types.Request`ļ¼Œäø¦é€éŽ `client.session.send_request` é€å‡ŗļ¼Œå¦‚[ęä¾›č‡Ŗå·±ēš„ę–¹ę³•](#serving-your-own-methods)ę‰€ē¤ŗć€‚ä»„äø€å€‹ä¼ŗęœå™Øē‚ŗä¾‹ļ¼Œå®ƒēš„ę““å……åŠŸčƒ½ęä¾›äø€å€‹å’Œå…·åå·„ä½œęœ‰é—œēš„å‹•č©žļ¼š -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +ē”Øęˆ¶ē«Æå¤šäø€ä»¶äŗ‹ļ¼šē•¶ęŸå€‹åƒę•øéµåæ…é ˆę­äøŠ `Mcp-Name` ęØ™é ­ę™‚ļ¼ˆåƒ tasks é€™é”žę““å……åŠŸčƒ½č¦ę ¼å°å®ƒå€‘ēš„å‹•č©žęœ‰ę­¤č¦ę±‚ļ¼‰ļ¼Œč«‹ę±‚åž‹åˆ„č¦å®£å‘Š `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + å·„ä½œéšŽę®µęœƒåœØęÆäø€ę¢é€å‡ŗč·Æå¾‘äøŠęŠŠ `params["jobId"]` 鏔射到 `Mcp-Name`ļ¼Œč€Œē¼ŗå°‘å€¼ę™‚ęœƒę˜Žē¢ŗå¤±ę•—ļ¼Œč€Œäøę˜Æé»˜é»˜ēœē•„åæ…č¦ēš„ęØ™é ­ć€‚ ## ę““å……åŠŸčƒ½äøčƒ½åšēš„äŗ‹ {#what-an-extension-cannot-do} diff --git a/i18n/zh-hant/pages/advanced/low-level-server.md b/i18n/zh-hant/pages/advanced/low-level-server.md index 9e05b314..62bcfd4d 100644 --- a/i18n/zh-hant/pages/advanced/low-level-server.md +++ b/i18n/zh-hant/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # 低階 Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### č©¦č©¦ēœ‹ {#try-it} -é€™å€‹ę²’ęœ‰ Inspector åÆē”Øļ¼š`mcp dev` 和 `mcp run` åŖęŽ„å— `MCPServer`ć€‚čØ˜ę†¶é«”å…§ēš„ `Client` å‰‡äøåœØä¹Žļ¼›å®ƒęŽ„ę”¶ä½ŽéšŽ `Server` ēš„ę–¹å¼å’ŒęŽ„ę”¶ `MCPServer` å®Œå…Øäø€ęØ£ļ¼š +`mcp dev` 和 `mcp run` åŖęŽ„å— `MCPServer`ļ¼Œę‰€ä»„é€™å€‹å¾—č‡Ŗå·±ęä¾›ęœå‹™ć€‚`server.py` ēš„ęœ€å¾Œäø€č”Œē”Øå®ƒå»ŗē«‹äø€å€‹ę™®é€šēš„ ASGI ę‡‰ē”ØēØ‹å¼ļ¼Œå†äŗ¤ēµ¦ uvicorn 執蔌: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +把 Inspector ęˆ–ä»»ä½•ē”Øęˆ¶ē«ÆęŒ‡å‘ `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` 是 `None`ć€‚é«˜éšŽä¼ŗęœå™Øęœƒå¹«ä½ ęŠŠ `-> str` åŒ…ęˆ `{"result": ...}`ļ¼›åœØé€™č£”ļ¼Œä½ ę²’å»ŗēš„ę±č„æļ¼Œę²’ęœ‰äŗŗęœƒę›æä½ å»ŗć€‚ * `list_tools` å›žå‚³ēš„ę˜Æ**ä½ **ę‰“å‡ŗä¾†ēš„ schemaļ¼Œäø€å­—äøå·®ć€‚é«˜éšŽē‰ˆęœ¬ęÆå€‹å±¬ę€§äøŠéƒ½ęœ‰ `"title": "Query"`ļ¼Œę ¹éƒØé‚„ęœ‰äø€å€‹ `"title": "search_booksArguments"`ļ¼šé‚£ę˜Æ Pydantic ēš„ē”¢ē‰©ć€‚åœØé€™äø€å±¤ļ¼Œē·šč·ÆäøŠęœ‰ēš„ę±č„æļ¼Œéƒ½ę˜Æä½ ę”¾äøŠåŽ»ēš„ć€‚ +åœØęø¬č©¦äø­åÆä»„č·³éŽ uvicorn å’Œé€£ęŽ„åŸ ļ¼š`Client(server)` åœØč™•ē†ēØ‹åŗå…§ęŽ„ę”¶ä½ŽéšŽ `Server` ēš„ę–¹å¼å’ŒęŽ„ę”¶ `MCPServer` å®Œå…Øäø€ęØ£ļ¼Œ**[測試](../get-started/testing.md)** č¬›ēš„å°±ę˜Æé€™å€‹ęØ”å¼ć€‚ + ## ę²’ęœ‰äŗŗę›æä½ ęŖ¢ęŸ„ {#nothing-is-checked-for-you} `MCPServer` ęœƒåœØå‡½å¼åŸ·č”Œä¹‹å‰å°±ę‹’ēµ•éŒÆčŖ¤ēš„å¼•ę•øļ¼Œä¾ē…§å®ƒē”¢ē”Ÿēš„ schema é©—č­‰é€™ę¬”å‘¼å«ļ¼ˆ**[å·„å…·](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` åÆä»„ęœå‹™ä»»ä½•ę–¹ę³•ć€‚`initialize` č¢«äæē•™ć€‚ * `Server` å…¬å‘Šēš„čƒ½åŠ›ļ¼Œē”±ä½ čØ»å†Šäŗ†å“Ŗäŗ›č™•ē†å‡½å¼ęŽØå°Žč€Œä¾†ć€‚ -`Client(server)` å°å…©ēØ®ä¼ŗęœå™Øäø€č¦–åŒä»ļ¼Œå› ē‚ŗå®ƒå€‘**就是**åŒäø€å€‹å”å®šļ¼Œé€™ę­£ę˜Æé‡é»žę‰€åœØć€‚å†å¾€äø‹äø€å±¤ę ¹ęœ¬äøę˜Æé”žåˆ„ļ¼šę˜Æ **[äø­ä»‹č»Ÿé«”](middleware.md)**怂 +ē”Øęˆ¶ē«Æå°å…©ēØ®ä¼ŗęœå™Øäø€č¦–åŒä»ļ¼Œå› ē‚ŗå®ƒå€‘**就是**åŒäø€å€‹å”å®šļ¼Œé€™ę­£ę˜Æé‡é»žę‰€åœØć€‚å†å¾€äø‹äø€å±¤ę ¹ęœ¬äøę˜Æé”žåˆ„ļ¼šę˜Æ **[äø­ä»‹č»Ÿé«”](middleware.md)**怂 diff --git a/i18n/zh-hant/pages/advanced/pagination.md b/i18n/zh-hant/pages/advanced/pagination.md index aa46b278..b0b0bd1b 100644 --- a/i18n/zh-hant/pages/advanced/pagination.md +++ b/i18n/zh-hant/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # 分頁 {#pagination} @@ -29,9 +29,13 @@ translation: ### č©¦č©¦ēœ‹ {#try-it} -`Client(server)` åœØčØ˜ę†¶é«”å…§é€£ē·šåˆ°ä½ŽéšŽ `Server` ēš„ę–¹å¼ļ¼Œå’Œé€£åˆ° `MCPServer` å®Œå…Øäø€ęØ£ć€‚ +`mcp run` åŖęŽ„å— `MCPServer`ļ¼Œę‰€ä»„é€™å€‹å¾—č‡Ŗå·±ęä¾›ęœå‹™ć€‚`server.py` ēš„ęœ€å¾Œäø€č”Œå¾ž `Server` å»ŗå‡ŗäø€å€‹ę™®é€šēš„ ASGI ę‡‰ē”ØēØ‹å¼ļ¼Œäŗ¤ēµ¦ uvicorn 執蔌: -äøåø¶å¼•ę•øå‘¼å« `list_resources()`ć€‚ęœƒę‹æåˆ°åå€‹č³‡ęŗļ¼Œ`book-1` 到 `book-10`ļ¼Œč€Œ `next_cursor` ę˜Æå­—äø² `"10"`怂 +```console +uvicorn server:app --port 8000 +``` + +ęŠŠä»»ä½•ē”Øęˆ¶ē«Æļ¼ˆ**[ē”Øęˆ¶ē«Æ](../client/index.md)**ļ¼Œęˆ– Inspectorļ¼‰ęŒ‡å‘ `http://localhost:8000/mcp`ļ¼Œäøåø¶å¼•ę•øå‘¼å« `list_resources()`ć€‚ęœƒę‹æåˆ°åå€‹č³‡ęŗļ¼Œ`book-1` 到 `book-10`ļ¼Œč€Œ `next_cursor` ę˜Æå­—äø² `"10"`怂 用 `list_resources(cursor="10")` ęŠŠå®ƒäŗ¤å›žåŽ»ļ¼Œē¬¬äø€å€‹č³‡ęŗå°±ę˜Æ `book-11`ļ¼Œę–°ēš„ `next_cursor` 是 `"20"`怂 @@ -41,7 +45,7 @@ translation: `Client` äøŠēš„ęÆå€‹ `list_*` ę–¹ę³•ļ¼ˆ`list_tools`态`list_resources`态`list_resource_templates`态`list_prompts`ļ¼‰éƒ½ęŽ„å— `cursor=` é—œéµå­—å¼•ę•øć€‚ęŠŠåˆ†é ęø…å–®ęŠ“å®ŒåŖč¦äø€å€‹ `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * 先 extend,**再**ēœ‹ `next_cursor`ļ¼šęœ€å¾Œäø€é ä¹Ÿęœ‰č³‡ęŗć€‚ * `next_cursor is None` å°±ę˜Æå‡ŗå£ć€‚å…¶ä»–ä»»ä½•å€¼éƒ½åŽŸå°äøå‹•ē›“ęŽ„ę”¾å›ž `cursor=`怂 -åŸ·č”Œå®ƒēš„ `main()`ļ¼Œęœƒå°å‡ŗ `100 resources`ļ¼šåé ć€ęÆé åå€‹ļ¼Œē”±äø€å€‹å¾žé ­åˆ°å°¾äøēŸ„é“ęœ‰åé ēš„čæ“åœˆęŽ„čµ·ä¾†ć€‚ +讓 uvicorn ē¹¼ēŗŒęä¾› `server.py`ļ¼ŒåœØē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œ `python client.py`ć€‚ęœƒå°å‡ŗ `100 resources`ļ¼šåé ć€ęÆé åå€‹ļ¼Œē”±äø€å€‹å¾žé ­åˆ°å°¾äøēŸ„é“ęœ‰åé ēš„čæ“åœˆęŽ„čµ·ä¾†ć€‚ 這和 **[ē”Øęˆ¶ē«Æ](../client/index.md)** ē‚ŗęÆå€‹ `list_*` å‹•č©žē¤ŗēÆ„ēš„čæ“åœˆę˜ÆåŒäø€å€‹ļ¼Œč€Œäø”å°äøåˆ†é ēš„ä¼ŗęœå™Øä¹Ÿę²’ęœ‰ä»»ä½•ä»£åƒ¹ļ¼šē¬¬äø€å€‹å›žę‡‰ēš„ `next_cursor` 就是 `None`ļ¼Œčæ“åœˆåŖåŸ·č”Œäø€ę¬”ć€‚ diff --git a/i18n/zh-hant/pages/client/caching.md b/i18n/zh-hant/pages/client/caching.md index bdd63b73..03ef804e 100644 --- a/i18n/zh-hant/pages/client/caching.md +++ b/i18n/zh-hant/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # åæ«å–ęē¤ŗ {#caching-hints} @@ -27,7 +27,7 @@ translation: åœØä½ŽéšŽēš„ `Server` äøŠļ¼Œč™•ē†å‡½å¼č‡Ŗå·±ę‰‹å‹•ēµ„å‡ŗēµęžœļ¼Œč€Œ `ttl_ms` / `cache_scope` åŖę˜ÆēµęžœęØ”åž‹äøŠēš„ę¬„ä½ć€‚ę˜Žē¢ŗčØ­å®šé€™äŗ›ę¬„ä½ēš„č™•ē†å‡½å¼ļ¼Œę°øé å‹éŽå»ŗę§‹å­ēš„å°ę‡‰č”Øļ¼Œč€Œäø”ę˜Æé€ę¬„ä½ęÆ”č¼ƒļ¼š -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` @@ -41,10 +41,24 @@ translation: 在 2026-07-28 ēš„å·„ä½œéšŽę®µļ¼ˆsessionļ¼‰äøŠļ¼Œ`Client` ęœƒę›æä½ éµå®ˆé€™äŗ›ęē¤ŗļ¼šå®ƒå…§å»ŗäø€å€‹å›žę‡‰åæ«å–ļ¼Œé čØ­é–‹å•Ÿć€‚åø¶č‘— `ttlMs` ęŠµé”ēš„ēµęžœęœƒč¢«å­˜čµ·ä¾†ļ¼ŒåœØ TTL å…§å®Œå…Øē›øåŒēš„å‘¼å«ęœƒē›“ęŽ„ē”±åæ«å–ęä¾›ļ¼Œäøéœ€č¦å¾€čæ”ć€‚**ę²’ęœ‰**åø¶ęē¤ŗēš„ēµęžœäøęœƒč¢«åæ«å–ļ¼šę²’ęœ‰ęē¤ŗēš„ēµęžœęœƒå„—ē”Ø `CacheConfig.default_ttl_ms`ļ¼Œå®ƒé čØ­ē‚ŗ `0`ļ¼ˆē«‹åˆ»éŽęœŸļ¼‰ļ¼Œę‰€ä»„ä»€éŗ¼éƒ½ę²’å®£å‘Šēš„ä¼ŗęœå™Øļ¼Œēœ‹åˆ°ēš„ęµé‡å’Œä»„å¾€äø€ęØ”äø€ęØ£ļ¼Œäø€ę¬”å‘¼å«å°±äø€ę¬”č«‹ę±‚ć€‚ -```python title="client.py" hl_lines="33 35 38" +č¦č¦Ŗēœ¼ēœ‹ēœ‹é€™å€‹éŽēØ‹ļ¼Œå°±ē”Ø uvicorn ęä¾›å‰äø€ēÆ€ēš„ `server.py`ļ¼ˆå®ƒēš„ęœ€å¾Œäø€č”Œęœƒå»ŗē«‹ ASGI ę‡‰ē”ØēØ‹å¼ļ¼‰ć€‚č™•ē†å‡½å¼ęÆę¬”ēœŸę­£åŸ·č”Œę™‚éƒ½ęœƒå°å‡ŗäø€č”Œļ¼š + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +åœØē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œ `python client.py`ć€‚å®ƒęœƒå°å‡ŗē¬¬äø€å€‹ēµęžœåø¶č‘—ēš„ęē¤ŗļ¼Œč™•ē†å‡½å¼ēš„ `ttlMs` å’Œå°ę‡‰č”Øēš„ `cacheScope` äø¦ęŽ’ļ¼š + +```text +1000 public +``` + +ä¼ŗęœå™Øēš„ēµ‚ē«Æę©Ÿäŗ¤ä»£äŗ†å‰©äø‹ēš„éƒØåˆ†ļ¼šåœØ uvicorn ēš„č«‹ę±‚čØ˜éŒ„ä¹‹é–“ļ¼Œ`tools/list served` å‡ŗē¾äŗ†äø‰ę¬”ć€‚ + å››ę¬”å‘¼å«ļ¼Œäø‰ę¬”ęŠ“å–ć€‚ē¬¬äŗŒę¬”å‘¼å«ę‰¾åˆ°ę–°é®®ēš„é …ē›®ļ¼Œę ¹ęœ¬ę²’é€åˆ°ä¼ŗęœå™Øļ¼›ęŠŠļ¼ˆę³Øå…„ēš„ļ¼‰ę™‚é˜ę’„éŽ TTL ä¹‹å¾Œļ¼Œē¬¬äø‰ę¬”åˆé‡ę–°ęŠ“å–ļ¼›ē¬¬å››ę¬”å‰‡ęŒ‡å®šäŗ† `cache_mode="refresh"`ć€‚é€™å€‹é—œéµå­—å¼•ę•øå­˜åœØę–¼äŗ”å€‹ęœƒåæ«å–ēš„å‹•č©žäøŠļ¼ˆ`list_tools`态`list_prompts`态`list_resources`态`list_resource_templates`态`read_resource`ļ¼‰ļ¼š * `"use"`ļ¼ˆé čØ­ļ¼‰å¦‚ęžœęœ‰ę–°é®®ēš„é …ē›®å°±ē›“ęŽ„ęä¾›ļ¼Œę²’ęœ‰ēš„č©±å°±ęŠ“å–äø¦å­˜čµ·ä¾†ć€‚ @@ -53,7 +67,7 @@ translation: ęœ‰äø€ę¢č¦å‰‡å‡Œé§•ę–¼ `"use"` ä¹‹äøŠļ¼š**åø¶ęœ‰ `meta` ēš„å‘¼å«äø€å®šęœƒé€åˆ°ä¼ŗęœå™Øć€‚**čØ­å®šäŗ† `meta` ēš„č«‹ę±‚ļ¼ˆé€²åŗ¦ tokenć€čæ½č¹¤ę¬„ä½ļ¼‰ęœŸå¾…ēš„ę˜Æäø€å€‹åÆ¦éš›é€äøŠē·šč·Æēš„č«‹ę±‚ļ¼Œę‰€ä»„åœØ `cache_mode="use"` äø‹ęœƒč¢«ē•¶ęˆ `"refresh"` č™•ē†ļ¼šč·³éŽåæ«å–č®€å–ļ¼Œč€ŒęŠ“å–å›žä¾†ēš„ēµęžœä»ē„¶ęœƒå–ä»£åæ«å–äø­ēš„é …ē›®ć€‚`"bypass"` å’Œę˜Žē¢ŗęŒ‡å®šēš„ `"refresh"` č”Œē‚ŗē…§čˆŠć€‚ -č¦å®Œå…Øé—œęŽ‰åæ«å–ļ¼Œå°±ē”Ø `Client(server, cache=None)` å»ŗę§‹ļ¼šęÆę¬”å‘¼å«åˆéƒ½č®Šå›žäø€ę¬”å¾€čæ”ļ¼Œč€Œ `cache_mode` é›–ē„¶ä»åÆęŽ„å—ļ¼Œä½†äøęœƒęœ‰ä»»ä½•ä½œē”Øć€‚ +č¦å®Œå…Øé—œęŽ‰åæ«å–ļ¼Œå°±åœØå»ŗę§‹ `Client` 時傳兄 `cache=None`ļ¼šęÆę¬”å‘¼å«åˆéƒ½č®Šå›žäø€ę¬”å¾€čæ”ļ¼Œč€Œ `cache_mode` é›–ē„¶ä»åÆęŽ„å—ļ¼Œä½†äøęœƒęœ‰ä»»ä½•ä½œē”Øć€‚ ēÆ„åœä¹Ÿęœƒč‡Ŗå‹•éµå®ˆļ¼š`"private"` é …ē›®ē¶å®šåœØåæ«å–ēš„**åˆ†å€ļ¼ˆpartition)**äøŠļ¼ˆč¦‹äø‹ę–‡ļ¼‰ļ¼Œč€Œ `"public"` é …ē›®å‰‡åÆä»„éøę“‡ę›“å»£ēš„å…±ē”Øć€‚ę­¤å¤–ļ¼Œå°é€šēŸ„é»žåēš„é‚£äŗ›é …ē›®ä¾†čŖŖļ¼Œ**é€šēŸ„å‹éŽ TTL**:`list_changed` é€šēŸ„ęœƒé€å‡ŗå°ę‡‰ēš„åæ«å–ęø…å–®ļ¼Œ`resources/updated` å‰‡ęœƒé€å‡ŗę°å„½å­˜åœØč©² URI äø‹ēš„åæ«å–č®€å–ēµęžœļ¼Œäøē®”å®ƒå€‘ęœ‰å¤šę–°é®®ć€‚åœØ 2026-07-28 é€£ē·šäøŠļ¼Œé€™äŗ›é€šēŸ„ę˜Æé€éŽä½ ē”Ø `client.listen(...)` é–‹å•Ÿēš„ `subscriptions/listen` äø²ęµé€é”ēš„ļ¼Œč€Œäø”é€å‡ŗęœƒåœØä½ ēš„ē›£ēœ‹ēØ‹å¼ēœ‹åˆ°äŗ‹ä»¶ä¹‹å‰å®Œęˆļ¼›č©³ęƒ…č«‹č¦‹ **[訂閱](subscriptions.md)**怂 diff --git a/i18n/zh-hant/pages/client/callbacks.md b/i18n/zh-hant/pages/client/callbacks.md index db0ad2f5..e121e763 100644 --- a/i18n/zh-hant/pages/client/callbacks.md +++ b/i18n/zh-hant/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # ē”Øęˆ¶ē«Æå›žå‘¼ {#client-callbacks} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') 你送出一個 `tools/call`ļ¼Œä¼ŗęœå™Øå›žé€äø€å€‹ `elicitation/create`ļ¼Œē”±ä½ ēš„å‡½å¼å›žē­”ļ¼Œå…Øéƒ½ē™¼ē”ŸåœØåŒäø€ę¬”å·„å…·å‘¼å«ä¹‹å…§ć€‚ !!! info - `Client(...)` å‘¼å«äøŠēš„ `mode="legacy"` ę˜ÆēœŸēš„ęœ‰ä½œē”Øć€‚é čØ­ęƒ…ę³äø‹ `Client(...)` ęœƒå”å•†å‡ŗē¾ä»£ēš„å”å®šč·Æå¾‘ļ¼Œč€Œé€™ę¢č·Æå¾‘ę²’ęœ‰č®“ä¼ŗęœå™Øå‘ē”Øęˆ¶ē«Æē™¼č«‹ę±‚ēš„åå‘é€šé“ļ¼ˆback-channelļ¼‰ļ¼š`ctx.elicit` åœØä½ ēš„å›žå‘¼ęœ‰ę©ŸęœƒåŸ·č”Œä¹‹å‰å°±å¤±ę•—äŗ†ć€‚ę±ŗå®šé€™ä»¶äŗ‹ēš„äøę˜Æå‚³č¼øę–¹å¼ļ¼Œč€Œę˜Æå”å•†å‡ŗä¾†ēš„å”å®šļ¼ŒčØ˜ę†¶é«”å…§å’Œé€éŽ URL é€£ē·šéƒ½äø€ęØ£ć€‚åŖč¦ē”Øęˆ¶ē«Æåæ…é ˆå›žę‡‰é€™é”žč«‹ę±‚ļ¼Œå°±å›ŗå®šē”Ø `mode="legacy"`ļ¼›é€™äø€é čƒŒå¾Œēš„ęÆå€‹ęø¬č©¦éƒ½ę˜Æé€™ęØ£åšēš„ć€‚å®Œę•“čŖŖę˜Žč«‹č¦‹ **[å”å®šē‰ˆęœ¬](../protocol-versions.md)**怂 + `Client(...)` å‘¼å«äøŠēš„ `mode="legacy"` ę˜ÆēœŸēš„ęœ‰ä½œē”Øć€‚é čØ­ęƒ…ę³äø‹ `Client(...)` ęœƒå”å•†å‡ŗē¾ä»£ēš„å”å®šč·Æå¾‘ļ¼Œč€Œé€™ę¢č·Æå¾‘ę²’ęœ‰č®“ä¼ŗęœå™Øå‘ē”Øęˆ¶ē«Æē™¼č«‹ę±‚ēš„åå‘é€šé“ļ¼ˆback-channelļ¼‰ļ¼š`ctx.elicit` åœØä½ ēš„å›žå‘¼ęœ‰ę©ŸęœƒåŸ·č”Œä¹‹å‰å°±å¤±ę•—äŗ†ć€‚ę±ŗå®šé€™ä»¶äŗ‹ēš„äøę˜Æå‚³č¼øę–¹å¼ļ¼Œč€Œę˜Æå”å•†å‡ŗä¾†ēš„å”å®šć€‚åŖč¦ē”Øęˆ¶ē«Æåæ…é ˆå›žę‡‰é€™é”žč«‹ę±‚ļ¼Œå°±å›ŗå®šē”Ø `mode="legacy"`ļ¼›é€™äø€é čƒŒå¾Œēš„ęÆå€‹ęø¬č©¦éƒ½ę˜Æé€™ęØ£åšēš„ć€‚å®Œę•“čŖŖę˜Žč«‹č¦‹ **[å”å®šē‰ˆęœ¬](../protocol-versions.md)**怂 在 2026-07-28 ēš„å·„ä½œéšŽę®µļ¼ˆsessionļ¼‰äøŠļ¼Œå›žå‘¼äø¦ę²’ęœ‰å¤±ę•ˆļ¼ŒåŖę˜Æé¤µēµ¦å®ƒēš„ę–¹å¼äøåŒļ¼šē•¶å·„å…·å›žå‚³åø¶ęœ‰ `ElicitRequest` ēš„ `InputRequiredResult` ę™‚ļ¼Œ`Client` ęœƒęŠŠé‚£å€‹é …ē›®åˆ†ę“¾ēµ¦åŒäø€å€‹ `elicitation_callback`ļ¼Œäø¦ę›æä½ é‡č©¦é€™ę¬”å‘¼å«ć€‚é€™å€‹ęµēØ‹å°±ę˜Æ **[å¤šč¼Ŗå¾€čæ”ļ¼ˆmulti-round-trip)請求](../handlers/multi-round-trip.md)**怂 @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` 和 `list_roots_callback` ēš„é‹ä½œę–¹å¼ē›øåŒļ¼Œä½†ęœå‹™ēš„ę˜Æå·²ę£„ē”Øēš„åŠŸčƒ½ļ¼›ē¾ä»£ēš„ä¼ŗęœå™Øę”¹ē”Øå¤šč¼Ŗå¾€čæ”č«‹ę±‚ć€‚ * `logging_callback` 和 `message_handler` ęŽ„ę”¶é€šēŸ„ć€‚å®ƒå€‘äøå®£å‘Šä»»ä½•ę±č„æć€‚ -`Client(...)` ēš„ē¬¬äø€å€‹å¼•ę•øę˜Æäø€å€‹å‚³č¼øē‰©ä»¶ć€‚**[ē”Øęˆ¶ē«Æå‚³č¼øę–¹å¼](transports.md)** ę¶µč“‹äŗ†ęÆäø€ēØ®ć€‚ +`Client(...)` ēš„ē¬¬äø€å€‹å¼•ę•øę±ŗå®šå‚³č¼øę–¹å¼ć€‚**[ē”Øęˆ¶ē«Æå‚³č¼øę–¹å¼](transports.md)** ę¶µč“‹äŗ†ęÆäø€ēØ®ć€‚ diff --git a/i18n/zh-hant/pages/client/index.md b/i18n/zh-hant/pages/client/index.md index 8bd6edee..8e88f950 100644 --- a/i18n/zh-hant/pages/client/index.md +++ b/i18n/zh-hant/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # ē”Øęˆ¶ē«Æ {#the-client} @@ -11,13 +11,23 @@ Python ēØ‹å¼č¦å’Œ MCP ä¼ŗęœå™Øå°č©±ļ¼Œé ēš„å°±ę˜Æ **`Client`**怂 ## ä½ ēš„ē¬¬äø€å€‹ē”Øęˆ¶ē«Æ {#your-first-client} -```python title="client.py" hl_lines="14-18" +ē”Øęˆ¶ē«Æéœ€č¦ęœ‰ä¼ŗęœå™ØåÆä»„å°č©±ć€‚é€™äø€é ēš„ęÆę®µēØ‹å¼ē¢¼é€£ēš„éƒ½ę˜Æé€™é–“ Bookshopć€‚ęŠŠå®ƒå­˜ęˆ `server.py`ļ¼Œč®“å®ƒé€éŽ HTTP 持續執蔌: + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -ęœ€äøŠé¢ēš„ä¼ŗęœå™ØåŖę˜Æč®“ä½ ęœ‰ę±č„æåÆä»„é€£č€Œå·²ć€‚ē”Øęˆ¶ē«Æå°±ę˜ÆęØ™ē¤ŗå‡ŗä¾†ēš„é‚£äŗ”č”Œć€‚ +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` ę‹æåˆ°ēš„ę˜Æ**ä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«**ć€‚é€™å°±ę˜ÆčØ˜ę†¶é«”å…§å‚³č¼øļ¼šę²’ęœ‰å­č™•ē†ēØ‹åŗć€ę²’ęœ‰é€£ęŽ„åŸ ć€ę²’ęœ‰ HTTPć€‚é€™äø€é ēš„ęÆå€‹ēÆ„ä¾‹ļ¼Œä»„åŠä½ åÆ«ēš„ęÆå€‹ęø¬č©¦ļ¼Œéƒ½ę˜Æé€™ęØ£é€£ē·šēš„ć€‚ +é€™ęØ£ä¼ŗęœå™Øå°±åœØ `http://localhost:8000/mcp` ęä¾›ęœå‹™ć€‚ē”Øęˆ¶ē«Æę˜Æå¦äø€å€‹ēØē«‹ēš„ēØ‹å¼ć€‚ęŠŠå®ƒå­˜ęˆ `client.py`ļ¼ŒåœØē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œ `python client.py`: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` ę‹æåˆ°ēš„ę˜Æäø€å€‹ **URL**ļ¼Œę‰€ä»„å®ƒé€éŽ Streamable HTTP é€£åˆ°ä½ å‰›å•Ÿå‹•ēš„ä¼ŗęœå™Øć€‚ * `async with` 就是**ē”Ÿå‘½é€±ęœŸ**ć€‚é€²å…„ę™‚é€£ē·šäø¦å”å•†ļ¼›é›¢é–‹ę™‚ę–·ē·šć€‚ę²’ęœ‰ `connect()` / `close()` é€™ēØ®ęˆå°ēš„ę–¹ę³•ļ¼Œč€Œäø”å€å”ŠēµęŸå¾Œ `Client` äøčƒ½å†é‡č¤‡ä½æē”Øć€‚ * åœØå€å”Šå…§ļ¼Œé€£ē·šēš„å„é …č³‡čØŠå·²ē¶“ä»„ę™®é€šå±¬ę€§ēš„å½¢å¼ęŗ–å‚™å„½äŗ†ć€‚ @@ -25,10 +35,10 @@ Python ēØ‹å¼č¦å’Œ MCP ä¼ŗęœå™Øå°č©±ļ¼Œé ēš„å°±ę˜Æ **`Client`**怂 `Client` ęŽ„å—äø€å€‹ä½ē½®å¼•ę•øļ¼Œäø¦ä¾å®ƒēš„åž‹åˆ„ę±ŗå®šå‚³č¼øę–¹å¼ļ¼š -* `MCPServer`ļ¼ˆęˆ–ä½ŽéšŽēš„ `Server`ļ¼‰åÆ¦ä¾‹ļ¼šåœØ**åŒäø€å€‹č™•ē†ēØ‹åŗå…§**é€£ē·šć€‚ -* URL å­—äø²ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPļ¼Œä¹Ÿå°±ę˜Æę­£å¼ē’°å¢ƒēš„č·Æå¾‘ć€‚ -* `StdioServerParameters`ļ¼šč¦ē•¶ä½œ**å­č™•ē†ēØ‹åŗ**å•Ÿå‹•ēš„å‘½ä»¤ļ¼Œé€éŽå®ƒēš„ stdin 和 stdout ęŗé€šć€‚ +* URL å­—äø²ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPļ¼Œä¹Ÿå°±ę˜ÆéƒØē½²ę™‚ä½æē”Øēš„å‚³č¼øę–¹å¼ć€‚ +* `StdioServerParameters`ļ¼šč¦ē•¶ä½œęœ¬ę©Ÿ**å­č™•ē†ēØ‹åŗ**å•Ÿå‹•ēš„å‘½ä»¤ļ¼Œé€éŽå®ƒēš„ stdin 和 stdout ęŗé€šć€‚ * 一個**傳輸**ļ¼šä»»ä½•åÆä»„ `async with ... as (read, write)` ēš„ę±č„æļ¼Œä¾‹å¦‚ē”Ø `streamable_http_client(url, http_client=...)` åŒ…ä½ä½ č‡Ŗå·±ēš„ HTTP ē”Øęˆ¶ē«Æć€‚ +* `MCPServer`ļ¼ˆęˆ–ä½ŽéšŽēš„ `Server`ļ¼‰åÆ¦ä¾‹ļ¼šåœØ**åŒäø€å€‹č™•ē†ēØ‹åŗå…§**é€£ē·šļ¼Œę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œä¹Ÿę²’ęœ‰é€£ęŽ„åŸ ć€‚é€™äø€ēØ®ę˜Æēµ¦ęø¬č©¦ē”Øēš„ļ¼Œ**[測試](../get-started/testing.md)** å°±ę˜Æä»„å®ƒē‚ŗåŸŗē¤Žć€‚ é€™äø€é å…¶é¤˜ēš„å…§å®¹åœØé€™å››ēØ®ęƒ…ę³äø‹å®Œå…Øē›øåŒć€‚ęØ™é ­ć€å­č™•ē†ēØ‹åŗć€é€¾ę™‚ļ¼Œä»„åŠ `Transport` å”å®šå¦å¤–ęœ‰å°ˆå±¬ēš„é é¢ļ¼š**[ē”Øęˆ¶ē«Æå‚³č¼øę–¹å¼](transports.md)**怂 @@ -48,11 +58,11 @@ Python ēØ‹å¼č¦å’Œ MCP ä¼ŗęœå™Øå°č©±ļ¼Œé ēš„å°±ę˜Æ **`Client`**怂 ## åˆ—å‡ŗå·„å…· {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` å›žå‚³äø€å€‹ `ListToolsResult`ļ¼›å·„å…·åœØ `.tools` č£”ć€‚ęÆäø€å€‹éƒ½ę˜Æ MCP 主機(hostļ¼‰ęœƒäŗ¤ēµ¦ęØ”åž‹ēš„å®Œę•“å®šē¾©ļ¼š +`list_tools()` å›žå‚³äø€å€‹ `ListToolsResult`ļ¼›å·„å…·åœØ `.tools` č£”ć€‚ęÆäø€å€‹éƒ½ę˜Æ MCP 主機(hostļ¼‰ęœƒäŗ¤ēµ¦ęØ”åž‹ēš„å®Œę•“å®šē¾©ć€‚ē¬¬äø€å€‹ę˜Æé€™ęØ£ļ¼š ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' 這份 schema 就是 UI ē®—ē¹Ŗå¼•ę•øč”Øå–®ę‰€éœ€ēš„äø€åˆ‡ļ¼Œä¹Ÿę˜ÆęØ”åž‹ē”¢ē”Ÿåˆę³•å¼•ę•øę‰€éœ€ēš„äø€åˆ‡ć€‚ +ē¬¬äŗŒå€‹å·„å…· `lookup_book` čØ»å†Šę™‚ę²’ęœ‰ēµ¦ `title=`ļ¼Œę‰€ä»„å®ƒēš„ `tool.title` 是 `None`怂 + !!! tip `title` ę˜Æéøå”«ēš„ļ¼Œę‰€ä»„ęŠŠå·„å…·é”Æē¤ŗēµ¦äŗŗēœ‹ēš„ UI å¾—č‡Ŗå·±ęŒ‘ļ¼šęœ‰ēš„č©±å°±ē”Ø `title`ļ¼Œę²’ęœ‰å°±ē”Ø `name`怂`from mcp.shared.metadata_utils import get_display_name` åšēš„ę­£ę˜Æé€™ä»¶äŗ‹ļ¼Œé©ē”Øę–¼å·„å…·ć€č³‡ęŗć€č³‡ęŗēÆ„ęœ¬å’Œęē¤ŗč©žć€‚ @@ -83,7 +95,7 @@ tool.description # 'Search the catalog by title or author.' `call_tool(name, arguments)` ęœƒåŸ·č”Œå·„å…·ļ¼Œäø¦å›žå‚³äø€å€‹ `CallToolResult`怂 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False č³‡ęŗēš„å‹•ä½œę˜Æęˆēµ„ēš„ļ¼šå…©ēØ®åˆ—å‡ŗēš„ę–¹å¼ļ¼Œäø€ēØ®č®€å–ēš„ę–¹å¼ć€‚ -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -145,7 +157,7 @@ result.is_error # False ## ęē¤ŗč©ž {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ęœ‰č‡Ŗå‹•å®Œęˆč™•ē†å‡½å¼ēš„ä¼ŗęœå™Øļ¼ŒåÆä»„åœØä½æē”Øč€…č¼øå…„ę™‚č‡Ŗå‹•å®Œęˆęē¤ŗč©žå’Œč³‡ęŗēÆ„ęœ¬ēš„å¼•ę•øć€‚ -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ęÆå€‹ `list_*` ę–¹ę³•éƒ½ęŽ„å— `cursor=` é—œéµå­—å¼•ę•øļ¼ŒęÆå€‹ēµęžœéƒ½åø¶ęœ‰ `next_cursor`。當 `next_cursor` 是 `None`ļ¼Œč”Øē¤ŗå…ØéƒØę‹æåˆ°äŗ†ć€‚ -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -é€™å€‹čæ“åœˆå°ä»»ä½•ä¼ŗęœå™Øéƒ½ę­£ē¢ŗć€‚`MCPServer` ęœƒäø€é å›žå‚³å…ØéƒØļ¼Œę‰€ä»„ `next_cursor` 是 `None`ļ¼Œčæ“åœˆåŖč·‘äø€ę¬”ļ¼Œé€™ä¹Ÿę˜Æē‚ŗä»€éŗ¼å¤§éƒØåˆ†ēØ‹å¼ē¢¼å¾žä¾†äøåÆ«å®ƒć€‚ēœŸę­£ęœƒåˆ†é ēš„ä¼ŗęœå™Øļ¼Œä»„åŠęøøęØ™éµå®ˆēš„č¦å‰‡ļ¼Œč«‹č¦‹ **[分頁](../advanced/pagination.md)**怂 +`list_all_tools` å°ä»»ä½•ä¼ŗęœå™Øéƒ½ę­£ē¢ŗć€‚`MCPServer` ęœƒäø€é å›žå‚³å…ØéƒØļ¼Œę‰€ä»„ `next_cursor` 是 `None`ļ¼Œčæ“åœˆåŖč·‘äø€ę¬”ļ¼Œé€™ä¹Ÿę˜Æē‚ŗä»€éŗ¼å¤§éƒØåˆ†ēØ‹å¼ē¢¼å¾žä¾†äøåÆ«å®ƒć€‚ēœŸę­£ęœƒåˆ†é ēš„ä¼ŗęœå™Øļ¼Œä»„åŠęøøęØ™éµå®ˆēš„č¦å‰‡ļ¼Œč«‹č¦‹ **[分頁](../advanced/pagination.md)**怂 ## 在測試中 {#in-tests} -äøéœ€č¦č™•ē†ēØ‹åŗć€äøéœ€č¦é€£ęŽ„åŸ ēš„ `Client(mcp)`ļ¼Œęœ¬čŗ«å°±å·²ē¶“ę˜Æä¼ŗęœå™Øēš„ęø¬č©¦å·„å…·äŗ†ć€‚ +é€™äø€é ēš„ęÆå€‹ `client.py` éƒ½ę˜Æé€éŽ HTTP 連到 `server.py`ć€‚åœØęø¬č©¦č£”åÆä»„č·³éŽē¶²č·Æļ¼Œē›“ęŽ„ęŠŠä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«äŗ¤ēµ¦ `Client`:`from server import mcp`ļ¼Œē„¶å¾Œ `Client(mcp)`ć€‚ę²’ęœ‰č™•ē†ēØ‹åŗć€ę²’ęœ‰é€£ęŽ„åŸ ļ¼Œč€ŒäøŠé¢ēš„ęÆå€‹ę–¹ę³•ē”Øčµ·ä¾†éƒ½äø€ęØ£ć€‚ -ęœ‰äø€å€‹å»ŗę§‹å­ę——ęØ™ę˜Æå°ˆē‚ŗę­¤čØ­čØˆēš„ļ¼š`Client(mcp, raise_exceptions=True)`ć€‚å®ƒåŖå°čØ˜ę†¶é«”å…§é€£ē·šęœ‰ä½œē”Øļ¼Œč€Œ **[測試](../get-started/testing.md)** é é¢ęœƒč§£é‡‹å®ƒļ¼Œäø¦åœē¹žå®ƒå»ŗē«‹ę•“å„—ęØ”å¼ć€‚ +ęœ‰äø€å€‹å»ŗę§‹å­ę——ęØ™ę˜Æå°ˆē‚ŗę­¤čØ­čØˆēš„ļ¼š`Client(mcp, raise_exceptions=True)`ć€‚å®ƒåŖå°åŒäø€č™•ē†ēØ‹åŗå…§ēš„é€£ē·šęœ‰ä½œē”Øļ¼Œč€Œ **[測試](../get-started/testing.md)** é é¢ęœƒč§£é‡‹å®ƒļ¼Œäø¦åœē¹žå®ƒå»ŗē«‹ę•“å„—ęØ”å¼ć€‚ ## 重點回锧 {#recap} -* `Client(x)` å°ä¼ŗęœå™Øē‰©ä»¶ä»„čØ˜ę†¶é«”å…§ę–¹å¼é€£ē·šļ¼Œå° URL å­—äø²é€éŽ Streamable HTTP é€£ē·šļ¼Œå…¶ä»–ęƒ…ę³å‰‡é€éŽå‚³č¼øé€£ē·šć€‚ +* `Client(x)` 對 URL å­—äø²é€éŽ Streamable HTTP é€£ē·šļ¼Œå° `StdioServerParameters` å•Ÿå‹•å­č™•ē†ēØ‹åŗļ¼Œå°å‚³č¼øå‰‡ē›“ęŽ„é€²å…„ļ¼ŒåœØęø¬č©¦äø­å‰‡ęŽ„å—ä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«ć€‚ * `async with` å°±ę˜Æę•“å€‹ē”Ÿå‘½é€±ęœŸć€‚åœØč£”é¢ļ¼Œ`server_capabilities` 和 `protocol_version` å·²ē¶“å”«å„½ļ¼›ä¼ŗęœå™Øęœ‰ęä¾›ę™‚ļ¼Œ`server_info` 和 `instructions` ä¹Ÿę˜Æć€‚ * `list_tools()` ēµ¦ä½ ęÆå€‹å·„å…·ēš„ `name`态`title`态`description` 和 `input_schema`怂 * `call_tool()` å›žå‚³ēµ¦ęØ”åž‹ēš„ `content`ć€ēµ¦ēØ‹å¼ē¢¼ēš„ `structured_content`ļ¼Œä»„åŠ `is_error`ć€‚ęœƒå¼•ē™¼ä¾‹å¤–ēš„å·„å…·ę˜Æäø€å€‹ēµęžœļ¼Œäøę˜Æä¾‹å¤–ć€‚ diff --git a/i18n/zh-hant/pages/client/oauth-clients.md b/i18n/zh-hant/pages/client/oauth-clients.md index fd0cb76a..6e369060 100644 --- a/i18n/zh-hant/pages/client/oauth-clients.md +++ b/i18n/zh-hant/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth ē”Øęˆ¶ē«Æ {#oauth-clients} @@ -76,18 +76,20 @@ translation: `Client` ē¬¬äø€ę¬”é€å‡ŗč«‹ę±‚ę™‚ļ¼Œä¼ŗęœå™Øå›žę‡‰ `401`怂provider ęŽ„ę‰‹ļ¼š -1. **ęŽ¢ē“¢ć€‚** č®€å– `WWW-Authenticate` ęØ™é ­ļ¼Œå¾ž `/.well-known/oauth-protected-resource` ęŠ“å–ä¼ŗęœå™Øēš„ Protected Resource Metadataļ¼Œå¾—ēŸ„ę˜Æå“Ŗå€‹ęŽˆę¬Šä¼ŗęœå™ØåœØäæč­·é€™å€‹č³‡ęŗļ¼Œå†åŽ»ęŠ“å–**那個**ä¼ŗęœå™Øēš„äø­ē¹¼č³‡ę–™ć€‚ +1. **ęŽ¢ē“¢ć€‚** č®€å– `WWW-Authenticate` ęØ™é ­ļ¼Œå¾ž `/.well-known/oauth-protected-resource` ęŠ“å–ä¼ŗęœå™Øēš„ Protected Resource Metadataļ¼Œå¾—ēŸ„ę˜Æå“Ŗå€‹ęŽˆę¬Šä¼ŗęœå™ØåœØäæč­·é€™å€‹č³‡ęŗļ¼Œå†åŽ»ęŠ“å–**那個**ä¼ŗęœå™Øēš„äø­ē¹¼č³‡ę–™ć€‚ļ¼ˆč¼ƒčˆŠć€ę²’ęœ‰ē™¼åøƒč³‡ęŗäø­ē¹¼č³‡ę–™ēš„ä¼ŗęœå™Øļ¼Œå‰‡ę”¹å‘å®ƒč‡Ŗå·±ēš„ origin ē“¢å–ęŽˆę¬Šä¼ŗęœå™Øäø­ē¹¼č³‡ę–™ć€‚ļ¼‰ē„”č«–å“ŖēØ®ęƒ…ę³ļ¼Œäø­ē¹¼č³‡ę–™åˆ—å‡ŗēš„ `issuer` éƒ½åæ…é ˆå°±ę˜Æå®ƒę‰€å±¬ēš„é‚£å€‹ä¼ŗęœå™Øļ¼›äøę˜Æēš„č©±äø€å¾‹ę‹’ēµ•ć€‚ 2. **čØ»å†Šć€‚** å„²å­˜åŗ«č£”ä»€éŗ¼éƒ½ę²’ęœ‰ļ¼Ÿå®ƒęœƒē”Øä½ ēš„ `OAuthClientMetadata` å‹•ę…‹čØ»å†Šļ¼Œäø¦ęŠŠēµęžœå­˜čµ·ä¾†ć€‚ 3. **ęŽˆę¬Šć€‚** ē”¢ē”Ÿ PKCE é…å°å’Œäø€å€‹ `state`ļ¼Œēµ„å‡ŗęŽˆę¬Š URL,await ä½ ēš„ `redirect_handler`ļ¼ŒęŽ„č‘— await ä½ ēš„ `callback_handler` å–å¾—ęŽˆę¬Šē¢¼ć€‚ 4. **äŗ¤ę›ć€‚** ę‹æęŽˆę¬Šē¢¼ę›å¾— `OAuthToken`ļ¼Œå­˜čµ·ä¾†ļ¼Œē„¶å¾Œåø¶č‘— `Authorization: Bearer ...` é‡é€ä½ åŽŸęœ¬ēš„č«‹ę±‚ć€‚ ä¹‹å¾Œå®ƒå°±å¾ˆå®‰éœć€‚ę¬Šę–å¾žå„²å­˜åŗ«ę‹æå‡ŗä¾†ē”Øļ¼ŒéŽęœŸēš„å­˜å–ę¬Šę–ē”Øé‡ę–°ę•“ē†ę¬Šę–ę›“ę–°ļ¼ŒåŖęœ‰é€™äŗ›éƒ½č”Œäøé€šę™‚ę‰ęœƒé‡č·‘ę•“å€‹ęµēØ‹ć€‚ +ęœ‰äø€ę¢å‚³č¼øč¦å‰‡é©ē”Øę–¼ę‰€ęœ‰é€™äŗ›č«‹ę±‚ļ¼šå’Œå®ƒå€‘ę‰€åœØēš„é‚£å€‹ MCP č«‹ę±‚äø€ęØ£ļ¼Œé‡ę–°å°Žå‘åŖęœ‰åœØē•™åœØåŒäø€å€‹ origin äø”äæęŒē›øåŒę–¹ę³•ę™‚ę‰ęœƒč·ŸéšØļ¼ˆä¾‹å¦‚č£œäøŠēµå°¾ę–œē·šēš„ 307/308ļ¼‰ļ¼Œå…¶ä»–ä»»ä½•é‡ę–°å°Žå‘éƒ½č¦–ē‚ŗé‚£å€‹ URL ę²’ęœ‰å›žę‡‰ć€‚ + é€™äŗ›ä½ äø€č”Œéƒ½ę²’åÆ«ć€‚é‚„å‰©å…©å€‹é—œéµå­—å¼•ę•øļ¼ˆ`client_metadata_url` 和 `validate_resource_url`ļ¼‰ļ¼Œé€™å€‹ęŖ”ę”ˆå…©å€‹éƒ½ē”Øäøåˆ°ć€‚å€¼å¾—čŖč­˜ēš„ę˜Æ `client_metadata_url`ļ¼Œäø‹é¢ęœ‰å®ƒå°ˆå±¬ēš„äø€ēÆ€ć€‚ ### č©¦č©¦ēœ‹ {#try-it} -é€™ä»½ę–‡ä»¶č£”ēš„å¤§å¤šę•øēÆ„ä¾‹éƒ½čƒ½ē”ØčØ˜ę†¶é«”å…§ēš„ `Client(server)` ęŖ¢é©—ć€‚é€™å€‹äøč”Œļ¼šę•“å€‹ęµēØ‹ēš„é‡é»žå°±ę˜Æäø€å€‹ HTTP `401`ļ¼Œč€ŒčØ˜ę†¶é«”å…§ēš„ē”Øęˆ¶ē«Æå’Œå®ƒēš„ä¼ŗęœå™Øä¹‹é–“ę ¹ęœ¬ę²’ęœ‰ HTTP怂 +ęø¬č©¦č£”ē”Øēš„čØ˜ę†¶é«”å…§ `Client(server)` åœØé€™č£”å¹«äøäøŠåæ™ļ¼šę•“å€‹ęµēØ‹ēš„é‡é»žå°±ę˜Æäø€å€‹ HTTP `401`ļ¼Œč€ŒčØ˜ę†¶é«”å…§ēš„ē”Øęˆ¶ē«Æå’Œå®ƒēš„ä¼ŗęœå™Øä¹‹é–“ę ¹ęœ¬ę²’ęœ‰ HTTP怂 å„²å­˜åŗ«č£”é™„ęœ‰åÆ¦éš›é‹ä½œēš„ē‰ˆęœ¬ć€‚`examples/servers/simple-auth/` ęœƒåŸ·č”Œäø€å€‹ēØē«‹ēš„ęŽˆę¬Šä¼ŗęœå™Øå’Œäø€å€‹å—äæč­·ēš„ MCP ä¼ŗęœå™Øļ¼›`examples/clients/simple-auth-client/` å‰‡ę˜Æé€™äø€é ēš„ē”Øęˆ¶ē«Æé•·ęˆēš„äø€å€‹å°åž‹ CLIć€‚å®ƒēš„ README ęœ‰é‚£å…©å€‹ęŒ‡ä»¤ļ¼šå•Ÿå‹•ä¼ŗęœå™Øć€å°č‘—å®ƒå€‘åŸ·č”Œē”Øęˆ¶ē«Æļ¼Œå°±čƒ½ēœ‹č‘—äøŠé¢å››å€‹ę­„é©Ÿä¾åŗē™¼ē”Ÿć€‚ @@ -105,13 +107,14 @@ URL åæ…é ˆę˜Æ HTTPS äø”č·Æå¾‘äøčƒ½ę˜Æę ¹č·Æå¾‘ļ¼›å¦å‰‡åœØå»ŗę§‹ę™‚å°±ęœƒå¼•ē™¼ `ClientCredentialsOAuthProvider` ę˜ÆåŒäø€å€‹ `httpx2.Auth`ļ¼ŒåŖę˜Æå°‘äŗ†äŗŗļ¼š -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` ę”¹č®Šēš„åœ°ę–¹ļ¼š * ę²’ęœ‰ `OAuthClientMetadata`ļ¼Œę²’ęœ‰č™•ē†å‡½å¼ć€‚å‚³å…„ `client_id` 和 `client_secret`ļ¼›provider ęœƒåœē¹žå®ƒå€‘å»ŗå‡ŗäø€ē­†ęœ€ē²¾ē°”ēš„ `client_credentials` čØ»å†Šļ¼Œäø¦å®Œå…Øč·³éŽå‹•ę…‹čØ»å†Šć€‚ +* `issuer` ęŒ‡ę˜Žē™¼å‡ŗé€™ēµ„ę†‘č­‰ēš„ęŽˆę¬Šä¼ŗęœå™Øļ¼›ē”Øå®ƒēš„ `/.well-known/oauth-authorization-server` ę–‡ä»¶å›žå‚³ēš„ `issuer` å€¼ć€‚ęŽ¢ē“¢ä»ē…§äøŠé¢é‚£ęØ£åŸ·č”Œļ¼Œä½†ę¬Šę–č«‹ę±‚ę°øé åŖęœƒē”Ø**那個** issuer ēš„äø­ē¹¼č³‡ę–™ä¾†ēµ„č£ļ¼›å¦‚ęžœ MCP ä¼ŗęœå™ØęŒ‡å‘åˆ„ēš„åœ°ę–¹ļ¼ŒęµēØ‹ęœƒåœäø‹äø¦å¼•ē™¼ `OAuthFlowError`ć€‚ēœē•„å®ƒå·²ę£„ē”Øļ¼Œ3.0 čµ·ęœƒč®Šęˆåæ…å”«ļ¼ˆč¦‹ **[å·²ę£„ē”Øēš„åŠŸčƒ½](../deprecated.md#deprecated-sdk-helpers)**ļ¼‰ļ¼›åœØé‚£ä¹‹å‰ļ¼Œprovider ęœƒē™¼å‡ŗč­¦å‘Šļ¼Œäø¦ä½æē”ØęŽ¢ē“¢ę‰¾åˆ°ēš„é‚£å€‹ęŽˆę¬Šä¼ŗęœå™Øć€‚ * `scope` ę˜Æä»„ē©ŗę ¼åˆ†éš”ēš„å­—äø²ļ¼Œä¹Ÿå°±ę˜Æ OAuth ēš„ē·šč·Æę ¼å¼ć€‚ * äø‹ęøøēš„äø€åˆ‡å®Œå…Øē›øåŒļ¼šåŒęØ£ēš„ `TokenStorage`ć€åŒęØ£ēš„ `httpx2.AsyncClient(auth=...)`ć€åŒęØ£ēš„ `streamable_http_client`怂 @@ -121,7 +124,7 @@ URL åæ…é ˆę˜Æ HTTPS äø”č·Æå¾‘äøčƒ½ę˜Æę ¹č·Æå¾‘ļ¼›å¦å‰‡åœØå»ŗę§‹ę™‚å°±ęœƒå¼•ē™¼ `client_secret` č¦å¾žē’°å¢ƒč®Šę•øęˆ–ē„•åÆ†ē®”ē†å·„å…·č®€å–ļ¼Œēµ•å°äøč¦ę”¾é€²ē‰ˆęœ¬ęŽ§åˆ¶ć€‚ !!! info - `mcp.client.auth.extensions.client_credentials` č£”é‚„ęœ‰äø€å€‹ provider:**`PrivateKeyJWTOAuthProvider`**ļ¼Œēµ¦ē”Ø JWT č€Œéžå…±ē”Ø secret ä¾†é©—č­‰ēš„ē”Øęˆ¶ē«Æä½æē”Øļ¼ˆ`private_key_jwt`ļ¼Œä¹Ÿå°±ę˜Æé‡‘é‘°å°čˆ‡å·„ä½œč² č¼‰čŗ«åˆ†é‚£äø€é”žļ¼‰ć€‚å®ƒéµå¾ŖåŒęØ£ēš„ęØ”å¼ļ¼šå»ŗę§‹äø€å€‹ļ¼Œę”¾åˆ° `auth=` äøŠć€‚åŒäø€å€‹ęØ”ēµ„é‚„é™„äŗ† `SignedJWTParameters` 和 `static_assertion_provider`ļ¼Œå…©å€‹ē”Øä¾†å»ŗå‡ŗå…¶ assertion ēš„č¼”åŠ©å·„å…·ć€‚ + `mcp.client.auth.extensions.client_credentials` č£”é‚„ęœ‰äø€å€‹ provider:**`PrivateKeyJWTOAuthProvider`**ļ¼Œēµ¦ē”Ø JWT č€Œéžå…±ē”Ø secret ä¾†é©—č­‰ēš„ē”Øęˆ¶ē«Æä½æē”Øļ¼ˆ`private_key_jwt`ļ¼Œä¹Ÿå°±ę˜Æé‡‘é‘°å°čˆ‡å·„ä½œč² č¼‰čŗ«åˆ†é‚£äø€é”žļ¼‰ć€‚å®ƒéµå¾ŖåŒęØ£ēš„ęØ”å¼ļ¼šå»ŗę§‹äø€å€‹ļ¼ˆå®ƒęŽ„å—åŒęØ£éøē”Øēš„ `issuer`ļ¼‰ļ¼Œę”¾åˆ° `auth=` äøŠć€‚åŒäø€å€‹ęØ”ēµ„é‚„é™„äŗ† `SignedJWTParameters` 和 `static_assertion_provider`ļ¼Œå…©å€‹ē”Øä¾†å»ŗå‡ŗå…¶ assertion ēš„č¼”åŠ©å·„å…·ć€‚ é‚„ęœ‰äø€ēØ®ē„”äŗŗä»‹å…„ēš„ęƒ…å¢ƒļ¼šē”Øęˆ¶ē«Æå±¬ę–¼ęŸå€‹ä¼ę„­ļ¼Œē”±ä¼ę„­ēš„čŗ«åˆ†ęä¾›č€…ļ¼ˆč€Œéžä½æē”Øč€…ļ¼‰ę±ŗå®šå®ƒåÆä»„é€£åˆ°å“Ŗäŗ› MCP ä¼ŗęœå™Øć€‚é‚£ę˜Æå¦äø€ēØ®ęŽˆę¬Šé”žåž‹ļ¼Œęœ‰č‡Ŗå·±ēš„äæ”ä»»ęØ”åž‹ļ¼Œä¹Ÿęœ‰č‡Ŗå·±ēš„é é¢ļ¼š**[čŗ«åˆ†ę–·čØ€](identity-assertion.md)**怂 diff --git a/i18n/zh-hant/pages/client/session-groups.md b/i18n/zh-hant/pages/client/session-groups.md index 504fd1c1..d8ec72fd 100644 --- a/i18n/zh-hant/pages/client/session-groups.md +++ b/i18n/zh-hant/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # å·„ä½œéšŽę®µē¾¤ēµ„ {#session-groups} @@ -76,7 +76,7 @@ translation: ## 重點回锧 {#recap} * `ClientSessionGroup` ę”ęœ‰å¤šę¢ä¼ŗęœå™Øé€£ē·šļ¼Œäø¦ęŠŠå®ƒå€‘ēš„å·„å…·ć€č³‡ęŗå’Œęē¤ŗč©žå„č‡Ŗåˆä½µęˆäø€å€‹ `dict`怂 -* ęÆå°ä¼ŗęœå™Øå‘¼å«äø€ę¬” `connect_to_server(params)`ć€‚å®ƒęŽ„å—å‚³č¼øåƒę•øļ¼Œēµ•äøę˜Æ `Client` ęŽ„å—ēš„ä¼ŗęœå™Øē‰©ä»¶ęˆ– URL怂 +* ęÆå°ä¼ŗęœå™Øå‘¼å«äø€ę¬” `connect_to_server(params)`ć€‚å®ƒęŽ„å—å‚³č¼øåƒę•øļ¼Œēµ•äøę˜Æ `Client` ęŽ„å—ēš„ URL ꈖ `Transport`怂 * `group.call_tool(name, arguments)` ęœƒę›æä½ č½‰é€åˆ°ę“ęœ‰č©²å·„å…·ēš„ä¼ŗęœå™Øć€‚ * åēØ±åœØę•“å€‹ē¾¤ēµ„å…§åæ…é ˆå”Æäø€ļ¼›å…©å°éƒ½ęœ‰ `search` å·„å…·ēš„ä¼ŗęœå™Øē„”ę³•åŽŸęØ£å…±å­˜ć€‚ * `component_name_hook=` ęœƒę”¹åÆ«ęÆå€‹ē™»čØ˜ēš„åēØ±ć€‚dict ēš„éµęœƒč®Šļ¼Œē·šč·ÆäøŠēš„åēØ±äøč®Šć€‚ diff --git a/i18n/zh-hant/pages/client/transports.md b/i18n/zh-hant/pages/client/transports.md index a7f839d4..5d626ba6 100644 --- a/i18n/zh-hant/pages/client/transports.md +++ b/i18n/zh-hant/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # ē”Øęˆ¶ē«Æå‚³č¼øę–¹å¼ {#client-transports} @@ -11,30 +11,15 @@ translation: ęÆēØ®å‚³č¼øę–¹å¼ēš„**ä¼ŗęœå™Ø**端(`mcp.run()` åšēš„äŗ‹ļ¼Œä»„åŠä½ éƒØē½²ēš„ę±č„æļ¼‰č«‹č¦‹ **[åŸ·č”Œä¼ŗęœå™Ø](../run/index.md)**怂 -## čØ˜ę†¶é«”å…§ {#in-memory} - -ē›“ęŽ„å‚³å…„ä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«ļ¼š - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œę²’ęœ‰é€£ęŽ„åŸ ļ¼Œē·šč·ÆäøŠä¹Ÿę²’ęœ‰ä»»ä½•ä½å…ƒēµ„ć€‚ē”Øęˆ¶ē«Æå’Œä¼ŗęœå™Øę˜ÆåŒäø€å€‹č™•ē†ēØ‹åŗč£”ēš„å…©å€‹ē‰©ä»¶ļ¼Œä½†å‘¼å«ä»ē„¶ęœƒē¶“éŽēœŸę­£ēš„å”å®šå±¤ļ¼š`search_books` č¢«åˆ—å‡ŗć€é©—č­‰ć€å‘¼å«ēš„ę–¹å¼ļ¼Œå’Œé€éŽ HTTP ę™‚å®Œå…Øäø€ęØ£ć€‚ - -ę‰€ä»„å®ƒåŒę™‚ę˜Æå…©ęØ£ę±č„æļ¼š - -* **測試巄具。** é€™ä»½čŖŖę˜Žę–‡ä»¶č£”ēš„ęÆå€‹ēÆ„ä¾‹éƒ½ę˜Æé€™ęØ£č·‘éŽēš„ļ¼Œč€Œ **[測試](../get-started/testing.md)** é‚£äø€é ę•“å€‹ęØ”å¼å°±ę˜Æåœē¹žå®ƒå»ŗē«‹ēš„ć€‚ -* **åµŒå…„ē”Øēš„ API怂** č‡Ŗå·±å»ŗē«‹ä¼ŗęœå™Øēš„ę‡‰ē”ØēØ‹å¼ļ¼Œäøéœ€č¦ē¹žäø€åœˆē¶²č·Æå°±čƒ½å‘¼å«å®ƒēš„å·„å…·ć€‚ - ## Streamable HTTP {#streamable-http} -傳兄一個 URL å­—äø²ļ¼Œå¾—åˆ°ēš„å°±ę˜Æ **Streamable HTTP**ļ¼Œä¹Ÿå°±ę˜ÆéƒØē½²ę™‚ä½æē”Øēš„é‚£ēØ®å‚³č¼øę–¹å¼ļ¼š +傳兄一個 URL å­—äø²ļ¼Œå¾—åˆ°ēš„å°±ę˜Æ **Streamable HTTP**ļ¼Œä¹Ÿå°±ę˜ÆéƒØē½²ę™‚ä½æē”Øć€ä¹Ÿč©²å„Ŗå…ˆéøē”Øēš„å‚³č¼øę–¹å¼ļ¼š ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -é€™å°±ę˜Æå®Œę•“ēš„ę­£å¼ē’°å¢ƒē”Øęˆ¶ē«Æć€‚`Client` ęœƒę›æä½ ęŠŠ URL 包進 `streamable_http_client(...)`ļ¼Œåŗ•äø‹ę˜Æäø€å€‹ä¾ MCP éœ€ę±‚čØ­å®šå„½ēš„ `httpx2.AsyncClient`:`follow_redirects=True`态connect/write/pool ēš„é€¾ę™‚ē‚ŗ 30 ē§’ļ¼Œread é€¾ę™‚å‰‡ę˜Æ 300 ē§’ļ¼Œå› ē‚ŗä¼ŗęœå™ØåÆčƒ½ęœƒč®“å›žę‡‰äø²ęµäø€ē›“é–‹č‘—ć€‚ +é€™å°±ę˜Æå®Œę•“ēš„ę­£å¼ē’°å¢ƒē”Øęˆ¶ē«Æć€‚`Client` ęœƒę›æä½ ęŠŠ URL 包進 `streamable_http_client(...)`ļ¼Œåŗ•äø‹ę˜Æäø€å€‹ä¾ MCP éœ€ę±‚čØ­å®šå„½ēš„ `httpx2.AsyncClient`:connect/write/pool ēš„é€¾ę™‚ē‚ŗ 30 ē§’ļ¼Œread é€¾ę™‚å‰‡ę˜Æ 300 ē§’ļ¼Œå› ē‚ŗä¼ŗęœå™ØåÆčƒ½ęœƒč®“å›žę‡‰äø²ęµäø€ē›“é–‹č‘—ć€‚ !!! check å»ŗē«‹å„½ēš„ `Client` **é‚„ę²’ęœ‰**é€£ē·šć€‚å»ŗē«‹åŖę˜Æéøå®šå‚³č¼øę–¹å¼ļ¼›ēœŸę­£é–‹å•Ÿå®ƒēš„ę˜Æ `async with`ć€‚åœØé€²å…„ä¹‹å‰å°±åŽ»ę‹æé€£ē·šļ¼ŒSDK ęœƒē›“ęŽ„å‘ŠčØ“ä½ ļ¼š @@ -49,7 +34,7 @@ translation: äø€ę—¦éœ€č¦ `Authorization` 標頭、cookie态proxy态mTLSļ¼Œęˆ–äøåŒēš„é€¾ę™‚ļ¼Œå°±č‡Ŗå·±å»ŗē«‹ `httpx2.AsyncClient`ļ¼Œå†äŗ¤ēµ¦ `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -70,7 +55,25 @@ translation: ę‰€ęœ‰č·Ÿ HTTP ęœ‰é—œēš„čØ­å®šļ¼Œē¾åœØéƒ½ę”¾åœØä½ å‚³å…„ēš„é‚£äø€å€‹ `httpx2.AsyncClient` äøŠć€‚ !!! info - `httpx2` äæē•™äŗ†ē†Ÿę‚‰ēš„ `httpx` APIļ¼Œę‰€ä»„åŖč¦ęœƒē”Ø `httpx`ļ¼Œå°±å·²ē¶“ēŸ„é“é€™č£”ēš„é©—č­‰ć€proxyć€äŗ‹ä»¶ęŽ›é‰¤ć€é‡č©¦å’Œé€£ē·šę•øé™åˆ¶č©²ę€Žéŗ¼åšć€‚SDK ę²’ęœ‰åœØäøŠé¢åŠ ä»»ä½•ę±č„æļ¼Œä¹Ÿę²’ęœ‰ę‹æęŽ‰ä»»ä½•ę±č„æć€‚OAuth ä¹Ÿę˜Æå¾žé€™č£”ęŽ„äøŠēš„ļ¼š`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。敓個流程請見 **[OAuth ē”Øęˆ¶ē«Æ](oauth-clients.md)**怂 + `httpx2` äæē•™äŗ†ē†Ÿę‚‰ēš„ `httpx` APIļ¼Œę‰€ä»„åŖč¦ęœƒē”Ø `httpx`ļ¼Œå°±å·²ē¶“ēŸ„é“é€™č£”ēš„é©—č­‰ć€proxyć€äŗ‹ä»¶ęŽ›é‰¤ć€é‡č©¦å’Œé€£ē·šę•øé™åˆ¶č©²ę€Žéŗ¼åšć€‚SDK ę²’ęœ‰åœØäøŠé¢åŠ ä»»ä½•ę±č„æļ¼Œä¹Ÿę²’ęœ‰ę‹æęŽ‰ä»»ä½•ę±č„æļ¼Œ[é‡ę–°å°Žå‘ēš„č™•ē†](#redirects)除外。OAuth ä¹Ÿę˜Æå¾žé€™č£”ęŽ„äøŠēš„ļ¼š`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。敓個流程請見 **[OAuth ē”Øęˆ¶ē«Æ](oauth-clients.md)**怂 + +### é‡ę–°å°Žå‘ {#redirects} + +å‚³č¼øåŖęœƒé€£åˆ°ä½ ēµ¦å®ƒēš„é‚£å€‹ URLļ¼Œč€Œäø”åŖé™é‚£å€‹ä¾†ęŗļ¼ˆorigin)。 + +* ē•™åœØåŒäø€å€‹ schemeć€äø»ę©Ÿå’Œé€£ęŽ„åŸ äøŠēš„ `307`/`308` é‡ę–°å°Žå‘ęœƒč·ŸéšØļ¼ŒåŒäø€å°äø»ę©ŸäøŠēš„ `http://` → `https://` ä¹Ÿęœƒć€‚é€™ę¶µč“‹äŗ†åøøč¦‹ēš„ `/mcp` → `/mcp/` ēµå°¾ę–œē·šé‡ę–°å°Žå‘ć€‚ +* å°Žå‘å…¶ä»–ä»»ä½•åœ°ę–¹ēš„é‡ę–°å°Žå‘å‰‡**äøęœƒ**č·ŸéšØć€‚å‘¼å«ęœƒå¤±ę•—ļ¼Œäø¦å‡ŗē¾ļ¼š + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + å¦‚ęžœé‚£å€‹ URL å°±ę˜Æä½ č¦ēš„ä¼ŗęœå™Øļ¼ŒęŠŠå®ƒåÆ«é€²čØ­å®šč£”ć€‚å¦‚ęžœäøę˜Æļ¼Œé‚£å°±ę˜Æä¼ŗęœå™Øęˆ–å®ƒå‰é¢ēš„ proxy čØ­å®šęœ‰čŖ¤ć€‚ + +äøē®”å‚³å…„å“Ŗå€‹ `httpx2.AsyncClient` éƒ½äø€ęØ£ļ¼šMCP č«‹ę±‚äøęœƒåƒč€ƒå®ƒēš„ `follow_redirects` čØ­å®šļ¼Œäøč«–čØ­ęˆå“Ŗå€‹ę–¹å‘ć€‚SDK ēš„ OAuth provider å°č‡Ŗå·±ē™¼å‡ŗēš„č«‹ę±‚ä¹Ÿå„—ē”ØåŒäø€ę¢č¦å‰‡ć€‚ + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` č”Øē¤ŗä¼ŗęœå™Øå‰é¢ęœ‰äø€å€‹å®ƒäøēŸ„é“ēš„ć€č² č²¬ēµ‚ēµ TLS ēš„ proxyļ¼Œę‰€ä»„å®ƒē™¼å‡ŗēš„ę˜Æ `http://` é‡ę–°å°Žå‘ć€‚é€™č¦åœØä¼ŗęœå™Øē«Æäæ®ę­£ļ¼ˆ**[éƒØē½²čˆ‡ę““å±•](../run/deploy.md#behind-a-tls-terminating-proxy)**ļ¼‰ļ¼Œęˆ–č€…ę”¹ē”ØčØŠęÆč£”å»ŗč­°ēš„é‚£å€‹ē¢ŗåˆ‡ēš„ `https://…/` URL怂 ## stdio {#stdio} @@ -91,6 +94,18 @@ translation: éœ€č¦ API é‡‘é‘°ēš„ä¼ŗęœå™ØåœØé‚£č£”ę˜Æę‰¾äøåˆ°ēš„ć€‚č«‹ē”Ø `env=` ę˜Žē¢ŗå‚³å…„ļ¼›é€™äŗ›č®Šę•øęœƒē–ŠåŠ åœØå…čØ±ęø…å–®ä¹‹äøŠć€‚äøŠé¢ēš„ `BOOKSHOP_API_KEY` åšēš„å°±ę˜Æé€™ä»¶äŗ‹ć€‚ +## čØ˜ę†¶é«”å…§ {#in-memory} + +åœØęø¬č©¦č£”ę²’ęœ‰ę±č„æč¦éƒØē½²ļ¼Œä¹Ÿę²’ęœ‰ę±č„æč¦å•Ÿå‹•ć€‚ē›“ęŽ„å‚³å…„ä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«ļ¼š + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œę²’ęœ‰é€£ęŽ„åŸ ļ¼Œē·šč·ÆäøŠä¹Ÿę²’ęœ‰ä»»ä½•ä½å…ƒēµ„ć€‚ē”Øęˆ¶ē«Æå’Œä¼ŗęœå™Øę˜ÆåŒäø€å€‹č™•ē†ēØ‹åŗč£”ēš„å…©å€‹ē‰©ä»¶ļ¼Œä½†å‘¼å«ä»ē„¶ęœƒē¶“éŽēœŸę­£ēš„å”å®šå±¤ļ¼š`search_books` č¢«åˆ—å‡ŗć€é©—č­‰ć€å‘¼å«ēš„ę–¹å¼ļ¼Œå’Œé€éŽ HTTP ę™‚å®Œå…Øäø€ęØ£ć€‚**[測試](../get-started/testing.md)** é‚£äø€é ę•“å€‹ęØ”å¼å°±ę˜Æåœē¹žå®ƒå»ŗē«‹ēš„ć€‚ + +åŒęØ£ēš„åÆ«ę³•ä¹ŸåÆä»„ē•¶ęˆåµŒå…„ē”Øēš„ APIļ¼šč‡Ŗå·±å»ŗē«‹ä¼ŗęœå™Øēš„ę‡‰ē”ØēØ‹å¼ļ¼Œäøéœ€č¦ē¹žäø€åœˆē¶²č·Æå°±čƒ½å‘¼å«å®ƒēš„å·„å…·ć€‚ + ## SSE {#sse} `mcp.client.sse` č£”ēš„ `sse_client(url)` 是被 Streamable HTTP å–ä»£ēš„é‚£å€‹ HTTP å‚³č¼øć€‚č¦å’Œé‚„åœØč¬›å®ƒēš„ä¼ŗęœå™Øęŗé€šļ¼Œē”ØåŒęØ£ēš„ę–¹å¼åŒ…čµ·ä¾†å³åÆļ¼š`Client(sse_client("http://localhost:8000/sse"))`ļ¼Œä½†äøč¦åœØå®ƒäøŠé¢č“‹ä»»ä½•ę–°ę±č„æć€‚ @@ -99,15 +114,16 @@ translation: 對 `Client` ä¾†čŖŖļ¼ŒäøŠé¢é€™äŗ›å…Øéƒ½ę˜ÆåŒäø€ēØ®ę±č„æć€‚ -**傳輸**ę˜Æä»»ä½•ęœƒē”¢å‡ŗäø€å° `(read, write)` čØŠęÆäø²ęµēš„éžåŒę­„ context managerļ¼šę­£å¼åœ°čŖŖļ¼Œå°±ę˜Æ `mcp.client` č£”ēš„ `Transport` å”å®šć€‚`Client` ä¾åž‹åˆ„č§£ęžå®ƒēš„å¼•ę•øļ¼šä¼ŗęœå™Øē‰©ä»¶å°±åœØč™•ē†ēØ‹åŗå…§é€£ē·šļ¼Œ`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`ļ¼Œå…¶ä»–ä»»ä½•ę±č„æå‰‡ē›“ęŽ„ē•¶ęˆå‚³č¼øé€²å…„ć€‚ęœ€å¾Œé€™ę¢č¦å‰‡å°±ę˜Æē‚ŗä»€éŗ¼ `stdio_client(...)`态`streamable_http_client(...)` 和 `sse_client(...)` éƒ½čƒ½ę”¾é€²åŒäø€å€‹ä½ē½®ļ¼Œä¹Ÿę˜Æē‚ŗä»€éŗ¼ä½ åÆä»„č‡Ŗå·±åÆ«äø€å€‹ć€‚ +**傳輸**ę˜Æä»»ä½•ęœƒē”¢å‡ŗäø€å° `(read, write)` čØŠęÆäø²ęµēš„éžåŒę­„ context managerļ¼šę­£å¼åœ°čŖŖļ¼Œå°±ę˜Æ `mcp.client` č£”ēš„ `Transport` å”å®šć€‚`Client` ä¾åž‹åˆ„č§£ęžå®ƒēš„å¼•ę•øļ¼š`str` 會變成 `streamable_http_client(url)`,`StdioServerParameters` 會變成 `stdio_client(params)`ļ¼Œä¼ŗęœå™Øē‰©ä»¶å°±åœØč™•ē†ēØ‹åŗå…§é€£ē·šļ¼Œå…¶ä»–ä»»ä½•ę±č„æå‰‡ē›“ęŽ„ē•¶ęˆå‚³č¼øé€²å…„ć€‚ęœ€å¾Œé€™ę¢č¦å‰‡å°±ę˜Æē‚ŗä»€éŗ¼ `stdio_client(...)`态`streamable_http_client(...)` 和 `sse_client(...)` éƒ½čƒ½ę”¾é€²åŒäø€å€‹ä½ē½®ļ¼Œä¹Ÿę˜Æē‚ŗä»€éŗ¼ä½ åÆä»„č‡Ŗå·±åÆ«äø€å€‹ć€‚ ## 重點回锧 {#recap} -* `Client(mcp)`ļ¼ˆä¼ŗęœå™Øē‰©ä»¶ļ¼‰åœØčØ˜ę†¶é«”å…§é€£ē·šć€‚ē”ØåœØęø¬č©¦å’ŒåµŒå…„ć€‚ * `Client("http://.../mcp")`(URLļ¼‰é€éŽ Streamable HTTP é€£ē·šļ¼Œä¹Ÿå°±ę˜Æę­£å¼ē’°å¢ƒē”Øēš„å‚³č¼øę–¹å¼ć€‚ * 標頭、驗證、proxy å’Œé€¾ę™‚éƒ½ę”¾åœØä½ å‚³ēµ¦ `streamable_http_client(url, http_client=...)` ēš„ `httpx2.AsyncClient` äøŠć€‚ę²’ęœ‰ `headers=` é€™å€‹é—œéµå­—å¼•ę•øć€‚ +* é‡ę–°å°Žå‘åŖåœØ URL č‡Ŗå·±ēš„ä¾†ęŗå…§č·ŸéšØļ¼ˆēµå°¾ę–œē·šēš„ `307`/`308`ļ¼‰ļ¼Œå¤–åŠ åŒäø€å°äø»ę©ŸäøŠēš„ `http`→`https`ć€‚å…¶ä»–ēš„äø€å¾‹å¤±ę•—äø¦å‡ŗē¾ `Redirect to … not followed`ļ¼›ęŠŠęœ€ēµ‚ēš„ URL åÆ«é€²čØ­å®šå³åÆć€‚ * stdio 是 `Client(StdioServerParameters(...))`ć€‚åŖęœ‰č¦ęŠŠå­č™•ē†ēØ‹åŗēš„ stderr å°Žåˆ°åˆ„č™•ę™‚ļ¼Œę‰éœ€č¦č‡Ŗå·±ē”Ø `stdio_client(...)` åŒ…čµ·ä¾†ć€‚ * å­č™•ē†ēØ‹åŗę‹æåˆ°ēš„ę˜Æå…čØ±ęø…å–®äøŠēš„ē’°å¢ƒļ¼Œäøę˜Æä½ ēš„ē’°å¢ƒļ¼›`env=` ęœƒå¾€äøŠåŠ ć€‚ +* `Client(mcp)`ļ¼ˆä¼ŗęœå™Øē‰©ä»¶ļ¼‰åœØčØ˜ę†¶é«”å…§é€£ē·šć€‚ē”ØåœØęø¬č©¦č£”ļ¼Œęˆ–ę˜ÆęŠŠä¼ŗęœå™ØåµŒå…„å»ŗē«‹å®ƒēš„é‚£å€‹ę‡‰ē”ØēØ‹å¼ć€‚ * å‚³č¼øå°±ę˜Æä»»ä½•åÆä»„ `async with x as (read, write)` ēš„ę±č„æć€‚åŖč¦äøę˜Æä¼ŗęœå™Øē‰©ä»¶ć€URL ꈖ `StdioServerParameters`,`Client` å°±ęœƒē›“ęŽ„äŗ¤ēµ¦é‚£å€‹å”å®šč™•ē†ć€‚ * 建立 `Client` ę˜Æéøå®šå‚³č¼øę–¹å¼ć€‚`async with` ę‰ę˜Æé–‹å•Ÿå®ƒć€‚ diff --git a/i18n/zh-hant/pages/deprecated.md b/i18n/zh-hant/pages/deprecated.md index ca8137da..ad5540b8 100644 --- a/i18n/zh-hant/pages/deprecated.md +++ b/i18n/zh-hant/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # å·²ę£„ē”Øēš„åŠŸčƒ½ {#deprecated-features} -2026-07-28 č¦ę ¼č®“äŗ”ęØ£ę±č„æé€€å “ć€‚SDK ä»ē„¶åÆ¦ä½œäŗ†å…¶äø­ęÆäø€é …ļ¼Œč€ŒęÆäø€é …ē¾åœØéƒ½åø¶ęœ‰**ę£„ē”Øč­¦å‘Š**ć€‚å¦å¤–ęœ‰äø€å€‹ SDK č¼”åŠ©å‡½å¼ę˜Æå› ē‚ŗč‡Ŗčŗ«ēš„åŽŸå› ę£„ē”Øļ¼Œåˆ—åœØ[ęœ€å¾Œ](#deprecated-sdk-helpers)怂 +2026-07-28 č¦ę ¼č®“äŗ”ęØ£ę±č„æé€€å “ć€‚SDK ä»ē„¶åÆ¦ä½œäŗ†å…¶äø­ęÆäø€é …ļ¼Œč€ŒęÆäø€é …ē¾åœØéƒ½åø¶ęœ‰**ę£„ē”Øč­¦å‘Š**ć€‚å¦å¤–ęœ‰å¹¾é … SDK å±¤ē“šēš„ę£„ē”Øę˜Æå‡ŗę–¼č‡Ŗčŗ«ēš„åŽŸå› ļ¼Œåˆ—åœØ[ęœ€å¾Œ](#deprecated-sdk-helpers)怂 äø‹č”Øåˆ—å‡ŗęÆäø€é …å·²ę£„ē”Øēš„åŠŸčƒ½ć€å®ƒē‚ŗä»€éŗ¼č¦é€€å “ļ¼Œä»„åŠę‡‰č©²ę”¹ē”Øēš„ę›æä»£åšę³•ć€‚ @@ -125,11 +125,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## å·²ę£„ē”Øēš„ SDK č¼”åŠ©å‡½å¼ {#deprecated-sdk-helpers} -é€™äŗ›äøę˜Æč¦ę ¼č®Šę›“ļ¼ŒåŖę˜Æęœ‰äŗ†ę›“å„½ę›æä»£åšę³•ēš„ SDK å…§éƒØåÆ¦ä½œć€‚å®ƒå€‘ē”ØåŒęØ£ēš„ `MCPDeprecationWarning` ē™¼å‡ŗč­¦å‘Šļ¼Œäø¦ęœƒåœØ 3.0 移除。 +é€™äŗ›äøę˜Æč¦ę ¼č®Šę›“ļ¼ŒåŖę˜Æęœ‰äŗ†ę›“å„½ę›æä»£åšę³•ēš„ SDK ē”Øę³•ć€‚å®ƒå€‘ē”ØåŒęØ£ēš„ `MCPDeprecationWarning` ē™¼å‡ŗč­¦å‘Šļ¼ŒčˆŠēš„åÆ«ę³•ęœƒåœØ 3.0 移除。 | 已棄用項目 | ę›æä»£åšę³• | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | 先 `FuncMetadata.validate_arguments()`ļ¼Œå† `FuncMetadata.call_fn()`ć€‚åŖęœ‰ē›“ęŽ„ę“ä½œ `FuncMetadata` ēš„ēØ‹å¼ē¢¼ļ¼ˆä¾‹å¦‚č‡ŖčØ‚ēš„ `Tool` å­é”žåˆ„ļ¼‰ę‰ęœƒå‘¼å«éŽå®ƒć€‚ | +| ę²’ęœ‰čØ­å®š `validate_token_resource=` ēš„ `AuthSettings(resource_server_url=...)` | 把它設儽:`True` ęœƒč®“ä¼ŗęœå™Øę‹’ēµ•é©—č­‰å™Øę²’ęœ‰å›žå ±ē‚ŗę øē™¼ēµ¦ `resource_server_url` ēš„ bearer ę¬Šę–ļ¼›`False` č”Øē¤ŗä½ ēš„é©—č­‰å™Øęœƒč‡Ŗå·±ęŖ¢ęŸ„ę¬Šę–ēš„ audienceļ¼ˆč«‹č¦‹ **[ęŽˆę¬Š](run/authorization.md#a-token-verifier)**ļ¼‰ć€‚äøčØ­å®šę™‚ēš„č”Œē‚ŗē­‰åŒ `False`ļ¼›3.0 čµ·ļ¼ŒåŖč¦čØ­äŗ† `resource_server_url`,預設就是 `True`怂 | +| ę²’ęœ‰å‚³å…„ `issuer=` ēš„ `ClientCredentialsOAuthProvider(...)` ꈖ `PrivateKeyJWTOAuthProvider(...)` | 傳兄 `issuer=`ļ¼ŒęŒ‡ę˜Žę øē™¼é€™ēµ„ę†‘č­‰ēš„ęŽˆę¬Šä¼ŗęœå™Øļ¼ˆč«‹č¦‹ **[撰寫 OAuth ē”Øęˆ¶ē«Æ](client/oauth-clients.md#machine-to-machine)**ļ¼‰ć€‚å°‘äŗ†å®ƒļ¼Œå°±č®Šęˆē”± MCP ä¼ŗęœå™Øę±ŗå®šå“Ŗå€‹ęŽˆę¬Šä¼ŗęœå™Øęœƒę”¶åˆ°é€™ēµ„ę†‘č­‰ļ¼›3.0 ęœƒęŠŠé€™å€‹é—œéµå­—å¼•ę•øę”¹ē‚ŗåæ…å”«ć€‚ | ## 重點回锧 {#recap} @@ -138,7 +140,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * ę£„ē”ØåŖę˜Æå‹øå‘Šę€§č³Ŗļ¼šē·šč·Æę²’ęœ‰č®Šę›“ļ¼Œäø€åˆ‡åœØ 2026 ä¹‹å‰ēš„å·„ä½œéšŽę®µäøŠéƒ½čƒ½ē¹¼ēŗŒé‹ä½œļ¼Œč€Œäø”ä½ ęœƒēœ‹åˆ°ę˜Žé”Æēš„ `MCPDeprecationWarning`(它是 `UserWarning`ļ¼Œę‰€ä»„é čØ­å°±ęœƒé”Æē¤ŗļ¼‰ć€‚ * å–ęØ£å’Œę ¹ē›®éŒ„é‚„é”å¤–éœ€č¦äø€ę¢åå‘é€šé“ļ¼Œč€Œ 2026-07-28 ēš„å·„ä½œéšŽę®µę²’ęœ‰ć€‚åœØē¾ä»£é€£ē·šäøŠļ¼Œå®ƒå€‘ęœƒå…ˆč­¦å‘Šļ¼Œå†å¼•ē™¼ä¾‹å¤–ć€‚ * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` ęœƒč®“ę•“å€‹é”žåˆ„éœéŸ³ļ¼›pytest č£”ēš„ `"error::mcp.MCPDeprecationWarning"` å‰‡ęŠŠå®ƒč®Šęˆęø¬č©¦å¤±ę•—ć€‚ -* ęœ‰äø€å€‹ SDK č¼”åŠ©å‡½å¼ `FuncMetadata.call_fn_with_arg_validation()` å¦å¤–å–®ēØę£„ē”Øļ¼Œé čØˆåœØ 3.0 移除。 +* [SDK å±¤ē“šēš„ę£„ē”Ø](#deprecated-sdk-helpers)éµå¾ŖåŒęØ£ēš„č¦å‰‡ļ¼šē¾åœØē™¼å‡ŗč­¦å‘Šļ¼Œ3.0 ē§»é™¤čˆŠēš„åÆ«ę³•ć€‚ * ę–°ēØ‹å¼ē¢¼äøę‡‰č©²å»ŗē«‹åœØé€™äŗ›ę±č„æä¹‹äøŠć€‚ é€™ä»½čŖŖę˜Žę–‡ä»¶ēš„å…¶ä»–ęÆäø€é ę•™ēš„éƒ½ę˜Æē›®å‰ēš„ API怂 diff --git a/i18n/zh-hant/pages/get-started/first-steps.md b/i18n/zh-hant/pages/get-started/first-steps.md index af48ee7a..49a3fb44 100644 --- a/i18n/zh-hant/pages/get-started/first-steps.md +++ b/i18n/zh-hant/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 第一歄 {#first-steps} @@ -17,7 +17,7 @@ translation: * **ē”Øęˆ¶ē«Æ**ä½ę–¼äø»ę©Ÿå…§éƒØļ¼Œč² č²¬č¬› MCPć€‚äø»ę©ŸęÆé€£äøŠäø€å€‹ä¼ŗęœå™Øļ¼Œå°±åŸ·č”Œäø€å€‹ē”Øęˆ¶ē«Æć€‚ * **ä¼ŗęœå™Ø**ę˜Æä½ ē”Øé€™å€‹ SDK ę‰“é€ ēš„ę±č„æć€‚å®ƒęŠŠę±č„æå…¬é–‹ēµ¦ē”Øęˆ¶ē«Æļ¼Œå¾žäøē›“ęŽ„å’ŒęØ”åž‹ęŗé€šć€‚ -ä¼ŗęœå™Øē”±ä½ ä¾†åÆ«ļ¼Œäø»ę©Ÿę˜Æåˆ„äŗŗēš„ē”¢å“ć€‚SDK ä¹Ÿęä¾›äø€å€‹ `Client`ļ¼Œē”Øä¾†ęø¬č©¦ä½ ēš„ä¼ŗęœå™Øļ¼Œé€™äø€é ēØå¾Œå°±ęœƒå‡ŗē¾ć€‚ +ä¼ŗęœå™Øē”±ä½ ä¾†åÆ«ļ¼Œäø»ę©Ÿę˜Æåˆ„äŗŗēš„ē”¢å“ć€‚SDK ä¹Ÿęä¾›äø€å€‹ `Client`ļ¼Œäø»ę©Ÿč¦é€éŽ URL é€£äøŠä¼ŗęœå™Øć€ęˆ–ęŠŠå®ƒē•¶ęˆå­č™•ē†ēØ‹åŗå•Ÿå‹•ļ¼Œē”Øēš„å°±ę˜ÆåŒäø€å€‹é”žåˆ„ć€‚å®ƒåœØé€™äø€é ēØå¾Œå°±ęœƒå‡ŗē¾ļ¼Œä¹Ÿę˜Æä½ ęø¬č©¦ä¼ŗęœå™Øēš„ę–¹å¼ć€‚ ## äø‰ēØ®åŸŗęœ¬å…ƒä»¶ {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector 透過 **stdio** åŸ·č”Œä½ ēš„ä¼ŗęœå™Øļ¼Œé€™ę˜Æ MCP ä¼ŗęœå™Øčƒ½ä½æē”Ø ē”Øęˆ¶ē«Æé€£ē·šę™‚ļ¼Œä¼ŗęœå™Øęœƒå®£å‘Šč‡Ŗå·±ēš„**čƒ½åŠ›**ļ¼šå®ƒęœƒå›žę‡‰å“Ŗå¹¾é”žč«‹ę±‚ć€‚ē”Øęˆ¶ē«Æę“šę­¤ę±ŗå®šē©¶ē«Ÿč©²č¦ę±‚ä»€éŗ¼ć€‚é€™ä»½å®£å‘Šäøę˜Æä½ åÆ«ēš„ļ¼Œ`MCPServer` ę›æä½ å®£å‘Šå„½äŗ†ć€‚ -č‡Ŗå·±ēœ‹ēœ‹å§ć€‚SDK ēš„ `Client` ē›“ęŽ„ęŽ„å—ä¼ŗęœå™Øē‰©ä»¶ļ¼Œäø¦åœØ**čØ˜ę†¶é«”å…§**čˆ‡å®ƒé€£ē·šļ¼ˆę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œę²’ęœ‰é€£ęŽ„åŸ ļ¼‰ļ¼š +č‡Ŗå·±ēœ‹ēœ‹å§ć€‚åœØäø€å€‹ēµ‚ē«Æę©Ÿč£”č®“ `server.py` 透過 HTTP 持續執蔌: -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +å†å¾žå¦äø€å€‹ēµ‚ē«Æę©ŸęŠŠē”Øęˆ¶ē«ÆęŒ‡å‘å®ƒļ¼š -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) ę³Øę„å°‘äŗ†ä»€éŗ¼ć€‚`completions`ļ¼ˆč³‡ęŗēÆ„ęœ¬å’Œęē¤ŗč©žēš„å¼•ę•øč‡Ŗå‹•å®Œęˆļ¼‰éœ€č¦ä½ åÆ«äø€å€‹č™•ē†å‡½å¼ļ¼Œé€™å€‹ä¼ŗęœå™Øę²’ęœ‰ļ¼Œę‰€ä»„é€™é …čƒ½åŠ›äøå­˜åœØļ¼Œå®ˆč¦ēŸ©ēš„ē”Øęˆ¶ē«Æä¹ŸäøęœƒåŽ»å•ć€‚ę‰€ęœ‰éøē”Øēš„ę±č„æéƒ½ē…§é€™ę¢č¦å‰‡ļ¼ščØ»å†Šäŗ†ļ¼Œčƒ½åŠ›å°±å‡ŗē¾ļ¼›**[č‡Ŗå‹•å®Œęˆ](../servers/completions.md)** ęœƒč­‰ę˜Žé€™äø€é»žć€‚ !!! info - `Client(mcp)` å°±ę˜Æé€™ä»½ę–‡ä»¶č£”ęÆå€‹ēÆ„ä¾‹ęø¬č©¦ę™‚ē”Øēš„åŒäø€å€‹čØ˜ę†¶é«”å…§ē”Øęˆ¶ē«Æļ¼Œä½ ä¹Ÿęœƒē”Øå®ƒä¾†ęø¬č©¦č‡Ŗå·±ēš„ć€‚å®ƒęœ‰å°ˆå±¬ēš„äø€ę•“é ļ¼š**[測試](testing.md)**怂 + 那個 `client.py` ę˜Æäø€å€‹å®Œę•“ēš„ MCP ē”Øęˆ¶ē«Æļ¼Œå®ƒēš„å°ˆå±¬é é¢ę˜Æ **[ē”Øęˆ¶ē«Æ](../client/index.md)**ć€‚ęø¬č©¦ę™‚åÆä»„č·³éŽēµ‚ē«Æę©Ÿå’Œé€£ęŽ„åŸ ļ¼ŒęŠŠä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«ē›“ęŽ„äŗ¤ēµ¦ `Client`,也就是 `Client(mcp)`ć€‚é‚£ä¹Ÿęœ‰å°ˆå±¬ēš„äø€ę•“é ļ¼š**[測試](testing.md)**怂 ## ä½ ę²’åÆ«ēš„ę±č„æ {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema怂`a: int, b: int` **就是** `add` ēš„ schema怂 * č«‹ę±‚č™•ē†å‡½å¼ć€‚`tools/list`态`resources/read`态`prompts/get`ļ¼šå…Øéƒ½ę›æä½ č™•ē†å„½äŗ†ć€‚ * čƒ½åŠ›å®£å‘Šć€‚`MCPServer` ę›æä½ åšäŗ†ć€‚ -* ä»»ä½•äø€č”Œå”å®šć€‚ē‰ˆęœ¬å”å•†ć€JSON-RPC čØŠę”†ć€čƒ½åŠ›äŗ¤ę›ļ¼šå…Øéƒ½ē™¼ē”ŸåœØ `mcp dev` 和 `Client(mcp)` č£”é¢ļ¼Œä½ å®Œå…Øę²’ēœ‹åˆ°ć€‚ +* ä»»ä½•äø€č”Œå”å®šć€‚ē‰ˆęœ¬å”å•†ć€JSON-RPC čØŠę”†ć€čƒ½åŠ›äŗ¤ę›ļ¼šå…Øéƒ½ē™¼ē”ŸåœØ `mcp dev` 和 `client.py` č£”é¢ļ¼Œä½ å®Œå…Øę²’ēœ‹åˆ°ć€‚ é€™å€‹ęÆ”ä¾‹ę­£ę˜Æ SDK ēš„ę„ē¾©ę‰€åœØć€‚ @@ -134,6 +132,6 @@ asyncio.run(main()) * ęÆēØ®åŸŗęœ¬å…ƒä»¶äø€å€‹č£é£¾å™Øļ¼š`@mcp.tool()`态`@mcp.resource(uri)`态`@mcp.prompt()`ć€‚åēØ±ć€ęčæ°å’Œ schema éƒ½ä¾†č‡Ŗå‡½å¼ć€‚ * åø¶ `{param}` ēš„ URI ęœƒē”¢ē”Ÿč³‡ęŗ**ēÆ„ęœ¬**ļ¼Œå’Œå…·é«”č³‡ęŗåˆ†é–‹åˆ—å‡ŗć€‚ * ä¼ŗęœå™Øēš„**čƒ½åŠ›**ęœƒę›æä½ å®£å‘Šå„½ļ¼Œč€Œē”Øęˆ¶ē«ÆåŖęœƒč¦ę±‚ä¼ŗęœå™Øå®£å‘ŠéŽēš„ę±č„æć€‚ -* `Client(mcp)` åœØčØ˜ę†¶é«”å…§é€£äøŠä¼ŗęœå™Øē‰©ä»¶ļ¼šå¾žē¬¬äø€å¤©čµ·å°±ę˜Æä½ ēš„ęø¬č©¦å·„å…·ć€‚ +* `Client("http://localhost:8000/mcp")` ęœƒå’ŒåŸ·č”Œäø­ēš„ä¼ŗęœå™Øå°č©±ć€‚ę”¹ęˆęŠŠä¼ŗęœå™Øē‰©ä»¶äŗ¤ēµ¦å®ƒļ¼Œä¹Ÿå°±ę˜Æ `Client(mcp)`ļ¼Œå¾žē¬¬äø€å¤©čµ·å®ƒå°±ę˜Æä½ ēš„ęø¬č©¦å·„å…·ć€‚ ęŽ„äø‹ä¾†ę˜Æ **[é€£ęŽ„ēœŸę­£ēš„äø»ę©Ÿ](real-host.md)**ļ¼šęŠŠé€™å€‹ä¼ŗęœå™ØēœŸēš„ę”¾é€² Claude Desktop ꈖ IDE č£”ć€‚ē„¶å¾Œę˜Æ **[測試](testing.md)**ļ¼šäø€é ć€äø€å€‹čØ˜ę†¶é«”å…§ē”Øęˆ¶ē«Æļ¼Œå¾žę­¤äøē”ØēŒœå®ƒåˆ°åŗ•čƒ½äøčƒ½å‹•ć€‚å†ä¹‹å¾Œļ¼ŒęÆēØ®åŸŗęœ¬å…ƒä»¶å„ęœ‰č‡Ŗå·±ēš„äø€é ļ¼Œå¾žęØ”åž‹äø»å°Žēš„é‚£å€‹é–‹å§‹ļ¼š**[å·„å…·](../servers/tools.md)**怂 diff --git a/i18n/zh-hant/pages/get-started/testing.md b/i18n/zh-hant/pages/get-started/testing.md index c37bac9d..f8a5d60b 100644 --- a/i18n/zh-hant/pages/get-started/testing.md +++ b/i18n/zh-hant/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # 測試 {#testing} -Python SDK 附帶一個 `Client` é”žåˆ„ļ¼Œå…§å»ŗ**čØ˜ę†¶é«”å…§å‚³č¼ø**ļ¼šęŠŠä¼ŗęœå™Øē‰©ä»¶å‚³ēµ¦å®ƒļ¼Œå®ƒå°±ęœƒē›“ęŽ„é€£äøŠåŽ»ć€‚ +SDK ēš„ `Client` é”žåˆ„ļ¼Œä¹Ÿå°±ę˜Æé€£åˆ° URL ęˆ–å•Ÿå‹•å­č™•ē†ēØ‹åŗēš„åŒäø€å€‹é”žåˆ„ļ¼Œä¹Ÿčƒ½åœØ**čØ˜ę†¶é«”å…§**é€£ē·šļ¼šęŠŠä¼ŗęœå™Øē‰©ä»¶å‚³ēµ¦å®ƒļ¼Œå®ƒå°±ęœƒē›“ęŽ„å’Œä¼ŗęœå™Øå°č©±ć€‚ -ę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œę²’ęœ‰é€£ęŽ„åŸ ļ¼Œę ¹ęœ¬ę²’ęœ‰å‚³č¼øå±¤ć€‚ę¦‚åæµå’Œ FastAPI ēš„ `TestClient` 一樣。 +ę²’ęœ‰å­č™•ē†ēØ‹åŗļ¼Œę²’ęœ‰é€£ęŽ„åŸ ļ¼Œē·šč·ÆäøŠä»€éŗ¼éƒ½ę²’ęœ‰ć€‚ę¦‚åæµå’Œ FastAPI ēš„ `TestClient` 一樣。 ## åŸŗęœ¬ē”Øę³• {#basic-usage} @@ -86,7 +86,7 @@ async def test_call_add_tool(client: Client): ęø¬č©¦č£”å°±é–‹č‘—å®ƒć€‚åœØę­£å¼ē’°å¢ƒēš„ēØ‹å¼ē¢¼č£”å®ƒę²’ęœ‰ä»»ä½•ę„ē¾©ć€‚ -## é čØ­ē‚ŗč™•ē†ēØ‹åŗå…§é€£ē·š {#in-process-by-default} +## 預設為世代中立 {#era-neutral-by-default} !!! note `Client(mcp)` ä»„č™•ē†ēØ‹åŗå…§ēš„ę–¹å¼é€£ē·šļ¼Œč€Œäø”é čØ­ę˜Æ**世代中立**ēš„ļ¼šå®ƒęœƒęŽ¢ęø¬ä¼ŗęœå™Øļ¼Œéøå‡ŗåˆé©ēš„å”å®šč·Æå¾‘ć€‚å¦‚ęžœęø¬č©¦č¦é©—č­‰čˆŠē‰ˆē‰¹ęœ‰ēš„čŖžę„ļ¼ˆå–ęØ£ļ¼ˆsamplingļ¼‰ęˆ–å¾µč©¢ļ¼ˆelicitationļ¼‰ēš„ęŽØé€ć€`message_handler`ļ¼‰ļ¼Œå°±å›ŗå®šē”Ø `mode="legacy"`ļ¼Œäø¦äø”åœØé‚£č£”ę‹æęŽ‰ `raise_exceptions=True`ļ¼ščˆŠē‰ˆé€£ē·šęœ¬ä¾†å°±äøęœƒę·ØåŒ–ļ¼Œč€Œé€™å€‹ę——ęØ™ęœƒč®“å¤±ę•—åœØä¼ŗęœå™Øä»»å‹™č£”é‡ę–°å¼•ē™¼ļ¼Œč€Œäøę˜ÆåœØä½ ēš„ęø¬č©¦č£”ć€‚ diff --git a/i18n/zh-hant/pages/handlers/multi-round-trip.md b/i18n/zh-hant/pages/handlers/multi-round-trip.md index 2d9c5a09..4d97c8cc 100644 --- a/i18n/zh-hant/pages/handlers/multi-round-trip.md +++ b/i18n/zh-hant/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # å¤šč¼Ŗå¾€čæ”č«‹ę±‚ {#multi-round-trip-requests} @@ -162,7 +162,7 @@ TTLć€äø»é«”ē¶å®šå’Œč«‹ę±‚ē¶å®šéƒ½**äøę˜Æ** codec ēš„å·„ä½œļ¼šäøč«–å“Ŗå€‹ co ## 2026-07-28 ēš„ēµęžœåž‹åˆ„ {#a-2026-07-28-result} -`InputRequiredResult` åŖå­˜åœØę–¼å”å®šē‰ˆęœ¬ **2026-07-28**ć€‚čØ˜ę†¶é«”å…§ēš„ `Client(server)` ęœƒę›æä½ å”å•†ļ¼›čµ°ē·šč·Æę™‚ļ¼Œ`mode="auto"` ęœƒęŽ¢ēŸ„å®ƒć€‚é€£ē·šä¹‹å¾Œļ¼Œ`client.protocol_version` ęœƒå‘ŠčØ“ä½ ę‹æåˆ°ēš„ę˜Æä»€éŗ¼ć€‚ +`InputRequiredResult` åŖå­˜åœØę–¼å”å®šē‰ˆęœ¬ **2026-07-28**怂`Client` é čØ­ēš„ `mode="auto"` åœØä»»ä½•é€£ē·šäøŠéƒ½ęœƒęŽ¢ēŸ„å®ƒć€‚é€£ē·šä¹‹å¾Œļ¼Œ`client.protocol_version` ęœƒå‘ŠčØ“ä½ ę‹æåˆ°ēš„ę˜Æä»€éŗ¼ć€‚ !!! warning 2026 ä¹‹å‰ēš„å·„ä½œéšŽę®µę²’ęœ‰åœ°ę–¹ę”¾ `InputRequiredResult`ć€‚åœØ `mode="legacy"` ēš„é€£ē·šäøŠå¾žč™•ē†å‡½å¼å›žå‚³äø€å€‹ļ¼Œrunner ē„”ę³•ęŠŠå®ƒåŗåˆ—åŒ–ęˆå”å•†å„½ēš„ē‰ˆęœ¬ļ¼›ē”Øęˆ¶ē«Æęœƒę‹æå›ž `-32603`怌Handler returned an invalid resultć€éŒÆčŖ¤ć€‚åŒę™‚ęœå‹™å…©å€‹äø–ä»£ēš„ä¼ŗęœå™Øļ¼Œåæ…é ˆå…ˆęŖ¢ęŸ„ `ctx.protocol_version` å†å‹•ē”Øå®ƒć€‚ diff --git a/i18n/zh-hant/pages/handlers/progress.md b/i18n/zh-hant/pages/handlers/progress.md index de2ba688..7f1d96d5 100644 --- a/i18n/zh-hant/pages/handlers/progress.md +++ b/i18n/zh-hant/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 進度 {#progress} @@ -29,19 +29,17 @@ translation: ē”Øęˆ¶ē«Æę˜Æ**é€ę¬”å‘¼å«**éøę“‡åŠ å…„ēš„ļ¼Œåšę³•ę˜ÆęŠŠ `progress_callback=` 傳給 `call_tool`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,23 +54,27 @@ anyio.run(main) å›žå‘¼ę˜Æäø€å€‹ `async` å‡½å¼ļ¼ŒęŽ„ę”¶ēš„ę­£ę˜Æä¼ŗęœå™Øå›žå ±ēš„å…§å®¹ļ¼š`progress`态`total`态`message`怂 !!! info - `Client(mcp)` åœØčØ˜ę†¶é«”å…§ē›“ęŽ„é€£äøŠä¼ŗęœå™Øē‰©ä»¶ļ¼Œå’Œ **[測試](../get-started/testing.md)** é‚£äø€é ē”Øēš„ę˜ÆåŒäø€å€‹ē”Øęˆ¶ē«Æć€‚äøē®” `Client` ē”Øå“ŖēØ®å‚³č¼øę–¹å¼ļ¼Œ`progress_callback` éƒ½ę˜ÆåŒäø€å€‹åƒę•øļ¼›ęŽ„äø‹ä¾†ēœ‹åˆ°ēš„**ę™‚åŗ**å‰‡ę˜ÆčØ˜ę†¶é«”å…§é€£ē·šēš„ć€‚å®ƒęœƒå°±åœ°åŸ·č”Œå›žå‘¼ļ¼Œę‰€ä»„ęÆäø€ē­†å›žå ±éƒ½ęœƒåœØ `call_tool` å›žå‚³ä¹‹å‰é€é”ć€‚ę›ęˆēœŸę­£ēš„å‚³č¼øę–¹å¼ļ¼Œé€šēŸ„ęœƒå’Œēµęžœē«¶é€Ÿļ¼Œäø€å€‹ę…¢ēš„å›žå‘¼åœØ `call_tool` å›žå‚³ä¹‹å¾ŒåÆčƒ½é‚„åœØåŸ·č”Œć€‚ + äøē®”äŗ¤ēµ¦ `Client` ēš„ę˜Æä»€éŗ¼ļ¼Œ`progress_callback` éƒ½ę˜ÆåŒäø€å€‹åƒę•øļ¼šåƒé€™č£”ēš„ URL、一個 `StdioServerParameters`ļ¼Œęˆ–ęø¬č©¦č£”ēš„ä¼ŗęœå™Øē‰©ä»¶ć€‚äøéŽčµ°ēœŸę­£ēš„å‚³č¼øę–¹å¼ę™‚č¦ē•™ę„ę™‚åŗć€‚ęÆå€‹é€šēŸ„éƒ½ę˜ÆåœØå›žę‡‰ä¹‹å¤–å–®ēØé€é”ēš„ļ¼Œę‰€ä»„äø€å€‹ę…¢ēš„å›žå‘¼åœØ `call_tool` å›žå‚³ä¹‹å¾ŒåÆčƒ½é‚„åœØåŸ·č”Œć€‚åŖęœ‰č™•ē†ēØ‹åŗå…§ēš„ęø¬č©¦é€£ē·šęœƒå°±åœ°åŸ·č”Œå›žå‘¼ļ¼Œäø¦äæč­‰ęÆäø€ē­†å›žå ±éƒ½å…ˆé€é”ć€‚ ### č©¦č©¦ēœ‹ {#try-it} -把 `client.py` ę”¾åœØ `server.py` ę—é‚Šļ¼Œē„¶å¾ŒåŸ·č”Œļ¼š +用 HTTP ęä¾› `server.py`ļ¼Œē„¶å¾ŒåœØē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œē”Øęˆ¶ē«Æļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -ä¼ŗęœå™ØäøŠēš„ęÆäø€å€‹ `await ctx.report_progress(...)` éƒ½č®Šęˆē”Øęˆ¶ē«ÆäøŠå° `show` ēš„äø€ę¬”å‘¼å«ļ¼Œä¾åŗē™¼ē”Ÿļ¼Œč€Œäø”å…©č”Œéƒ½åœØ `call_tool` 回傳**之前**å°å‡ŗć€‚é€²åŗ¦äøęœƒę‰“åŒ…é€²ēµęžœč£”ļ¼›å®ƒåœØå·„å…·é‚„åœØåŸ·č”Œę™‚å°±ęŒēŗŒäø²ęµéŽä¾†ć€‚ +ä¼ŗęœå™ØäøŠēš„ęÆäø€å€‹ `await ctx.report_progress(...)` éƒ½č®Šęˆē”Øęˆ¶ē«ÆäøŠå° `show` ēš„äø€ę¬”å‘¼å«ļ¼Œä¾åŗē™¼ē”Ÿć€‚é€²åŗ¦äøęœƒę‰“åŒ…é€²ēµęžœč£”ļ¼›å®ƒåœØå·„å…·é‚„åœØåŸ·č”Œę™‚å°±ęŒēŗŒäø²ęµéŽä¾†ć€‚ !!! warning `progress_callback` 屬於那一欔**呼叫**ļ¼Œäøå±¬ę–¼ `Client`ć€‚ę²’ęœ‰å°ę‡‰ēš„å»ŗę§‹å­å¼•ę•øļ¼Œå› ē‚ŗäøåŒēš„å‘¼å«ęƒ³č¦äøåŒēš„å›žå‘¼ļ¼šé€™äø€ę¬”é©…å‹•äø‹č¼‰é€²åŗ¦ę¢ļ¼Œäø‹äø€ę¬”ę˜Æäø€č”ŒčØ˜éŒ„ć€‚ diff --git a/i18n/zh-hant/pages/protocol-versions.md b/i18n/zh-hant/pages/protocol-versions.md index 459a6b41..993fce45 100644 --- a/i18n/zh-hant/pages/protocol-versions.md +++ b/i18n/zh-hant/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # å”å®šē‰ˆęœ¬ {#protocol-versions} @@ -11,9 +11,17 @@ MCP ęœ‰å…©å€‹äø–ä»£ć€‚ ä½ å¹¾ä¹Žäøéœ€č¦åœØę„é€™ä»¶äŗ‹ļ¼Œå› ē‚ŗ `Client` ęœƒę›æä½ å”å•†ć€‚é€™äø€é č«‡ēš„ę˜ÆęŽ§åˆ¶é€™ä»¶äŗ‹ēš„é‚£äø€å€‹å»ŗę§‹å­å¼•ę•ø `mode=`ļ¼Œä»„åŠéœ€č¦ę”¹å‹•å®ƒēš„äø‰ēØ®ęƒ…ę³ć€‚ +é€™äø€é ēš„ęÆę®µēØ‹å¼ē¢¼éƒ½ę˜Æäø€å€‹ `client.py`ļ¼Œé€£ē·šåˆ° **[ē”Øęˆ¶ē«Æ](client/index.md)** é‚£é ēš„ Bookshop `server.py`ć€‚å…ˆåœØäø€å€‹ēµ‚ē«Æę©Ÿč£”å•Ÿå‹•é‚£å€‹ä¼ŗęœå™Øļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +ęŽ„č‘—åœØē¬¬äŗŒå€‹ēµ‚ē«Æę©Ÿč£”ē”Ø `python client.py` åŸ·č”ŒęÆäø€ę®µēØ‹å¼ē¢¼ć€‚ + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,11 +39,11 @@ MCP ęœ‰å…©å€‹äø–ä»£ć€‚ ę•“å€‹åŠŸčƒ½å°±é€™ęØ£ć€‚äø€å€‹ `Client`ļ¼Œä»»ä½•äø–ä»£ēš„ä¼ŗęœå™Øļ¼ŒēØ‹å¼ē¢¼č£”äøē”Øåˆ†ę”Æć€‚ !!! info - `MCPServer` åœØęÆäø€ēØ®å‚³č¼øę–¹å¼äøŠéƒ½ęœƒå›žē­” `server/discover`ļ¼ˆčØ˜ę†¶é«”å…§ć€stdio态Streamable HTTPļ¼‰ļ¼Œę‰€ä»„å°ä½ č‡Ŗå·±ēš„ä¼ŗęœå™Øļ¼Œ`auto` 永遠會落在 `2026-07-28`ć€‚é€€å›žę©Ÿåˆ¶åŖęœƒåœØé¢å°ēœŸę­£ēš„ 2026 ä¹‹å‰ēš„ä¼ŗęœå™Øę™‚č§øē™¼ļ¼Œč€Œé‚£ę­£ę˜Æä½ åøŒęœ›å®ƒč§øē™¼ēš„ę™‚å€™ć€‚ + `MCPServer` åœØęÆäø€ēØ®å‚³č¼øę–¹å¼äøŠéƒ½ęœƒå›žē­” `server/discover`(Streamable HTTP态stdioļ¼Œä»„åŠęø¬č©¦ę™‚ē”Øēš„č™•ē†ēØ‹åŗå…§é€£ē·šļ¼‰ļ¼Œę‰€ä»„å°ä½ č‡Ŗå·±ēš„ä¼ŗęœå™Øļ¼Œ`auto` 永遠會落在 `2026-07-28`ć€‚é€€å›žę©Ÿåˆ¶åŖęœƒåœØé¢å°ēœŸę­£ēš„ 2026 ä¹‹å‰ēš„ä¼ŗęœå™Øę™‚č§øē™¼ļ¼Œč€Œé‚£ę­£ę˜Æä½ åøŒęœ›å®ƒč§øē™¼ēš„ę™‚å€™ć€‚ ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP ęœ‰å…©å€‹äø–ä»£ć€‚ `mode` ä¹ŸęŽ„å—ę–°äø–ä»£ēš„å”å®šē‰ˆęœ¬å­—äø²ć€‚ē›®å‰é€™å€‹é›†åˆå‰›å„½å°±ę˜Æ `["2026-07-28"]`怂 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 ę‰€ä»„ęŠŠå®ƒē•™äø‹ä¾†ć€‚`auto` é€£ē·šä¹‹å¾Œļ¼Œ`client.session.discover_result` äæå­˜č‘—ä¼ŗęœå™Øé€ä¾†ēš„é‚£ä»½ `DiscoverResult`ļ¼šå®ƒēš„ `supported_versions`态`capabilities`态`instructions`ļ¼Œä»„åŠä¼ŗęœå™Øč“‹é€²ēµęžœ `_meta` č£”ēš„čŗ«åˆ†ć€‚äø‹ę¬”ęŠŠå®ƒä½œē‚ŗ `prior_discover=` äŗ¤å›žåŽ»ļ¼š -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/zh-hant/pages/run/asgi.md b/i18n/zh-hant/pages/run/asgi.md index 32c34aeb..dc7eb217 100644 --- a/i18n/zh-hant/pages/run/asgi.md +++ b/i18n/zh-hant/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # åŠ åˆ°ē¾ęœ‰ēš„ę‡‰ē”ØēØ‹å¼äø­ {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette ēš„ `Host` č·Æē”±ē”Øę³•ē›øåŒļ¼šęŠŠ `Mount("/", ...)` ꏛꈐ `Host("mc --8<-- "docs_src/asgi/tutorial004.py" ``` -ē¾åœØē”Øęˆ¶ē«Æé€£åˆ° `/notes`ļ¼Œč€Œäøę˜Æ `/notes/mcp`怂 +ē¾åœØē”Øęˆ¶ē«Æé€£åˆ° `/notes/`ļ¼Œč€Œäøę˜Æ `/notes/mcp`怂 ## ēµ¦ē€č¦½å™Øē”Øęˆ¶ē«Æēš„ CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette ēš„ `Host` č·Æē”±ē”Øę³•ē›øåŒļ¼šęŠŠ `Mount("/", ...)` ꏛꈐ `Host("mc * ē€č¦½å™Øē”Øęˆ¶ē«Æéœ€č¦ CORS:`allow_headers` 給 `Mcp-*` č«‹ę±‚ęØ™é ­ē”Øļ¼Œ`expose_headers=["Mcp-Session-Id"]` ēµ¦å›žę‡‰ē”Øć€‚ * `@mcp.custom_route()` 在 `/mcp` ę—é‚ŠåŠ äøŠę™®é€šć€äøē¶“é©—č­‰ēš„ HTTP ē«Æé»žć€‚ -äø€ę—¦ä¼ŗęœå™Øčƒ½é€éŽēœŸę­£ēš„ URL é€£åˆ°ļ¼Œ**[ē”Øęˆ¶ē«Æ](../client/index.md)** å°±ęœƒē”Øé‚£å€‹ URL č€Œäøę˜Æä¼ŗęœå™Øē‰©ä»¶ä¾†é€£ē·šć€‚ +äø€ę—¦ä¼ŗęœå™Øčƒ½é€éŽēœŸę­£ēš„ URL é€£åˆ°ļ¼Œ**[ē”Øęˆ¶ē«Æ](../client/index.md)** å°±ęœƒē”Øé‚£å€‹ URL ä¾†é€£ē·šć€‚ diff --git a/i18n/zh-hant/pages/run/authorization.md b/i18n/zh-hant/pages/run/authorization.md index 1618cddc..52b0ce73 100644 --- a/i18n/zh-hant/pages/run/authorization.md +++ b/i18n/zh-hant/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # ęŽˆę¬Š {#authorization} @@ -23,12 +23,12 @@ translation: ęœ‰ę•ˆēš„ę¬Šę–é•·ä»€éŗ¼ęØ£å­ļ¼ŒSDK ę²’ęœ‰ä»»ä½•é čØ­ē«‹å “ć€‚ē”±ä½ ä¾†å‘ŠčØ“å®ƒļ¼Œę–¹ę³•ę˜ÆåÆ¦ä½œ **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` ę˜ÆåŖęœ‰äø€å€‹éžåŒę­„ę–¹ę³•ēš„ protocol怂`verify_token` ęœƒę‹æåˆ° `Authorization` ęØ™é ­č£”ēš„åŽŸå§‹ę¬Šę–ļ¼Œęœ‰ę•ˆå°±å›žå‚³ **`AccessToken`**ļ¼Œē„”ę•ˆå°±å›žå‚³ `None`ć€‚ę²’ęœ‰åˆ„ēš„éœ€č¦åÆ¦ä½œć€‚ -* é€™å€‹ēÆ„ä¾‹ę˜ÆåœØäø€å¼µč”Øč£”ęŸ„ę¬Šę–ć€‚ēœŸåÆ¦ēš„åÆ¦ä½œęœƒé©—č­‰ JWT ē°½ē« ļ¼Œęˆ–å‘¼å«ęŽˆę¬Šä¼ŗęœå™Øēš„ę¬Šę–å…§ēœļ¼ˆtoken introspectionļ¼‰ē«Æé»žć€‚é‚£ę®µēØ‹å¼ē¢¼ę˜Æä½ ēš„ļ¼ŒSDK åŖč² č²¬å‘¼å«å®ƒć€‚ +* é€™å€‹ēÆ„ä¾‹ę˜ÆåœØäø€å¼µč”Øč£”ęŸ„ę¬Šę–ļ¼›ęÆäø€ē­†éƒ½čØ˜éŒ„äŗ†ę¬Šę–ę˜Æē‚ŗå“Ŗå€‹č³‡ęŗē™¼å‡ŗēš„ć€‚ēœŸåÆ¦ēš„åÆ¦ä½œęœƒé©—č­‰ JWT ē°½ē« ļ¼Œęˆ–å‘¼å«ęŽˆę¬Šä¼ŗęœå™Øēš„ę¬Šę–å…§ēœļ¼ˆtoken introspectionļ¼‰ē«Æé»žļ¼Œäø¦åœØ `AccessToken.resource` č£”å›žå ±ę¬Šę–ę˜Æē™¼ēµ¦čŖ°ēš„ļ¼ˆå®ƒēš„ `aud`ļ¼‰ć€‚é‚£ę®µēØ‹å¼ē¢¼ę˜Æä½ ēš„ļ¼ŒSDK åŖč² č²¬å‘¼å«å®ƒć€‚ * `token_verifier=` 和 `auth=` ę°øé ęˆå°å‡ŗē¾ć€‚åŖå‚³å…¶äø­äø€å€‹ļ¼Œ`MCPServer(...)` åœØęœå‹™ä»»ä½•č«‹ę±‚ä¹‹å‰å°±ęœƒå¼•ē™¼ `ValueError`怂 `AuthSettings` ę˜Æč³‡ęŗä¼ŗęœå™Øå°å¤–ēš„é–€é¢ļ¼š @@ -36,6 +36,10 @@ translation: * `issuer_url`ļ¼šē™¼å‡ŗę¬Šę–ēš„ęŽˆę¬Šä¼ŗęœå™Øć€‚ * `resource_server_url`ļ¼šé€™å€‹ MCP ē«Æé»žēš„å…¬é–‹ URLć€‚å®ƒęŒ‡ę˜Žę¬Šę–ę˜Æēµ¦**哪一個**č³‡ęŗē”Øēš„ļ¼Œä¹Ÿę˜ÆęŽ¢ē“¢ę–‡ä»¶ę‰€åœØēš„ä½ē½®ć€‚ * `required_scopes`ļ¼šęÆå€‹ę¬Šę–éƒ½åæ…é ˆåø¶ęœ‰å…ØéƒØé€™äŗ› scope怂 +* `validate_token_resource`ļ¼šę‹’ēµ•ä»»ä½• `AccessToken.resource` äøē­‰ę–¼ `resource_server_url` ēš„ę¬Šę–ć€‚čØ­å®šäŗ† `resource_server_url` å»ę²’čØ­å®šå®ƒļ¼Œęœƒē™¼å‡ŗč­¦å‘Šļ¼ˆ`MCPDeprecationWarning`ļ¼‰äø¦č¦–åŒ `False`ļ¼›3.0 čµ·ļ¼Œč³‡ęŗä¼ŗęœå™Øēš„é čØ­å€¼ęœƒę”¹ē‚ŗ `True`怂 + * å¦‚ęžœä½ ēš„ęŽˆę¬Šä¼ŗęœå™ØęœƒęŠŠę¬Šę–ē¶å®šåˆ°ē”Øęˆ¶ē«Æč«‹ę±‚ēš„ `resource`(MCP ē”Øęˆ¶ē«Æäø€å®šęœƒé€ļ¼‰ļ¼Œå°±ęŠŠå®ƒę‰“é–‹ć€‚č®“ `resource_server_url` čˆ‡ē”Øęˆ¶ē«ÆåÆ¦éš›é€£ē·šēš„ URL å®Œå…Øäø€č‡“ć€‚ + * å¦‚ęžœä½ ēš„ęŽˆę¬Šä¼ŗęœå™Øä½æē”Øč‡Ŗå·±ēš„ audience 識刄碼(Auth0 ēš„ API identifier态Entra ēš„ę‡‰ē”ØēØ‹å¼ IDļ¼‰ļ¼Œå°±äæęŒé—œé–‰ļ¼Œę”¹åœØé©—č­‰å™Øč£”ęŖ¢ęŸ„ `aud`ļ¼Œé‡åˆ°äøę˜Æēµ¦é€™å€‹ä¼ŗęœå™Øēš„ę¬Šę–å°±å›žå‚³ `None`怂 + * å¦‚ęžœ `aud` ę˜Æäø€å€‹ęø…å–®ļ¼ŒęŠŠē­‰ę–¼ `resource_server_url` ēš„é‚£äø€é …ę”¾é€² `resource`怂 !!! tip SDK å„²å­˜åŗ«č£”ēš„ `examples/servers/simple-auth/` ęœ‰äø€å€‹ `IntrospectionTokenVerifier`ļ¼Œęœƒå‘¼å«ēœŸåÆ¦ęŽˆę¬Šä¼ŗęœå™Øēš„ [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) ē«Æé»žć€‚å¤§å¤šę•øę­£å¼ē’°å¢ƒēš„é©—č­‰å™Øéƒ½ę˜Æé€™å€‹ęØ£å­ć€‚ @@ -85,7 +89,7 @@ translation: åœØä»»ä½•č™•ē†å‡½å¼å…§ļ¼Œ**`get_access_token()`** å°±ę˜Æé©—č­‰å™Øē‚ŗē›®å‰č«‹ę±‚å›žå‚³ēš„é‚£å€‹ `AccessToken`: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK ēµ¦ä½ ēš„ę˜Æč³‡ęŗä¼ŗęœå™Øé€™äø€åŠļ¼šé©—č­‰ć€å…¬å‘Šć€ę‹’ēµ•ć€‚å®ƒäøę * `token_verifier=` 和 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` ę°øé ęˆå°å‡ŗē¾ć€‚ * SDK 會在 `/.well-known/oauth-protected-resource/...` ē™¼åøƒ [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadata,並仄 401 å›žę‡‰ęœŖé©—č­‰ēš„č«‹ę±‚ļ¼Œå…¶ `WWW-Authenticate` ęØ™é ­ęœƒęŒ‡å‘é€™ä»½ę–‡ä»¶ć€‚ę•“å€‹ęŽ¢ē“¢ę©Ÿåˆ¶å°±é€™ęØ£ć€‚ * åœØä»»ä½•č™•ē†å‡½å¼č£”ļ¼Œ`get_access_token()` å°±ę˜ÆčŖ°åœØå‘¼å«ć€‚ -* ęŽˆę¬Šę˜Æ HTTP å±¤ēš„äŗ‹ć€‚`stdio` å’ŒčØ˜ę†¶é«”å…§ē”Øęˆ¶ē«Æę°øé ēœ‹äøåˆ°å®ƒć€‚ +* ęŽˆę¬Šę˜Æ HTTP å±¤ēš„äŗ‹ć€‚`stdio` å’ŒčØ˜ę†¶é«”å…§ęø¬č©¦ē”Øęˆ¶ē«Æę°øé ēœ‹äøåˆ°å®ƒć€‚ ē”Øęˆ¶ē«Æé‚£äø€åŠļ¼ˆęŽ¢ē“¢ä½ ēš„ęŽˆę¬Šä¼ŗęœå™Øäø¦ę›æä½ å–å¾—ę¬Šę–ļ¼‰č«‹č¦‹ **[OAuth ē”Øęˆ¶ē«Æ](../client/oauth-clients.md)**ć€‚č‡³ę–¼äøå•ä½æē”Øč€…ć€č€Œę˜Æē›“ęŽ„**斷言**čŗ«åˆ†ēš„ē”Øęˆ¶ē«Æļ¼Œč«‹č¦‹ **[čŗ«åˆ†ę–·čØ€](../client/identity-assertion.md)**怂 diff --git a/i18n/zh-hant/pages/run/deploy.md b/i18n/zh-hant/pages/run/deploy.md index 01f1ed3a..c500dcaa 100644 --- a/i18n/zh-hant/pages/run/deploy.md +++ b/i18n/zh-hant/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # éƒØē½²čˆ‡ę““å±• {#deploy-scale} @@ -42,6 +42,22 @@ translation: åœØē”Øęˆ¶ē«Æę‰¾äøåˆ°é€™å¹¾å€‹å­—ć€‚`421` ę˜Æē“”ę–‡å­—ēš„ HTTP å›žę‡‰ļ¼Œäøę˜Æ JSON-RPC éŒÆčŖ¤ļ¼Œę‰€ä»„ MCP ē”Øęˆ¶ē«ÆåŖęœƒå¼•ē™¼äø€å€‹ē± ēµ±ēš„å‚³č¼øéŒÆčŖ¤ļ¼›å®ƒäøå–œę­”ēš„äø»ę©ŸåēØ±åŖęœƒå‡ŗē¾åœØ**ä¼ŗęœå™Ø**ēš„čØ˜éŒ„č£”ļ¼Œå°±äø€å‰‡č­¦å‘Šć€‚å‰›éƒØē½²å„½å»ę‹’ēµ•ę‰€ęœ‰é€£ē·šēš„ä¼ŗęœå™Øļ¼ŒåœØč­‰ę˜Žę˜Æåˆ„ēš„åŽŸå› ä¹‹å‰ļ¼Œå°±ę˜Æ Host å…čØ±ęø…å–®ēš„å•é”Œć€‚**[ē–‘é›£ęŽ’č§£](../troubleshooting.md)** ä¹Ÿå¾žé€™č£”é–‹å§‹ć€‚ +## åœØēµ‚ę­¢ TLS ēš„ä»£ē†å¾Œé¢ {#behind-a-tls-terminating-proxy} + +å¦‚ęžœ TLS åœØä»£ē†ļ¼ˆingress、負載平蔔器、Caddy态nginxļ¼‰å°±ēµęŸļ¼Œuvicorn åœØå®ƒå¾Œé¢ęä¾›ē“” HTTPļ¼Œå°±å‘ŠčØ“ uvicorn äæ”ä»»ä»£ē†ēš„ `X-Forwarded-*` ęØ™é ­ļ¼š + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +äøé€™éŗ¼åšēš„č©±ļ¼Œę‡‰ē”ØēØ‹å¼ęœƒä»„ē‚ŗč‡Ŗå·±ę˜Æé€éŽ `http://` ęä¾›ęœå‹™ļ¼Œå®ƒē™¼å‡ŗēš„ä»»ä½•é‡ę–°å°Žå‘ļ¼ˆåøøč¦‹ēš„ę˜Æ `/mcp` → `/mcp/`ļ¼‰éƒ½ęœƒęŒ‡å‘ `http://…`怂Python ē”Øęˆ¶ē«Æę‹’ēµ•å¾ž HTTPS ē«Æé»žč·Ÿåˆ°ē“” HTTPļ¼Œč€Œäø”ęœƒę˜ŽčŖŖļ¼š + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +ē”Øęˆ¶ē«Æēš„ę¬Šå®œä¹‹čØˆę˜ÆčØ­å®šä¼ŗęœå™ØåÆ¦éš›ęä¾›ęœå‹™ēš„ē²¾ē¢ŗ URL(`https://mcp.example.com/mcp/`ļ¼Œå«ēµå°¾ę–œē·šļ¼‰ļ¼Œč®“é‡ę–°å°Žå‘ę ¹ęœ¬äøē™¼ē”Ÿć€‚ę­£č§£ę˜ÆäøŠé¢é‚£å€‹ę——ęØ™ć€‚`FORWARDED_ALLOW_IPS` ę˜Æē’°å¢ƒč®Šę•øēš„åÆ«ę³•ļ¼›`*` ęœƒäæ”ä»»ęÆäø€č·³ļ¼ŒåŖęœ‰åœØé™¤äŗ†ä»£ē†ä¹‹å¤–ę²’ęœ‰ę±č„æē¢°å¾—åˆ° uvicorn ę™‚ę‰ę­£ē¢ŗć€‚ + ## Workerļ¼Œä»„åŠčŖ°éœ€č¦é»ę€§ {#workers-and-who-has-to-be-sticky} äø»ę©ŸåēØ±čƒ½å›žę‡‰ä¹‹å¾Œļ¼Œå°±åœØå¾Œé¢ę”¾äøåŖäø€å€‹ worker怂SDK ę²’ęœ‰é€™ę–¹é¢ēš„čØ­å®šļ¼›ę““å±• Starlette ę‡‰ē”ØēØ‹å¼ēš„ę–¹å¼č·Ÿę““å±•ä»»ä½• ASGI ę‡‰ē”ØēØ‹å¼äø€ęØ£ļ¼ŒęŠŠē‰©ä»¶äŗ¤ēµ¦ę‡‚å¾— fork ēš„ę±č„æļ¼š @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## 重點回锧 {#recap} * é čØ­ęƒ…ę³äø‹ļ¼Œé€™å€‹ę‡‰ē”ØēØ‹å¼åŖå›žę‡‰é€å¾€ localhost ēš„č«‹ę±‚ć€‚`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ę˜ÆäøŠē·šēš„é—œå”ļ¼šåœØå‚³å…„å®ƒä¹‹å‰ļ¼ŒēœŸę­£äø»ę©ŸåēØ±å¾Œé¢ēš„ęÆå€‹č«‹ę±‚éƒ½ę˜Æ `421`ļ¼ŒåŽŸå› åŖåœØä¼ŗęœå™ØčØ˜éŒ„č£”ć€‚ +* åœØēµ‚ę­¢ TLS ēš„ä»£ē†å¾Œé¢ļ¼Œē”Ø `--proxy-headers --forwarded-allow-ips=...` 執蔌 uvicornļ¼Œå¦å‰‡å®ƒēš„é‡ę–°å°Žå‘ęœƒęŒ‡å‘ `http://`ļ¼Œē”Øęˆ¶ē«Æęœƒę‹’ēµ•č·ŸéšØć€‚ * 在 2026-07-28 äøŠę²’ęœ‰å·„ä½œéšŽę®µļ¼Œč² č¼‰å¹³č””å™Øä¹Ÿę²’ęœ‰ę±č„æåÆé»ć€‚`stateless_http=True` ę˜ÆåŖēµ¦čˆŠē‰ˆē”Øēš„é–‹é—œļ¼Œå› ē‚ŗē¾ä»£č«‹ę±‚åœØé‚£å€‹ę——ęØ™č¢«č®€åˆ°ä¹‹å‰å°±å·²ē¶“åˆ†ęµäø¦å›žę‡‰äŗ†ć€‚ * é čØ­ēš„ `requestState` é‡‘é‘°ę˜Æ `os.urandom(32)`ļ¼ŒęÆå€‹č™•ē†ēØ‹åŗå„č‡Ŗé‘„é€ ć€‚é€åˆ°äøåŒ worker ēš„å¤šč¼Ŗå¾€čæ”é‡č©¦ęœƒä»„ `-32602`怌Invalid or expired requestStateć€å¤±ę•—ć€‚ * č§£ę³•ę˜Æ `RequestStateSecurity(keys=[...])` **加上**ęÆå€‹åŸ·č”Œå€‹é«”ē›øåŒēš„ä¼ŗęœå™ØåēØ±ć€‚åēØ±ę˜Æę¬Šę–é čØ­ēš„ audience å®£å‘Šć€‚ē›øåŒēš„é‡‘é‘°ļ¼Œē›øåŒēš„åēØ±ć€‚ diff --git a/i18n/zh-hant/pages/run/index.md b/i18n/zh-hant/pages/run/index.md index a0a3960b..2d4afdaf 100644 --- a/i18n/zh-hant/pages/run/index.md +++ b/i18n/zh-hant/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # åŸ·č”Œä¼ŗęœå™Ø {#running-your-server} @@ -71,6 +71,8 @@ Inspector åšēš„äŗ‹å’ŒēœŸę­£ēš„äø»ę©Ÿäø€ęØ”äø€ęØ£ļ¼šęŠŠ `server.py` ē•¶ęˆå­č™• * `json_response=True`ļ¼šęÆå€‹ POST éƒ½ē”Øå–®äø€ JSON ęœ¬é«”å›žę‡‰ļ¼Œč€Œäøę˜Æ SSE äø²ęµć€‚é‚£å€‹ęœ¬é«”åŖč£å¾—äø‹å›žę‡‰ęœ¬čŗ«ļ¼Œåˆ„ēš„éƒ½ę²’ęœ‰ļ¼Œę‰€ä»„åœØč«‹ę±‚äø­é€”å›žé ­å‘¼å«ē”Øęˆ¶ē«Æēš„å·„å…·ļ¼ˆ`ctx.elicit()`ć€å–ęØ£ļ¼ˆsamplingļ¼‰ļ¼‰åœØé€™äø€ę®µęœƒå¼•ē™¼ `NoBackChannelError`ļ¼Œč€Œē¶åœØé€²č”Œäø­å‘¼å«äøŠēš„é€šēŸ„ļ¼ˆ`ctx.report_progress()` ēš„é€²åŗ¦ć€ęÆę¬”å‘¼å«ēš„čØ˜éŒ„čØŠęÆļ¼‰ęœƒč¢«äøŸę£„ļ¼›ēØē«‹ēš„ `GET` äø²ęµä»ęœƒę‰æč¼‰äøē›øé—œēš„é‚£äŗ›ć€‚ * `stateless_http=True`ļ¼šęÆå€‹č«‹ę±‚äø€å€‹å…Øę–°ēš„å‚³č¼øļ¼Œäøčæ½č¹¤å·„ä½œéšŽę®µļ¼ˆsession)。 * `max_request_body_size`ļ¼šåÆęŽ„å—ēš„ęœ€å¤§č«‹ę±‚ęœ¬é«”ļ¼Œä»„ä½å…ƒēµ„čØˆć€‚é čØ­ē‚ŗ 4 MiBļ¼›ę›“å¤§ēš„č«‹ę±‚åœØč§£ęžęˆ–å»ŗē«‹å·„ä½œéšŽę®µä¹‹å‰å°±ęœƒę”¶åˆ° HTTP 413ć€‚åŖęœ‰åœØåˆę³•ēš„ MCP čØŠęÆč¶…éŽé€™å€‹å¤§å°ę™‚ę‰čŖæé«˜å®ƒć€‚ +* `session_idle_timeout`ļ¼ščˆŠē‰ˆå·„ä½œéšŽę®µåœØę²’ęœ‰ä»»ä½•é€²č”Œäø­ēš„č«‹ę±‚ę™‚åÆä»„é–’ē½®ēš„ē§’ę•øļ¼Œč¶…éŽå¾Œä¼ŗęœå™Øå°±ęŠŠå®ƒé—œęŽ‰ć€‚é čØ­ē‚ŗ 1800怂`None` ęœƒåœē”Øå®ƒć€‚č«‹č¦‹ [å·„ä½œéšŽę®µå­˜ē•™ę™‚é–“čˆ‡é™åˆ¶](legacy-clients.md#session-lifetime-and-limits)怂 +* `max_sessions`ļ¼šäø€å€‹č™•ē†ēØ‹åŗåŒę™‚čƒ½ęŒęœ‰å¤šå°‘å€‹čˆŠē‰ˆå·„ä½œéšŽę®µć€‚é čØ­ē‚ŗ 10 000怂`None` ęœƒē§»é™¤é€™å€‹é™åˆ¶ć€‚åœØåŒäø€ēÆ€ęœ‰čŖŖę˜Žć€‚ * `event_store`态`retry_interval`态`transport_security`ļ¼šåÆēŗŒå‚³ę€§čˆ‡ DNS é‡ę–°ē¶å®šé˜²č­·ć€‚é€™äŗ›åÆä»„å…ˆę”¾č‘—ļ¼Œē­‰åˆ°éƒØē½²åˆ° localhost ä»„å¤–ēš„åœ°ę–¹å†čŖŖļ¼›`transport_security` 在 **[éƒØē½²čˆ‡ę““å±•](deploy.md)** ęœ‰čŖŖę˜Žć€‚ !!! warning diff --git a/i18n/zh-hant/pages/run/legacy-clients.md b/i18n/zh-hant/pages/run/legacy-clients.md index edfbce63..cce6d297 100644 --- a/i18n/zh-hant/pages/run/legacy-clients.md +++ b/i18n/zh-hant/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ęœå‹™čˆŠē‰ˆē”Øęˆ¶ē«Æ {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK ä¾ęÆå€‹č«‹ę±‚ēš„ `MCP-Protocol-Version` ęØ™é ­ä¾†č·Æē”±ć€‚ęØ™ę˜Ž `2026-07- ## äø€å€‹č™•ē†å‡½å¼ļ¼Œå…©å€‹äø–ä»£ {#one-handler-both-eras} -äø‹é¢ę˜Æäø€å€‹åæ…é ˆå•ä½æē”Øč€…å•é”Œēš„å·„å…·ļ¼Œä»„åŠå…©å€‹äø–ä»£ēš„ē”Øęˆ¶ē«Æå‘¼å«å®ƒļ¼š +äø‹é¢ę˜Æäø€å€‹åæ…é ˆå•ä½æē”Øč€…å•é”Œēš„å·„å…·ļ¼š -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` éœ€č¦äø€ęØ£ęØ”åž‹ę²’ęœ‰ęä¾›ēš„ę±č„æļ¼šč¦å¹¾ęœ¬ć€‚`Annotated[..., Resolve(ask_quantity)]` å°±ę˜Æå·„å…·å®£å‘Šé€™ä»¶äŗ‹ēš„ę–¹å¼ļ¼ˆå®Œę•“čŖŖę˜Žč«‹č¦‹ **[ē›øä¾ę€§](../handlers/dependencies.md)**)。`reserve` č£”ę²’ęœ‰ä»»ä½•åœ°ę–¹ęŒ‡åē‰ˆęœ¬ć€ęŖ¢ęŸ„čƒ½åŠ›ęˆ–åšåˆ†ę”Æć€‚ -å…©å€‹ē”Øęˆ¶ē«Æ**åŒę™‚**é–‹č‘—ļ¼Œé€£åˆ°åŒäø€å€‹ `mcp` 物件。`mode="legacy"` 會執蔌 `initialize` äŗ¤ę”ļ¼šę­£ę˜Æ 2026 ä¹‹å‰ēš„ē”Øęˆ¶ē«Æęœƒé–‹å•Ÿēš„é‚£ēØ®é€£ē·šć€‚å¦äø€å€‹ē”Øé čØ­å€¼ļ¼Œč½åœØ `2026-07-28`怂 +透過 HTTP ęä¾›ęœå‹™ļ¼Œäø‹é¢ę˜Æå…©å€‹äø–ä»£ēš„ē”Øęˆ¶ē«Æå‘¼å«å®ƒļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +å…©å€‹ē”Øęˆ¶ē«Æ**åŒę™‚**é–‹č‘—ļ¼Œé€£åˆ°åŒäø€å€‹ę­£åœØåŸ·č”Œēš„ä¼ŗęœå™Øć€‚`mode="legacy"` 會執蔌 `initialize` äŗ¤ę”ļ¼šę­£ę˜Æ 2026 ä¹‹å‰ēš„ē”Øęˆ¶ē«Æęœƒé–‹å•Ÿēš„é‚£ēØ®é€£ē·šć€‚å¦äø€å€‹ē”Øé čØ­å€¼ļ¼Œč½åœØ `2026-07-28`ć€‚å¾žē¬¬äŗŒå€‹ēµ‚ē«Æę©ŸåŸ·č”Œ `python client.py`: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK ä¾ęÆå€‹č«‹ę±‚ēš„ `MCP-Protocol-Version` ęØ™é ­ä¾†č·Æē”±ć€‚ęØ™ę˜Ž `2026-07- !!! warning `event_store=` ēœ‹čµ·ä¾†åƒč§£ę³•ļ¼Œä½†äøę˜Æć€‚å®ƒę˜Æ**åÆę¢å¾©ę€§**ļ¼ˆęŠŠę¼ęŽ‰ēš„ SSE äŗ‹ä»¶é‡ę’­ēµ¦é‡ę–°é€£å›ž**åŒäø€å€‹**å·„ä½œéšŽę®µēš„ē”Øęˆ¶ē«Æļ¼‰ļ¼Œäøę˜Æå·„ä½œéšŽę®µå„²å­˜å€ć€‚å®ƒę°øé äøęœƒč®“å·„ä½œéšŽę®µčƒ½å¾žå¦äø€å€‹č™•ē†ēØ‹åŗå­˜å–åˆ°ć€‚ +## å·„ä½œéšŽę®µēš„å­˜ę“»ę™‚é–“čˆ‡äøŠé™ {#session-lifetime-and-limits} + +čˆŠē‰ˆå·„ä½œéšŽę®µäøęœƒę°øé ę“»č‘—ļ¼Œå–®äø€č™•ē†ēØ‹åŗä¹ŸäøęœƒęŒęœ‰ē„”é™å¤šå€‹ć€‚ęœ‰å…©å€‹čØ­å®šęŽ§åˆ¶é€™ä»¶äŗ‹ļ¼Œå…©č€…éƒ½ę˜Æ `run()`态`streamable_http_app()` 和 `Server.streamable_http_app()` äøŠēš„é—œéµå­—å¼•ę•øć€‚ē¾ä»£ļ¼ˆ`2026-07-28`ļ¼‰é€£ē·šå’Œ `stateless_http=True` éƒ½ę²’ęœ‰å·„ä½œéšŽę®µļ¼Œę‰€ä»„é€™å…©å€‹čØ­å®šå°å®ƒå€‘éƒ½äøé©ē”Øć€‚ + +| 設定 | 預設值 | ä½œē”Ø | ē”Øęˆ¶ē«Æēœ‹åˆ°ä»€éŗ¼ | é—œé–‰ę–¹å¼ | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 åˆ†é˜ļ¼‰ | é—œé–‰äø€å€‹å·²ē¶“é‚£éŗ¼ä¹…ę²’ęœ‰ä»»ä½•é€²č”Œäø­äŗ‹é …ēš„å·„ä½œéšŽę®µć€‚ | `404 Session not found`ć€‚åæ…é ˆé‡ę–° `initialize`怂 | `None` | +| `max_sessions` | `10_000` | č¶…éŽé€™å€‹ę•øé‡å°±ę‹’ēµ•å†é–‹ę–°ēš„å·„ä½œéšŽę®µć€‚ę—¢ęœ‰ēš„å·„ä½œéšŽę®µäøå—å½±éŸæļ¼Œä¹Ÿäøęœƒé©…é€ä»»ä½•äø€å€‹ć€‚ | `503 Too many open sessions`,JSON-RPC 錯誤碼 `-32603`怂 | `None` | + +ä»€éŗ¼ē®—ć€Œé€²č”Œäø­ć€ļ¼š + +* äø€ę¢é–‹č‘—ēš„ `GET` 串流。SDK ēš„ē”Øęˆ¶ē«ÆęœƒäæęŒäø€ę¢é–‹č‘—ļ¼Œę‰€ä»„å·²é€£ē·šēš„ē”Øęˆ¶ē«Æļ¼Œå®ƒēš„å·„ä½œéšŽę®µę°øé äøęœƒéŽęœŸć€‚ +* é‚„åœØå›žę‡‰äø­ēš„č«‹ę±‚ć€‚åŸ·č”Œę™‚é–“č¶…éŽé€¾ę™‚ēš„å·„å…·å‘¼å«äøęœƒč¢«äø­ę–·ļ¼Œå€’ę•øč¦ē­‰å®ƒēµęŸå¾Œę‰é–‹å§‹ć€‚ +* ę²’ęœ‰åˆ„ēš„äŗ†ć€‚č«‹ę±‚čˆ‡č«‹ę±‚ä¹‹é–“ļ¼Œę™‚é˜ē…§č·‘ć€‚å·„ä½œéšŽę®µäøŠēš„ä»»ä½•č«‹ę±‚éƒ½ęœƒč®“å®ƒé‡ę–°čµ·ē®—ļ¼Œ`ping` ä¹Ÿē®—ć€‚å·„ä½œéšŽę®µäø€ę—¦éŽęœŸļ¼Œę²’ęœ‰ä»»ä½•ę±č„æčƒ½č®“å®ƒå¾©ę“»ć€‚ + +用 `DELETE` ēµęŸå·„ä½œéšŽę®µēš„ē”Øęˆ¶ē«Æęœƒē«‹åˆ»é‡‹ę”¾å®ƒć€‚é–‹é ­č«‹ę±‚č¢«ę‹’ēµ•ēš„ē”Øęˆ¶ē«Æä¹Ÿę˜Æć€‚ + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +å…©ēØ®äŗ‹ä»¶éƒ½ęœƒå‡ŗē¾åœØä¼ŗęœå™ØčØ˜éŒ„č£”ć€‚éŽęœŸę˜Æ `INFO` å±¤ē“šēš„ `Session idle timeout`ć€‚ę‹’ēµ•é–‹å•Ÿę˜Æ `WARNING` å±¤ē“šēš„ `Refusing to open a new session: sessions are already open`怂 + +äøŠé™ę˜Æä»„č™•ē†ēØ‹åŗē‚ŗå–®ä½ć€‚å››å€‹ worker ę™‚ļ¼ŒäøŠé™ę˜Æ `max_sessions` ēš„å››å€ļ¼ŒęÆå€‹ worker å„č‡Ŗč®“č‡Ŗå·±ēš„å·„ä½œéšŽę®µéŽęœŸć€‚ + ## å”Æäø€ēš„é–‹é—œļ¼š`stateless_http` {#the-one-knob-stateless_http} å¦‚ęžœé»ę€§ę˜Æä½ äøé”˜ä»˜ēš„ä»£åƒ¹ļ¼Œčƒ½ę”¹ēš„ę±č„æå‰›å„½åŖęœ‰äø€ęØ£ć€‚ @@ -73,7 +108,7 @@ SDK ä¾ęÆå€‹č«‹ę±‚ēš„ `MCP-Protocol-Version` ęØ™é ­ä¾†č·Æē”±ć€‚ęØ™ę˜Ž `2026-07- `json_response=True` äøę˜Æé‚£å€‹é–‹é—œļ¼Œä½†å®ƒåœØ**ęÆå€‹**čˆŠē‰ˆå·„ä½œéšŽę®µäøŠéƒ½ęœƒä»˜å‡ŗäø€åŠåŒęØ£ēš„ä»£åƒ¹ļ¼šē”Øå–®äø€ JSON ęœ¬é«”å›žę‡‰ēš„ `POST` ę²’ęœ‰äø²ęµåÆä¾›č«‹ę±‚ēÆ„åœēš„é€šé“ä½æē”Øļ¼Œę‰€ä»„č«‹ę±‚é€”äø­ēš„ `ctx.elicit()` ęœƒå¼•ē™¼åŒęØ£ēš„ `NoBackChannelError`ļ¼Œē¶åœØč©²č«‹ę±‚äøŠēš„é€šēŸ„å‰‡č¢«äøŸęŽ‰ć€‚å·„ä½œéšŽę®µēš„ēØē«‹äø²ęµäøå—å½±éŸæļ¼šäøē›øé—œēš„é€šēŸ„ē…§ęØ£é€é”ć€‚ !!! check - ę•…ę„åšéŒÆäø€ę¬”ć€‚`reserve` ę­£ę˜Æå‰›å‰›ęœå‹™äŗ†å…©å€‹ē”Øęˆ¶ē«Æēš„é‚£å€‹å·„å…·ć€‚ē”Ø `stateless_http=True` éƒØē½²å®ƒļ¼Œé€éŽ HTTP é€£äøŠåŒęØ£ēš„å…©å€‹ē”Øęˆ¶ē«Æļ¼Œå¾žå„č‡Ŗå‘¼å«å®ƒć€‚ + ę•…ę„åšéŒÆäø€ę¬”ć€‚`reserve` ę­£ę˜Æå‰›å‰›ęœå‹™äŗ†å…©å€‹ē”Øęˆ¶ē«Æēš„é‚£å€‹å·„å…·ć€‚ē”Ø `stateless_http=True` éƒØē½²å®ƒļ¼Œé€£äøŠåŒęØ£ēš„å…©å€‹ē”Øęˆ¶ē«Æļ¼Œå¾žå„č‡Ŗå‘¼å«å®ƒć€‚ ē¾ä»£ē”Øęˆ¶ē«Æé‚„ę˜Æå¾—åˆ° `Reserved 2 of 'Dune'.`ļ¼Œē¾ä»£č·Æå¾‘ę²’č®Šć€‚ diff --git a/i18n/zh-hant/pages/troubleshooting.md b/i18n/zh-hant/pages/troubleshooting.md index fb91a046..e7bc4717 100644 --- a/i18n/zh-hant/pages/troubleshooting.md +++ b/i18n/zh-hant/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # ē–‘é›£ęŽ’č§£ {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +é‚£å¹¾å‰‡ęœƒå¾ž `http://localhost:8000/mcp` é€£äøŠå®ƒļ¼Œę‰€ä»„č®“å®ƒä»„ HTTP ęŒēŗŒåŸ·č”Œč‘—ļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + é€™äø€é å¼•ē”Øēš„éŒÆčŖ¤éƒ½ę˜ÆēœŸēš„ļ¼šSDK č‡Ŗå·±ēš„ęø¬č©¦å„—ä»¶ęœƒé‡ē¾ęÆäø€å€‹ć€‚ ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -ä¼ŗęœå™ØäøčŖå¾—ē”Øęˆ¶ē«Æé€ä¾†ēš„ `Mcp-Session-Id`ļ¼Œå¹¾ä¹Žéƒ½ę˜Æå› ē‚ŗä¼ŗęœå™Ø**é‡ę–°å•Ÿå‹•äŗ†**ļ¼ˆęˆ–ę˜Æä½ č¢«å°Žåˆ°å¦äø€å€‹åÆ¦ä¾‹ļ¼‰ć€‚å·„ä½œéšŽę®µå­˜åœØé‚£äø€å€‹č™•ē†ēØ‹åŗēš„čØ˜ę†¶é«”å…§ć€‚ +ä¼ŗęœå™ØäøčŖå¾—ē”Øęˆ¶ē«Æé€ä¾†ēš„ `Mcp-Session-Id`ć€‚č¦å˜›ä¼ŗęœå™Ø**é‡ę–°å•Ÿå‹•äŗ†**ļ¼ˆęˆ–ę˜Æä½ č¢«å°Žåˆ°å¦äø€å€‹åÆ¦ä¾‹ļ¼‰ļ¼Œč¦å˜›å·„ä½œéšŽę®µ**éŽęœŸäŗ†**ļ¼Œå› ē‚ŗåœØ `session_idle_timeout`(預設 30 åˆ†é˜ļ¼‰é€™ę®µę™‚é–“å…§ę²’ęœ‰ä»»ä½•é€²č”Œäø­ēš„č«‹ę±‚ć€‚č«‹č¦‹ [å·„ä½œéšŽę®µēš„å­˜ę“»ę™‚é–“čˆ‡é™åˆ¶](run/legacy-clients.md#session-lifetime-and-limits)ć€‚å·„ä½œéšŽę®µå­˜åœØé‚£äø€å€‹č™•ē†ēØ‹åŗēš„čØ˜ę†¶é«”å…§ć€‚ ę²’ęœ‰ä¼ŗęœå™Øēš„ bug åÆę‰¾ć€‚HTTP å›žę‡‰ę˜Æ `404`ļ¼Œč€Œå®ƒēš„ęœ¬ę–‡**就是** JSON-RPCļ¼Œę‰€ä»„å’ŒäøŠé¢ēš„ `421` äøåŒļ¼Œpython `Client` ęœƒåŽŸå°äøå‹•åœ°ęŠŠé€™å€‹é”Æē¤ŗēµ¦ä½ ļ¼š @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif äæ®ę­£ę˜Æé‡ę–°é€£ē·šļ¼šé›¢é–‹ `async with Client(...)` å€å”Šļ¼Œé€²å…„äø€å€‹ę–°ēš„ļ¼Œå®ƒęœƒå”å•†å‡ŗę–°ēš„å·„ä½œéšŽę®µć€‚å°ę–¼é•·ę™‚é–“å­˜ę“»ēš„ē”Øęˆ¶ē«Æļ¼Œé€™č”Øē¤ŗåœØå‘¼å«å¤–é¢ę””ęˆŖ `MCPError`ļ¼Œé‡åˆ°é€™å€‹čØŠęÆå°±é‡ę–°é€£ē·šļ¼Œč€Œäøę˜ÆåœØå·²ē¶“ę­»ęŽ‰ēš„å·„ä½œéšŽę®µč£”é‡č©¦ć€‚ -å¦‚ęžœ**ę²’ęœ‰**é‡ę–°å•Ÿå‹•ä¹Ÿē™¼ē”Ÿļ¼Œä»£č”Øä½ č·‘äŗ†äøåŖäø€å€‹ worker å»ę²’ęœ‰é»ę€§å·„ä½œéšŽę®µļ¼ˆsticky sessionļ¼‰ļ¼šęÆå€‹ worker éƒ½ęœ‰č‡Ŗå·±ēš„å·„ä½œéšŽę®µč”Øļ¼Œę‰€ä»„å°Žåˆ°éŒÆčŖ¤ worker ēš„č«‹ę±‚å°±ęœƒč½åˆ°é€™č£”ć€‚é€™ä»¶äŗ‹å’Œå®ƒēš„å…©ēØ®äæ®ę­£ļ¼ˆé»ę€§č·Æē”±ļ¼Œęˆ– `stateless_http=True`)請見 **[éƒØē½²čˆ‡ę““å±•](run/deploy.md)** 和 **[ęœå‹™čˆŠē‰ˆē”Øęˆ¶ē«Æ](run/legacy-clients.md)**怂 +å¦‚ęžœ**ę²’ęœ‰**é‡ę–°å•Ÿå‹•ć€ē”Øęˆ¶ē«Æä¹Ÿę²’ęœ‰å®‰éœé‚£éŗ¼ä¹…å»é‚„ę˜Æē™¼ē”Ÿļ¼Œä»£č”Øä½ č·‘äŗ†äøåŖäø€å€‹ worker å»ę²’ęœ‰é»ę€§å·„ä½œéšŽę®µļ¼ˆsticky sessionļ¼‰ļ¼šęÆå€‹ worker éƒ½ęœ‰č‡Ŗå·±ēš„å·„ä½œéšŽę®µč”Øļ¼Œę‰€ä»„å°Žåˆ°éŒÆčŖ¤ worker ēš„č«‹ę±‚å°±ęœƒč½åˆ°é€™č£”ć€‚é€™ä»¶äŗ‹å’Œå®ƒēš„å…©ēØ®äæ®ę­£ļ¼ˆé»ę€§č·Æē”±ļ¼Œęˆ– `stateless_http=True`)請見 **[éƒØē½²čˆ‡ę““å±•](run/deploy.md)** 和 **[ęœå‹™čˆŠē‰ˆē”Øęˆ¶ē«Æ](run/legacy-clients.md)**怂 -å°ä¼ŗęœå™Øē¶­é‹äŗŗå“”ä¾†čŖŖļ¼Œå°ę‡‰ēš„čØ˜éŒ„č”Œę˜Æ `Rejected request with unknown or expired session ID: `ć€‚å®ƒä»„ `INFO` å±¤ē“ščØ˜éŒ„ļ¼Œę‰€ä»„åœØåøøē”Øēš„ `WARNING` é–€ęŖ»äø‹ēœ‹äøåˆ°ć€‚å‰›éƒØē½²å®Œēœ‹åˆ°å®ƒäø€é™£é™£å†’å‡ŗä¾†ę˜Æę­£åøøēš„ļ¼›ęÆå€‹å·²é€£ē·šēš„ē”Øęˆ¶ē«Æéƒ½åœØé‡ę–°é€£ē·šć€‚ +å°ä¼ŗęœå™Øē¶­é‹äŗŗå“”ä¾†čŖŖļ¼Œå°ę‡‰ēš„čØ˜éŒ„č”Œę˜Æ `Rejected request with unknown or expired session ID: `ć€‚å®ƒä»„ `INFO` å±¤ē“ščØ˜éŒ„ļ¼Œę‰€ä»„åœØåøøē”Øēš„ `WARNING` é–€ęŖ»äø‹ēœ‹äøåˆ°ć€‚å‰›éƒØē½²å®Œēœ‹åˆ°å®ƒäø€é™£é™£å†’å‡ŗä¾†ę˜Æę­£åøøēš„ļ¼›ęÆå€‹å·²é€£ē·šēš„ē”Øęˆ¶ē«Æéƒ½åœØé‡ę–°é€£ē·šć€‚å¦‚ęžœę˜Æå·„ä½œéšŽę®µéŽęœŸļ¼Œé‚£äø€č”Œå‰é¢ęœƒå…ˆå‡ŗē¾ `Session idle timeout`,同樣是 `INFO` å±¤ē“šć€‚ ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ä¼ŗęœå™Øęƒ³å•ä½æē”Øč€…äø€ä»¶äŗ‹ļ¼Œč€Œé€™å€‹ē”Øęˆ¶ē«Æå¾žę²’čŖŖéŽå®ƒåÆä»„č¢«å•ć€‚ -徵詢(elicitationļ¼‰č§£ęžå™ØåœØå·²é€£ē·šēš„ē”Øęˆ¶ē«Æę²’ęœ‰å®£å‘Šč”Øå–®å¾µč©¢ę™‚ļ¼Œęœƒäø€é–‹å§‹å°±ę‹’ēµ•ļ¼Œč€Œ `e.error.data` ęœƒē²¾ē¢ŗåÆ«å‡ŗē¼ŗäŗ†ä»€éŗ¼ļ¼š +這間 Bistro åœØčØ‚ä½å‰ęœƒé€éŽč§£ęžå™Øå…ˆå•äø€č²ļ¼š + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +ē”Øå®ƒå–ä»£ Weather ä¼ŗęœå™Øä¾†ęœå‹™ļ¼Œå†å¾žäø€å€‹ę²’ęœ‰å‚³ `elicitation_callback` ēš„ē”Øęˆ¶ē«Æå‘¼å« `book_table`ć€‚č§£ęžå™Øęœƒäø€é–‹å§‹å°±ę‹’ēµ•ļ¼Œå› ē‚ŗå·²é€£ē·šēš„ē”Øęˆ¶ē«Æå¾žę²’å®£å‘Šč”Øå–®å¾µč©¢ļ¼ˆelicitationļ¼‰ļ¼Œč€Œ `e.error.data` ęœƒē²¾ē¢ŗåÆ«å‡ŗē¼ŗäŗ†ä»€éŗ¼ļ¼š ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: č™•ē†å‡½å¼č©¦åœ–åœØč«‹ę±‚é€”äø­čÆē¹«ē”Øęˆ¶ē«Æļ¼Œä½†é€™ę¢é€£ē·šäøŠēš„é€™ę¬”å‘¼å«ę²’ęœ‰čƒ½ę‰æč¼‰ä¼ŗęœå™Øē™¼å‡ŗč«‹ę±‚ēš„é€šé“ć€‚ęœ‰äø‰ēØ®ä¼ŗęœå™ØčØ­å®šęœƒč®“å‘¼å«č½åˆ°é€™ēØ®č™•å¢ƒć€‚ -**`2026-07-28` é€£ē·šļ¼šä»»ä½•å‚³č¼øę–¹å¼ļ¼Œäø€å¾‹å¦‚ę­¤ć€‚** ē¾ä»£å”å®šå®Œå…Øę²’ęœ‰ä¼ŗęœå™Øē™¼čµ·ēš„č«‹ę±‚ļ¼Œę‰€ä»„ä¼ŗęœå™ØåœØé€å‡ŗä»»ä½•ę±č„æä¹‹å‰å°±ę‹’ēµ•ć€‚å·„å…·č£”ēš„ `ctx.elicit()` ę˜Æé‡åˆ°é€™å€‹ēš„å…øåž‹ę–¹å¼ļ¼ˆå°±åœØē¬¬äø€ę¬”čØ˜ę†¶é«”å…§ęø¬č©¦ę™‚ļ¼Œå› ē‚ŗ `Client(server)` äøē”Øäŗ¤ä»£å°±ęœƒå”å•†å‡ŗ `2026-07-28`ļ¼‰ļ¼Œč€Œå‚³å…„ `elicitation_callback=` ä»€éŗ¼éƒ½äøęœƒę”¹č®Šļ¼Œå› ē‚ŗę ¹ęœ¬ę²’ęœ‰č«‹ę±‚é€åˆ°ē”Øęˆ¶ē«Æč®“å®ƒå›žē­”ļ¼š +**`2026-07-28` é€£ē·šļ¼šä»»ä½•å‚³č¼øę–¹å¼ļ¼Œäø€å¾‹å¦‚ę­¤ć€‚** ē¾ä»£å”å®šå®Œå…Øę²’ęœ‰ä¼ŗęœå™Øē™¼čµ·ēš„č«‹ę±‚ļ¼Œę‰€ä»„ä¼ŗęœå™ØåœØé€å‡ŗä»»ä½•ę±č„æä¹‹å‰å°±ę‹’ēµ•ć€‚å·„å…·č£”ēš„ `ctx.elicit()` ę˜Æé‡åˆ°é€™å€‹ēš„å…øåž‹ę–¹å¼ļ¼Œé€šåøøę˜ÆåœØé‚£å€‹å·„å…·ēš„ē¬¬äø€å€‹čØ˜ę†¶é«”å…§ **[測試](get-started/testing.md)** č£”ļ¼Œå› ē‚ŗ `Client(mcp)` äøē”Øäŗ¤ä»£å°±ęœƒå”å•†å‡ŗ `2026-07-28`。傳兄 `elicitation_callback=` ä»€éŗ¼éƒ½äøęœƒę”¹č®Šļ¼Œå› ē‚ŗę ¹ęœ¬ę²’ęœ‰č«‹ę±‚é€åˆ°ē”Øęˆ¶ē«Æč®“å®ƒå›žē­”ļ¼š ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * ä¼ŗęœå™ØčØ˜éŒ„č£”ēš„ `Tool already exists:` ę˜Æå…©å€‹åŒåå·„å…·åˆä½µęˆäø€å€‹ēš„å”Æäø€č·”č±”ć€‚ * 一個 421ļ¼Œäø‰ēØ®åÆ«ę³•ļ¼š`Server returned an error response`(python `Client`)、`421 Misdirected Request` / `Invalid Host header`ļ¼ˆå…¶ä»–ę‰€ęœ‰ę±č„æļ¼‰ć€`Invalid Host header: `ļ¼ˆä¼ŗęœå™ØčØ˜éŒ„ļ¼‰ć€‚äæ®ę­£ļ¼š`transport_security=TransportSecuritySettings(allowed_hosts=[...])`怂 * `Task group is not initialized` -> ęŽ›č¼‰ēš„ę‡‰ē”ØēØ‹å¼ļ¼Œå…¶å¤–å±¤ē”Ÿå‘½é€±ęœŸå¾žęœŖé€²å…„ `mcp.session_manager.run()`怂 -* `Session not found` -> ä¼ŗęœå™Øé‡ę–°å•Ÿå‹•äŗ†ļ¼›é‡ę–°é€£ē·šć€‚ +* `Session not found` -> ä¼ŗęœå™Øé‡ę–°å•Ÿå‹•äŗ†ļ¼Œęˆ–å·„ä½œéšŽę®µéŽęœŸäŗ†ļ¼ˆ`session_idle_timeout`ļ¼‰ļ¼›é‡ę–°é€£ē·šć€‚ * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` éœ€č¦ä¼ŗęœå™Øåˆ°ē”Øęˆ¶ē«Æēš„é€šé“ļ¼š`2026-07-28` é€£ē·šå¾žä¾†ę²’ęœ‰ļ¼Œ`stateless_http=True` ę‹æčµ°äŗ†čˆŠē‰ˆēš„é‚£ę¢ļ¼Œ`json_response=True` ę‹æčµ°äŗ†č«‹ę±‚ēÆ„åœēš„é‚£ę¢ć€‚ę”¹ē”Øč§£ęžå™Øļ¼ˆčˆŠē‰ˆē”Øęˆ¶ē«Æé‚„éœ€č¦äø€å€‹äæē•™é€šé“ēš„ä¼ŗęœå™Øļ¼‰ć€‚å®ƒēš„é„°å±… `Method not found` å‰‡ę˜Æč«‹ę±‚äŗ†äø€å€‹å°ę–¹ēš„å”å®šäæ®čØ‚ē‰ˆę²’ęœ‰ēš„ę–¹ę³•ć€‚ * `Client did not declare the form elicitation capability ...` 和 `Elicitation not supported` -> ē”Øęˆ¶ē«Æå°‘äŗ† `elicitation_callback=`怂 * `Invalid or expired requestState` åœØē·šč·ÆäøŠå¾žäøčŖŖåŽŸå› ć€‚ä¼ŗęœå™ØčØ˜éŒ„ęœƒčŖŖļ¼›`unknown key` č”Øē¤ŗč¦åœØå„ worker 之間共用 `RequestStateSecurity(keys=[...])`怂 diff --git a/i18n/zh-hant/pages/whats-new.md b/i18n/zh-hant/pages/whats-new.md index 50a07665..bcdfc7fe 100644 --- a/i18n/zh-hant/pages/whats-new.md +++ b/i18n/zh-hant/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 ēš„ę–°åŠŸčƒ½ {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 äŗ¤ēµ¦ä½ ēš„ę˜Æäø‰å±¤å·¢ē‹€ēµę§‹ļ¼šäø€å€‹ē”¢å‡ŗåŽŸå§‹äø²ęµēš„å‚³č¼ø context managerć€åŒ…åœØå¤–é¢ēš„ `ClientSession`ļ¼Œå†åŠ äøŠę‰‹å‹•å‘¼å«ēš„ `await session.initialize()`怂v2 åŖęœ‰äø€å€‹ē‰©ä»¶ļ¼š -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` ęŽ„å—äø€å€‹ä¼ŗęœå™Øē‰©ä»¶ļ¼ˆčØ˜ę†¶é«”å…§ć€ę²’ęœ‰å‚³č¼øļ¼Œä¹Ÿå°±ę˜Æęø¬č©¦ēš„åšę³•ļ¼‰ć€äø€å€‹ URL(Streamable HTTP)、一個 `StdioServerParameters`(stdio å­č™•ē†ēØ‹åŗļ¼‰ļ¼Œęˆ–ä»»ä½•å…¶ä»–å‚³č¼ø context managerļ¼Œä¾‹å¦‚ `sse_client(...)`。進兄 `async with` å°±ęœƒé€£ē·šäø¦å”å•†å”å®šē‰ˆęœ¬ļ¼Œäøē®”ä¼ŗęœå™Øč¬›ēš„ę˜Æå“Ŗå€‹äø–ä»£ļ¼›ä¹‹å¾Œ `client.server_capabilities` 和 `client.protocol_version` å°±ē›“ęŽ„åœØé‚£č£”ļ¼Œä¼ŗęœå™Øęœ‰č”Øę˜Žčŗ«åˆ†ę™‚ `client.server_info` ä¹ŸåœØļ¼ˆå®ƒē¾åœØę˜Æ `Implementation | None`ļ¼Œå› ē‚ŗ 2026 äø–ä»£ēš„čŗ«åˆ†ę˜Æéøē”Øēš„ļ¼‰ć€‚åœØ v1 čØ»å†Šēš„å–ęØ£å’Œå¾µč©¢å›žå‘¼ä»ē„¶ęœ‰ę•ˆļ¼ˆå›žå‘¼ęœ¬é«”ęœƒé‡åˆ°č·Ÿęœ¬é å…¶ä»–åœ°ę–¹äø€ęØ£ēš„ snake_case å±¬ę€§ę”¹åļ¼‰ļ¼Œē¾åœØä¹Ÿęœƒå›žę‡‰ 2026 é¢Øę ¼ēš„ć€Œēµęžœäø­å¤¾åø¶č«‹ę±‚ć€ļ¼ˆč¦‹äø‹ę–‡ļ¼‰ļ¼Œč€Œäø”ę˜Æäø¦č”ŒåŸ·č”Œļ¼Œäøå†äø€ę¬”äø€å€‹ć€‚ęƒ³č¦ä½ŽéšŽä»‹é¢ēš„äŗŗļ¼Œ`ClientSession` ä»åœØåŗ•äø‹ļ¼Œ`client.session` ęœƒęŠŠå®ƒäŗ¤ēµ¦ä½ ļ¼›å®ƒä¹Ÿęœ‰č®Šå‹•ļ¼ˆč·‘åœØę–°ēš„åˆ†ę“¾å™Øå¼•ę“ŽäøŠļ¼Œč‡Ŗå·±ēš„éƒØåˆ†ē°½ē« ä¹Ÿę”¹äŗ†ļ¼‰ļ¼Œę‰€ä»„å¾€äø‹é‘½ä¹‹å‰å…ˆč®€ **[é·ē§»ęŒ‡å—](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**怂 +`Client` ęŽ„å—äø€å€‹ URL(Streamable HTTP)、一個 `StdioServerParameters`(stdio å­č™•ē†ēØ‹åŗļ¼‰ć€ä»»ä½•å…¶ä»–å‚³č¼ø context managerļ¼ˆä¾‹å¦‚ `sse_client(...)`ļ¼‰ļ¼Œęˆ–č€…åœØęø¬č©¦č£”ē›“ęŽ„ęŽ„å—ä¼ŗęœå™Øē‰©ä»¶ęœ¬čŗ«ļ¼ˆčØ˜ę†¶é«”å…§ć€ę²’ęœ‰å‚³č¼øļ¼‰ć€‚é€²å…„ `async with` å°±ęœƒé€£ē·šäø¦å”å•†å”å®šē‰ˆęœ¬ļ¼Œäøē®”ä¼ŗęœå™Øč¬›ēš„ę˜Æå“Ŗå€‹äø–ä»£ļ¼›ä¹‹å¾Œ `client.server_capabilities` 和 `client.protocol_version` å°±ē›“ęŽ„åœØé‚£č£”ļ¼Œä¼ŗęœå™Øęœ‰č”Øę˜Žčŗ«åˆ†ę™‚ `client.server_info` ä¹ŸåœØļ¼ˆå®ƒē¾åœØę˜Æ `Implementation | None`ļ¼Œå› ē‚ŗ 2026 äø–ä»£ēš„čŗ«åˆ†ę˜Æéøē”Øēš„ļ¼‰ć€‚åœØ v1 čØ»å†Šēš„å–ęØ£å’Œå¾µč©¢å›žå‘¼ä»ē„¶ęœ‰ę•ˆļ¼ˆå›žå‘¼ęœ¬é«”ęœƒé‡åˆ°č·Ÿęœ¬é å…¶ä»–åœ°ę–¹äø€ęØ£ēš„ snake_case å±¬ę€§ę”¹åļ¼‰ļ¼Œē¾åœØä¹Ÿęœƒå›žę‡‰ 2026 é¢Øę ¼ēš„ć€Œēµęžœäø­å¤¾åø¶č«‹ę±‚ć€ļ¼ˆč¦‹äø‹ę–‡ļ¼‰ļ¼Œč€Œäø”ę˜Æäø¦č”ŒåŸ·č”Œļ¼Œäøå†äø€ę¬”äø€å€‹ć€‚ęƒ³č¦ä½ŽéšŽä»‹é¢ēš„äŗŗļ¼Œ`ClientSession` ä»åœØåŗ•äø‹ļ¼Œ`client.session` ęœƒęŠŠå®ƒäŗ¤ēµ¦ä½ ļ¼›å®ƒä¹Ÿęœ‰č®Šå‹•ļ¼ˆč·‘åœØę–°ēš„åˆ†ę“¾å™Øå¼•ę“ŽäøŠļ¼Œč‡Ŗå·±ēš„éƒØåˆ†ē°½ē« ä¹Ÿę”¹äŗ†ļ¼‰ļ¼Œę‰€ä»„å¾€äø‹é‘½ä¹‹å‰å…ˆč®€ **[é·ē§»ęŒ‡å—](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**怂 **[ē”Øęˆ¶ē«Æ](client/index.md)** ä»‹ē“¹å®ƒļ¼Œ**[ē”Øęˆ¶ē«Æå‚³č¼øę–¹å¼](client/transports.md)** čŖŖę˜Žå››ēØ®é€£ē·šå½¢å¼ļ¼Œ**[ē”Øęˆ¶ē«Æå›žå‘¼](client/callbacks.md)** čŖŖę˜Žå›žå‘¼ęœ¬čŗ«ļ¼Œ**[測試](get-started/testing.md)** ē¤ŗēÆ„å–ä»£ v1 `create_connected_server_and_client_session()` č¼”åŠ©å‡½å¼ēš„čØ˜ę†¶é«”å…§ęØ”å¼ć€‚ @@ -166,11 +166,15 @@ v2 實作 2026-07-28 äæ®čØ‚ē‰ˆļ¼Œč€Œäø”**兩個**äæ®čØ‚ē‰ˆåŒę™‚ęœå‹™ļ¼šåŒäø€ ę›æä»£ę–¹ę”ˆęŠŠå‘¼å«åéŽä¾†ć€‚éœ€č¦å‘ä½æē”Øč€…č¦ę±č„æēš„å·„å…·**回傳**é‚£å€‹å•é”Œļ¼ˆ`InputRequiredResult`ļ¼‰ļ¼Œē”Øęˆ¶ē«Æē”Øäø€ē›“éƒ½ęœ‰ēš„é‚£äŗ›å›žå‘¼å›žē­”å®ƒļ¼Œē„¶å¾Œåø¶č‘—ē­”ę”ˆé‡č©¦é€™ę¬”å‘¼å«ć€‚é‚£å€‹čæ“åœˆ `Client` ęœƒę›æä½ é©…å‹•ć€‚åœØä¼ŗęœå™ØäøŠå¾ˆå°‘éœ€č¦č‡Ŗå·±å»ŗé‚£å€‹ēµęžœļ¼Œå› ē‚ŗ **[ē›øä¾ę€§](handlers/dependencies.md)** ęœƒåšļ¼šē”Ø `Resolve(ask_quantity)` ęØ™čØ»äø€å€‹åƒę•øļ¼Œå…¶äø­ `ask_quantity` ę˜Æä½ åÆ«ēš„ę™®é€šå‡½å¼ļ¼ŒSDK å°±ęœƒē”Øé€£ē·šę”Æę“ēš„ę©Ÿåˆ¶åŽ»å•ļ¼ŒåœØčˆŠē‰ˆå·„ä½œéšŽę®µäøŠę˜Æå³ę™‚ēš„å¾µč©¢č«‹ę±‚ļ¼ŒåœØ 2026 äøŠę˜Æå¤šč¼Ŗå¾€čæ”ć€‚äø€å€‹å·„å…·ęœ¬é«”ļ¼Œå…©å€‹äø–ä»£ļ¼š -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -é€™å€‹ęŖ”ę”ˆęŠŠę•“å€‹č³£é»žé›†äø­åœØäø€č™•ļ¼šäø€å€‹ä¼ŗęœå™Øć€äø€å€‹ä»„ `Resolve` ē‚ŗå¾Œē›¾ēš„å·„å…·ļ¼Œä»„åŠäø€å€‹čˆŠē‰ˆē”Øęˆ¶ē«ÆåŠ äø€å€‹ē¾ä»£ē”Øęˆ¶ē«Æéƒ½ę‹æåˆ°ē­”ę”ˆļ¼Œå…ØåœØčØ˜ę†¶é«”å…§ć€‚**[å¤šč¼Ŗå¾€čæ”č«‹ę±‚](handlers/multi-round-trip.md)** č§£é‡‹ę©Ÿåˆ¶ļ¼ˆåŒ…ę‹¬ SDK ę›æä½ åÆ†å°å’Œé©—č­‰ēš„ `request_state`);**[徵詢](handlers/elicitation.md)** čŖŖę˜Žę€Žéŗ¼å•ć€‚ +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +é€™å…©å€‹ęŖ”ę”ˆå°±ę˜Æę•“å€‹č³£é»žļ¼šäø€å€‹ä¼ŗęœå™Øć€äø€å€‹ä»„ `Resolve` ē‚ŗå¾Œē›¾ēš„å·„å…·ļ¼Œä»„åŠäø€å€‹čˆŠē‰ˆē”Øęˆ¶ē«ÆåŠ äø€å€‹ē¾ä»£ē”Øęˆ¶ē«Æļ¼Œéƒ½å¾žåŒäø€å€‹åŸ·č”Œäø­ēš„ä¼ŗęœå™Øę‹æåˆ°ē­”ę”ˆļ¼ˆ**[ęœå‹™čˆŠē‰ˆē”Øęˆ¶ē«Æ](run/legacy-clients.md)** ęœƒåø¶ä½ čµ°éŽé€™å…©å€‹ęŖ”ę”ˆļ¼‰ć€‚**[å¤šč¼Ŗå¾€čæ”č«‹ę±‚](handlers/multi-round-trip.md)** č§£é‡‹ę©Ÿåˆ¶ļ¼ˆåŒ…ę‹¬ SDK ę›æä½ åÆ†å°å’Œé©—č­‰ēš„ `request_state`);**[徵詢](handlers/elicitation.md)** čŖŖę˜Žę€Žéŗ¼å•ć€‚ !!! warning "é€™ę˜Æē§»ę¤å¾Œēš„ v1 ä¼ŗęœå™Øå”Æäø€ęœƒę”¹č®Šč”Œē‚ŗēš„åœ°ę–¹" ä½ č‡Ŗå·±ēš„ęø¬č©¦ęœ€å…ˆē¢°åˆ°ļ¼š`Client(mcp)` 對 v2 ä¼ŗęœå™Øé čØ­å”å•† 2026-07-28ļ¼Œę‰€ä»„å‘¼å« `ctx.elicit()` ēš„å·„å…·åœØ v1 é€šéŽēš„ęø¬č©¦č£”ęœƒå¤±ę•—ć€‚ęŠŠå•é”Œę¬é€² `Resolve(...)` åƒę•øļ¼ˆč·Øäø–ä»£åÆę”œļ¼‰ļ¼Œęˆ–č€…å¦‚ęžœēœŸēš„ęƒ³č¦ęŽØé€č”Œē‚ŗļ¼Œå°±ęŠŠęø¬č©¦ē”Øęˆ¶ē«Æé‡˜åœØ `mode="legacy"`怂 @@ -197,7 +201,7 @@ v2 實作 2026-07-28 äæ®čØ‚ē‰ˆļ¼Œč€Œäø”**兩個**äæ®čØ‚ē‰ˆåŒę™‚ęœå‹™ļ¼šåŒäø€ * **ę““å……åŠŸčƒ½ę˜Æäø€ē­‰å…¬ę°‘ć€‚** ä¼ŗęœå™Øå’Œē”Øęˆ¶ē«ÆåœØåå‘ DNS č­˜åˆ„ē¢¼åŗ•äø‹å®£å‘Šéøē”Øēš„čƒ½åŠ›ēµ„åˆļ¼ˆ[SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)ļ¼‰ļ¼›å…§å»ŗēš„ `Apps` ę““å……åŠŸčƒ½ļ¼ˆMCP Appsļ¼‰ę˜Æåƒč€ƒēÆ„ä¾‹ć€‚**[ę““å……åŠŸčƒ½](advanced/extensions.md)** 和 **[MCP Apps](advanced/apps.md)**怂 * **éŒÆčŖ¤ē¢¼ęØ™ęŗ–åŒ–äŗ†ć€‚** ę‰¾äøåˆ°ēš„č³‡ęŗę˜Æ `-32602`,URI ę”¾åœØ `error.data`ļ¼Œę–°ēš„č¦ę ¼äæē•™ē¢¼å‰‡ę˜Æ `-32020`ļ¼ˆęØ™é ­äøē¬¦ļ¼‰ć€`-32021`ļ¼ˆē¼ŗå°‘åæ…č¦čƒ½åŠ›ļ¼‰å’Œ `-32022`ļ¼ˆäøę”Æę“ēš„å”å®šē‰ˆęœ¬ļ¼‰ć€‚**[ē–‘é›£ęŽ’č§£](troubleshooting.md)** ä»„ē¢ŗåˆ‡ēš„čØŠęÆē‚ŗē“¢å¼•ć€‚ * **ęŽˆę¬Šę›“äøå®¹ę˜“ē”ØéŒÆäŗ†ć€‚** ē”Øęˆ¶ē«Æęœƒé©—č­‰éšØęŽˆę¬Šē¢¼å›žå‚³ēš„ `iss`([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)ļ¼›`callback_handler` ē¾åœØå›žå‚³ `AuthorizationCodeResult`ļ¼‰ļ¼ŒčØ»å†Šę™‚é€å‡ŗ `application_type`ļ¼Œč€Œäø”ēµ•äøęœƒå°äøåŒēš„ęŽˆę¬Šä¼ŗęœå™Øé‡é€ę†‘č­‰ć€‚ä¼ę„­é‚£äø€č§’ēš„ę–°ę±č„æļ¼š[SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) čŗ«åˆ†ę–·čØ€ęµēØ‹ć€‚**[é·ē§»ęŒ‡å—](migration.md)** åˆ—å‡ŗęÆäø€é … OAuth č®Šę›“ļ¼›å®Œę•“čŖŖę˜Žč«‹č¦‹ **[ē”Øęˆ¶ē«Æēš„ OAuth](client/oauth-clients.md)** 和 **[čŗ«åˆ†ę–·čØ€](client/identity-assertion.md)**怂 -* **ęÆå€‹ä¼ŗęœå™Øéƒ½åÆčæ½č¹¤ć€‚** OpenTelemetry ä»„äø­ä»‹č»Ÿé«”ēš„å½¢å¼é čØ­å•Ÿē”Øļ¼šęÆå€‹č«‹ę±‚éƒ½ęœ‰äø€å€‹ä¼ŗęœå™Ø spanļ¼ŒåœØč™•ē†ēØ‹åŗčØ­å®š exporter ä¹‹å‰å®Œå…Øę²’ęœ‰ęˆęœ¬ć€‚å…©ē«Æéƒ½č·‘ SDK ę™‚ļ¼Œē”Øęˆ¶ē«Æé‚„ęœƒåœØ `_meta` 裔傳播 W3C trace contextļ¼Œę‰€ä»„čæ½č¹¤ęœƒęŽ„čµ·ä¾†ć€‚**[OpenTelemetry](run/opentelemetry.md)**怂 +* **ęÆå€‹ä¼ŗęœå™Øéƒ½åÆčæ½č¹¤ć€‚** OpenTelemetry ä»„äø­ä»‹č»Ÿé«”ēš„å½¢å¼é čØ­å•Ÿē”Øļ¼šęÆå€‹č«‹ę±‚éƒ½ęœ‰äø€å€‹ä¼ŗęœå™Ø spanļ¼ŒåœØč™•ē†ēØ‹åŗčØ­å®š exporter ä¹‹å‰å®Œå…Øę²’ęœ‰ęˆęœ¬ć€‚å…©ē«Æéƒ½ä½æē”Ø SDK ę™‚ļ¼Œē”Øęˆ¶ē«Æé‚„ęœƒåœØ `_meta` 裔傳播 W3C trace contextļ¼Œę‰€ä»„čæ½č¹¤ęœƒęŽ„čµ·ä¾†ć€‚**[OpenTelemetry](run/opentelemetry.md)**怂 ## 從 v1 å‡ē“šļ¼Ÿ {#upgrading-from-v1} diff --git a/i18n/zh/pages/advanced/apps.md b/i18n/zh/pages/advanced/apps.md index f3bb1d4f..1cc37465 100644 --- a/i18n/zh/pages/advanced/apps.md +++ b/i18n/zh/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK ęŠŠå®ƒä½œäøŗå†…ē½®ēš„ `Apps` ę‰©å±•ļ¼ˆ`io.modelcontextprotocol/ui`ļ¼‰ęä¾› ## äø€äøŖåø¦ē•Œé¢ēš„ę—¶é’Ÿ {#a-clock-with-a-face} -```python title="server.py" hl_lines="19 22 30 32" +```python title="server.py" hl_lines="17 20 28 30" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,11 +39,31 @@ HTML ęœ¬čŗ«ē›‘å¬å®æäø»ēš„ `postMessage` å¹¶ę˜¾ē¤ŗē»“ęžœć€‚ēœŸę­£ēš„åŗ”ē”ØčÆ·åœØ ęØ”åž‹čÆ»ēš„ę˜Æ `content`ļ¼›iframe ę˜Æē»™äŗŗēœ‹ēš„ć€‚ę”ÆęŒ UI ēš„å®æäø»ē…§ę ·ä¼šęŠŠę–‡ęœ¬ē»“ęžœäŗ¤ē»™ęØ”åž‹ļ¼Œč€ŒēŗÆę–‡ęœ¬å®¢ęˆ·ē«Æ**åŖ**ę‹æåˆ°é‚£éƒØåˆ†ć€‚ę‰€ä»„ę ‡å‡†ęØ”å¼ę˜Æäø€äøŖå·„å…·ć€äø¤ē§å›žē­”ć€‚å†ēœ‹äø€é `get_time`: -```python title="server.py" hl_lines="23-27" +```python title="server.py" hl_lines="21-25" --8<-- "docs_src/apps/tutorial001.py" ``` -åŖęœ‰å½“å®¢ęˆ·ē«Æå£°ę˜Žäŗ† `io.modelcontextprotocol/ui` ę‰©å±•ļ¼Œ**å¹¶äø”**åœØå…¶ `mimeTypes` č®¾ē½®é‡Œåˆ—å‡ŗäŗ† `text/html;profile=mcp-app` ę—¶ļ¼Œ`client_supports_apps(ctx)` ę‰äøŗ `True`ć€‚čæ™äøŖå­—ę®µę˜Æåæ…å”«ēš„ļ¼Œēœē•„å®ƒēš„å®¢ęˆ·ē«Æäøē®—ę•°ć€‚åŒäø€ę–‡ä»¶é‡Œēš„ `main()` å£°ę˜Žēš„ę­£ę˜Æčæ™äŗ›ļ¼šåå•†äø­å®¢ęˆ·ē«Æēš„é‚£äø€åŠļ¼ŒäŗŽę˜ÆåÆŒē»“ęžœå°±čæ”å›žäŗ†ć€‚ +åŖęœ‰å½“å®¢ęˆ·ē«Æå£°ę˜Žäŗ† `io.modelcontextprotocol/ui` ę‰©å±•ļ¼Œ**å¹¶äø”**åœØå…¶ `mimeTypes` č®¾ē½®é‡Œåˆ—å‡ŗäŗ† `text/html;profile=mcp-app` ę—¶ļ¼Œ`client_supports_apps(ctx)` ę‰äøŗ `True`ć€‚čæ™äøŖå­—ę®µę˜Æåæ…å”«ēš„ļ¼Œēœē•„å®ƒēš„å®¢ęˆ·ē«Æäøē®—ę•°ć€‚äø‹é¢ę˜Æåå•†äø­å®¢ęˆ·ē«Æēš„é‚£äø€åŠļ¼š + +```python title="client.py" hl_lines="8 12" +--8<-- "docs_src/apps/tutorial001_client.py" +``` + +é€ščæ‡ HTTP ęä¾› `server.py`ļ¼Œē„¶åŽåœØå¦äø€äøŖē»ˆē«Æčæč”Œå®¢ęˆ·ē«Æļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +```console +python client.py +``` + +```text +2026-06-26T12:00:00Z +``` + +čæ”å›žēš„ę˜ÆåÆŒē»“ęžœć€‚ä»Ž `Client` č°ƒē”Øé‡ŒåŽ»ęŽ‰ `extensions=[APPS_SUPPORT]`ļ¼ŒåŒäø€äøŖēØ‹åŗå°±ä¼šę”¹äøŗę‰“å° `The time is 2026-06-26T12:00:00Z.`ļ¼Œčæ™ä¹Ÿę˜ÆēŗÆę–‡ęœ¬å®¢ęˆ·ē«Æę‰€čƒ½ēœ‹åˆ°ēš„å…ØéƒØć€‚ !!! warning ē»äøč¦ęŠŠ `"[Rendered UI]"` čæ™ę ·ēš„å ä½ē¬¦å½“ä½œå”Æäø€ēš„å†…å®¹čæ”å›žć€‚å¦‚ęžœå›žé€€ę–‡ęœ¬ę²”ē”Øļ¼Œčæ™äøŖå·„å…·åÆ¹ę‰€ęœ‰ēŗÆę–‡ęœ¬å®¢ęˆ·ē«Æä¹ƒč‡³ęØ”åž‹ęœ¬čŗ«å°±éƒ½ę²”ē”Øć€‚ęŠŠé‚£å„čÆå†™å‡ŗę„ć€‚ diff --git a/i18n/zh/pages/advanced/extensions.md b/i18n/zh/pages/advanced/extensions.md index 13be45f4..118616bf 100644 --- a/i18n/zh/pages/advanced/extensions.md +++ b/i18n/zh/pages/advanced/extensions.md @@ -1,6 +1,6 @@ --- translation: - sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff] + sections: [05891e7cc1938a13, b3c01a6af28c51ee, 6eba6ce094f4417e, 125f28588c85dd7b, ddd8969b698ca11c, ed6af2df4b656dff] tool: 1 --- # 扩展 {#extensions} @@ -49,7 +49,7 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ęœ€å°ēš„ęœ‰ē”Øę‰©å±•å°±ę˜Æäø€äøŖå·„å…·åŠ äø€ä»½č®¾ē½®ę˜ å°„ļ¼š -```python title="server.py" hl_lines="17 19-20 22-23 26" +```python title="server.py" hl_lines="16 18-19 21-22 25" --8<-- "docs_src/extensions/tutorial003.py" ``` @@ -57,17 +57,23 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string * `settings()` 是在 `capabilities.extensions["com.example/stamps"]` å¤„å£°ę˜Žēš„å€¼ć€‚čæ”å›ž `{}`ļ¼ˆé»˜č®¤å€¼ļ¼‰č”Øē¤ŗå£°ę˜ŽčÆ„ę‰©å±•ä½†äøåø¦ä»»ä½•č®¾ē½®ć€‚ * ę‰©å±•ę°øčæœę‹æäøåˆ°ęœåŠ”å™Øć€‚å®ƒä»„ę•°ę®ēš„å½¢å¼å£°ę˜Žč“”ēŒ®ļ¼Œē”± `MCPServer` ę¶ˆč“¹ć€‚ę²”ęœ‰åÆä¾›äæ®ę”¹ēš„ `self.server`怂 -`main()` å°±ę˜ÆčÆę˜Žļ¼šäø€äøŖē›“ęŽ„åÆ¹ē€ `mcp` ēš„å†…å­˜å®¢ęˆ·ē«Æļ¼š +用 HTTP ęä¾›ęœåŠ”ļ¼Œäø€äøŖå®¢ęˆ·ē«Æå°±ę˜ÆčÆę˜Žļ¼š -```python title="server.py" hl_lines="29-34" ---8<-- "docs_src/extensions/tutorial003.py" +```console +uv run mcp run server.py --transport streamable-http ``` +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/extensions/tutorial003_client.py" +``` + +ęœ¬é”µęÆäøŖ `server.py` éƒ½ē”Øčæ™ę”å‘½ä»¤åÆåŠØļ¼ŒęÆäøŖ `client.py` éƒ½åœØå¦äø€äøŖē»ˆē«Æé‡Œē”Ø `python client.py` äøŽå®ƒå¹¶č”Œčæč”Œć€‚ + ### ęä¾›č‡Ŗå·±ēš„ę–¹ę³• {#serving-your-own-methods} ę‰©å±•åÆä»„ę³Øå†Œ**ę–°ēš„čÆ·ę±‚ę–¹ę³•**ļ¼šå®ƒč‡Ŗå·±ēš„åŠØčÆļ¼ŒäøŽč§„čŒƒå®šä¹‰ēš„ę–¹ę³•å¹¶åˆ—ęä¾›ļ¼š -```python title="server.py" hl_lines="16-22 31 40-48" +```python title="server.py" hl_lines="14-20 24 33-41" --8<-- "docs_src/extensions/tutorial004.py" ``` @@ -83,14 +89,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ### å®¢ęˆ·ē«Æäø€ä¾§ {#the-client-side} -åŒäø€äøŖę–‡ä»¶ēš„ `main()` å°±ę˜Æå®¢ęˆ·ē«Æēš„å…ØéƒØå†…å®¹ļ¼Œäø¤åŠéƒ½åœØļ¼š +å®¢ęˆ·ē«Æę˜Æäø€äøŖē‹¬ē«‹ēš„ēØ‹åŗļ¼Œå®¢ęˆ·ē«Æčæ™äø€ä¾§ēš„äø¤åŠéƒ½åœØé‡Œé¢ļ¼š -```python title="server.py" hl_lines="54-58" ---8<-- "docs_src/extensions/tutorial004.py" +```python title="client.py" hl_lines="21-23 27-30" +--8<-- "docs_src/extensions/tutorial004_client.py" ``` * `Client(..., extensions=[advertise(EXTENSION_ID)])` å£°ę˜ŽčÆ„ę‰©å±•ć€‚čæ™äŗ›å£°ę˜Žä¼šå˜ęˆ `ClientCapabilities.extensions`:在 2026-07-28 čæžęŽ„äøŠļ¼ŒčÆ„ę˜ å°„éšęÆäøŖčÆ·ę±‚ēš„ `_meta` äæ”å°ä¼ é€’ļ¼Œę‰€ä»„ęœåŠ”å™ØåœØ**ęÆäøŖ**čÆ·ę±‚äøŠéƒ½čƒ½ēœ‹åˆ°å®ƒļ¼›åœØę—§ē‰ˆčæžęŽ„äøŠļ¼Œå®ƒéš `initialize` ę”ę‰‹ä¼ é€’ć€‚ęœåŠ”å™Øä»£ē äøē”Øå…³åæƒę˜Æå“Ŗäø€ē§ļ¼š`require_client_extension(ctx, ...)` 和 `ctx.session.check_client_capability(...)` åœØäø¤ę”č·Æå¾„äøŠéƒ½ä¼ščÆ»å–ę­£ē”®ēš„ę„ęŗć€‚ * åŽ‚å•†ę–¹ę³•č¦å¾€äø‹äø€å±‚ļ¼Œē”Ø `client.session.send_request(...)`ļ¼›`Client` åŖäøŗč§„čŒƒåŠØčÆęä¾›äø€ē­‰ę–¹ę³•ć€‚`send_request` ęŽ„å—ä»»ä½• `Request` å­ē±»ļ¼Œę‰€ä»„åŽ‚å•†čÆ·ę±‚åŽŸę ·ä¼ å…„å³åÆć€‚ +* `SearchRequest` å’Œå®ƒęŗåø¦ēš„äø¤äøŖęØ”åž‹ę˜Æčæ™äøŖę‰©å±•ēš„ēŗæč·Æēŗ¦å®šļ¼Œę‰€ä»„å®¢ęˆ·ē«Æč‡Ŗå·±ä¹Ÿå£°ę˜Žäø€ä»½ć€‚å…¬å¼€å‘åøƒēš„ę‰©å±•ä¼šęŠŠå®ƒä»¬ę”¾čæ›äø€äøŖåŒę–¹éƒ½åÆ¼å…„ēš„åŒ…é‡Œć€‚ ### ę‹¦ęˆŖ `tools/call` {#intercepting-toolscall} @@ -109,12 +116,18 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ## ä½æē”Øå®¢ęˆ·ē«Æę‰©å±• {#using-a-client-extension} -**å®¢ęˆ·ē«Æę‰©å±•**ę˜Æä»Žę¶ˆč“¹äø€ä¾§ēœ‹ēš„åŒäø€ä»½ēŗ¦å®šļ¼šäø€ē»„å½’åœØåŒäø€äøŖę ‡čÆ†ē¬¦ä¹‹äø‹ēš„å®¢ęˆ·ē«Æč”Œäøŗć€‚ęŠŠå®žä¾‹ä¼ ē»™ `Client(extensions=[...])`ļ¼Œē„¶åŽē…§åøøč°ƒē”Øå·„å…·ļ¼š +**å®¢ęˆ·ē«Æę‰©å±•**ę˜Æä»Žę¶ˆč“¹äø€ä¾§ēœ‹ēš„åŒäø€ä»½ēŗ¦å®šļ¼šäø€ē»„å½’åœØåŒäø€äøŖę ‡čÆ†ē¬¦ä¹‹äø‹ēš„å®¢ęˆ·ē«Æč”Œäøŗć€‚čæ™é‡Œēš„ęœåŠ”å™Øå›žē­” `buy` ę—¶ē»™ēš„äøę˜Æč“§ē‰©ļ¼Œč€Œę˜Æäø€å¼ å¾…å…‘ę¢ēš„ę”¶ę®ļ¼Œč€Œäø”åŖåÆ¹å£°ę˜Žäŗ†čÆ„ę‰©å±•ēš„å®¢ęˆ·ē«Æčæ™ę ·åšļ¼š -```python title="client.py" hl_lines="66-68" +```python title="server.py" hl_lines="22-25" --8<-- "docs_src/extensions/tutorial006.py" ``` +åœØå®¢ęˆ·ē«Æļ¼ŒęŠŠå®žä¾‹ä¼ ē»™ `Client(extensions=[...])`ļ¼Œē„¶åŽē…§åøøč°ƒē”Øå·„å…·ļ¼š + +```python title="client.py" hl_lines="33-35" +--8<-- "docs_src/extensions/tutorial006_client.py" +``` + `call_tool("buy", ...)` čæ”å›žäø€äøŖę™®é€šēš„ `CallToolResult`ļ¼Œå’Œå…¶ä»–ä»»ä½•č°ƒē”Øäø€ę ·ć€‚ę‰©å±•ę”¹å˜ēš„ę˜Æļ¼šęœåŠ”å™ØēŽ°åœØåÆä»„ē”Ø `receipt` **ē»“ęžœå½¢ę€**č€Œäøę˜Æęœ€ē»ˆē»“ęžœę„å›žē­” `buy`,`Receipts` 会在 `call_tool` čæ”å›žä¹‹å‰ęŠŠå®ƒå®Œęˆļ¼ˆčæ™é‡Œę˜Æē”Øäø€ę¬”åŽē»­č°ƒē”Øå…‘ę¢ę”¶ę®ļ¼‰ć€‚č°ƒē”Øå¤„ä»€ä¹ˆéƒ½äøē”ØåŠØć€‚ åŽ»ęŽ‰čæ™äøŖę‰©å±•ļ¼Œčæ™äø€åˆ‡å°±éƒ½äøå­˜åœØļ¼šęœåŠ”å™Øēš„é—Øę§›ä¼šę‹’ē»ę²”ęœ‰å£°ę˜Žå®ƒēš„å®¢ęˆ·ē«Æļ¼ˆé”™čÆÆ -32021ļ¼‰ļ¼Œč€Œč·³čæ‡é—Øę§›ēš„ęœåŠ”å™Øå‘ę„ēš„č¢«č®¤é¢†å½¢ę€ä¼šę ”éŖŒå¤±č“„ļ¼Œę­£å¦‚č§„čŒƒåÆ¹ę— ę³•čÆ†åˆ«ēš„ `resultType` ę‰€č¦ę±‚ēš„é‚£ę ·ć€‚é»˜č®¤å…³é—­ļ¼Œēŗæč·Æäø¤ē«Æéƒ½ę˜Æć€‚ @@ -124,15 +137,15 @@ TypeError: Stamps.identifier must be a `vendor-prefix/name` string ```python from mcp.client import advertise -client = Client(mcp, extensions=[advertise("com.example/search")]) +client = Client("http://localhost:8000/mcp", extensions=[advertise("com.example/search")]) ``` ## ē¼–å†™å®¢ęˆ·ē«Æę‰©å±• {#writing-a-client-extension} 继承 `ClientExtension`ļ¼ŒåŖé‡å†™éœ€č¦ēš„éƒØåˆ†ć€‚č“”ēŒ®åˆ†äø‰ē±»ļ¼Œå„ęœ‰é»˜č®¤å®žēŽ°ļ¼š`settings()`态`claims()` 和 `notifications()`怂 -```python title="client.py" hl_lines="17-18 43-44 46-47" ---8<-- "docs_src/extensions/tutorial006.py" +```python title="client.py" hl_lines="16-17 25-26 28-29" +--8<-- "docs_src/extensions/tutorial006_client.py" ``` * ę ‡čÆ†ē¬¦éµå¾ŖäøŽęœåŠ”å™Øē«Æē›øåŒēš„čÆ­ę³•ļ¼ŒåœØē±»å®šä¹‰ę—¶ę ”éŖŒć€‚ @@ -153,12 +166,18 @@ def notifications(self) -> Sequence[NotificationBinding[Any]]: ### ę‰©å±•åŠØčÆ {#extension-verbs} -ę‰©å±•č‡Ŗå·±ēš„čÆ·ę±‚ę–¹ę³•äøéœ€č¦åœØå®¢ęˆ·ē«Æę³Øå†Œć€‚åŽ‚å•†čÆ·ę±‚ē±»åž‹ē»§ę‰æ `mcp.types.Request`ļ¼Œé€ščæ‡ `client.session.send_request` å‘é€ļ¼Œå¦‚[ęä¾›č‡Ŗå·±ēš„ę–¹ę³•](#serving-your-own-methods)ę‰€ē¤ŗć€‚č”„å……äø€ē‚¹ļ¼šå½“ęŸäøŖå‚ę•°é”®åæ…é”»ę”¾čæ› `Mcp-Name` 夓(tasks ä¹‹ē±»ēš„ę‰©å±•č§„čŒƒåÆ¹å…¶åŠØčÆęœ‰ę­¤č¦ę±‚ļ¼‰ę—¶ļ¼ŒčÆ·ę±‚ē±»åž‹č¦å£°ę˜Ž `name_param`: +ę‰©å±•č‡Ŗå·±ēš„čÆ·ę±‚ę–¹ę³•äøéœ€č¦åœØå®¢ęˆ·ē«Æę³Øå†Œć€‚åŽ‚å•†čÆ·ę±‚ē±»åž‹ē»§ę‰æ `mcp.types.Request`ļ¼Œé€ščæ‡ `client.session.send_request` å‘é€ļ¼Œå¦‚[ęä¾›č‡Ŗå·±ēš„ę–¹ę³•](#serving-your-own-methods)ę‰€ē¤ŗć€‚ä»„čæ™ę ·äø€äøŖęœåŠ”å™Øäøŗä¾‹ļ¼Œå®ƒēš„ę‰©å±•ęä¾›äø€äøŖäøŽęŸäøŖå…·åä½œäøšē›øå…³ēš„åŠØčÆļ¼š -```python title="client.py" hl_lines="22-25 46-47" +```python title="server.py" hl_lines="12-13 30" --8<-- "docs_src/extensions/tutorial007.py" ``` +å®¢ęˆ·ē«ÆäøŠč”„å……äø€ē‚¹ļ¼šå½“ęŸäøŖå‚ę•°é”®åæ…é”»ę”¾čæ› `Mcp-Name` 夓(tasks ä¹‹ē±»ēš„ę‰©å±•č§„čŒƒåÆ¹å…¶åŠØčÆęœ‰ę­¤č¦ę±‚ļ¼‰ę—¶ļ¼ŒčÆ·ę±‚ē±»åž‹č¦å£°ę˜Ž `name_param`: + +```python title="client.py" hl_lines="20-23 28-29" +--8<-- "docs_src/extensions/tutorial007_client.py" +``` + ä¼ščÆåœØęÆę”å‘é€č·Æå¾„äøŠéƒ½ä¼šęŠŠ `params["jobId"]` 镜像到 `Mcp-Name` äø­ļ¼Œå€¼ē¼ŗå¤±ę—¶ä¼šę˜Žē”®ęŠ„é”™ļ¼Œč€Œäøę˜Æę‚„ę‚„ę¼ęŽ‰äø€äøŖåæ…éœ€ēš„å¤“ć€‚ ## ę‰©å±•äøčƒ½åšä»€ä¹ˆ {#what-an-extension-cannot-do} diff --git a/i18n/zh/pages/advanced/low-level-server.md b/i18n/zh/pages/advanced/low-level-server.md index c4200b0f..437905d3 100644 --- a/i18n/zh/pages/advanced/low-level-server.md +++ b/i18n/zh/pages/advanced/low-level-server.md @@ -1,6 +1,6 @@ --- translation: - sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, cd0e9c933350390e] + sections: [2c79b6338e09b7ac, 9d5d10a5f0405d0a, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, d50fe7faead8cf68, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, 8db7116fc8ddd0ee, ebc33704fbd74262, 0fde3bcea081ba3a] tool: 1 --- # 底层 Server {#the-low-level-server} @@ -36,18 +36,22 @@ translation: ### 试一试 {#try-it} -čæ™äøŖę²”ęœ‰ Inspector åÆē”Øļ¼š`mcp dev` 和 `mcp run` åŖęŽ„å— `MCPServer`ć€‚å†…å­˜äø­ēš„ `Client` äøåœØä¹Žļ¼›å®ƒęŽ„ę”¶åŗ•å±‚ `Server` ēš„ę–¹å¼å’ŒęŽ„ę”¶ `MCPServer` å®Œå…Øäø€ę ·ļ¼š +`mcp dev` 和 `mcp run` åŖęŽ„å— `MCPServer`ļ¼Œę‰€ä»„čæ™äøŖęœåŠ”å™Øč¦č‡Ŗå·±ę„č·‘ć€‚`server.py` ēš„ęœ€åŽäø€č”Œē”Øå®ƒęž„å»ŗäŗ†äø€äøŖę™®é€šēš„ ASGI åŗ”ē”Øļ¼Œäŗ¤ē»™ uvicorn 运蔌: -```python title="main.py" +```console +uvicorn server:app --port 8000 +``` + +把 Inspector ęˆ–ä»»ä½•å®¢ęˆ·ē«ÆęŒ‡å‘ `http://localhost:8000/mcp`: + +```python title="client.py" import asyncio from mcp import Client -from server import server - async def main() -> None: - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("search_books", {"query": "dune", "limit": 5}) print(result.content) @@ -64,6 +68,8 @@ asyncio.run(main()) * `result.structured_content` 是 `None`ć€‚é«˜å±‚ęœåŠ”å™Øä¼šę›æä½ ęŠŠ `-> str` åŒ…č£…ęˆ `{"result": ...}`ļ¼›åœØčæ™é‡Œļ¼Œä½ ę²”ęž„å»ŗēš„äøœč„æę²”äŗŗę›æä½ ęž„å»ŗć€‚ * `list_tools` čæ”å›žēš„ę˜Æ**ä½ **ę•²čæ›åŽ»ēš„ęØ”å¼ļ¼Œäø€å­—äøå·®ć€‚é«˜å±‚ē‰ˆęœ¬åœØęÆäøŖå±žę€§äøŠéƒ½ęœ‰ `"title": "Query"`ļ¼Œę ¹äøŠčæ˜ęœ‰äø€äøŖ `"title": "search_booksArguments"`:Pydantic ēš„äŗ§ē‰©ć€‚åœØčæ™äø€å±‚ļ¼Œēŗæč·ÆäøŠęœ‰ä»€ä¹ˆļ¼Œéƒ½ę˜Æä½ ę”¾äøŠåŽ»ēš„ć€‚ +åœØęµ‹čÆ•é‡ŒåÆä»„č·³čæ‡ uvicorn å’Œē«Æå£ļ¼š`Client(server)` åœØčæ›ēØ‹å†…ęŽ„ę”¶åŗ•å±‚ `Server` ēš„ę–¹å¼å’ŒęŽ„ę”¶ `MCPServer` å®Œå…Øäø€ę ·ļ¼Œ**[测试](../get-started/testing.md)** č®²ēš„å°±ę˜Æčæ™äøŖęØ”å¼ć€‚ + ## ę²”ęœ‰ę›æä½ åšä»»ä½•ę£€ęŸ„ {#nothing-is-checked-for-you} `MCPServer` ä¼šåœØä½ ēš„å‡½ę•°čæč”Œä¹‹å‰ę‹’ē»é”™čÆÆēš„å‚ę•°ļ¼ŒęŒ‰å®ƒē”Ÿęˆēš„ęØ”å¼ę ”éŖŒč°ƒē”Øļ¼ˆ**[å·„å…·](../servers/tools.md)**)。 @@ -214,4 +220,4 @@ use Server.middleware to observe or wrap initialization * `add_request_handler(method, params_type, handler)` ęä¾›ä»»ę„ę–¹ę³•ć€‚`initialize` ę˜Æäæē•™ēš„ć€‚ * `Server` å…¬åøƒēš„čƒ½åŠ›ē”±ä½ ę³Øå†Œäŗ†å“Ŗäŗ›å¤„ē†å‡½ę•°ęŽØåÆ¼č€Œę„ć€‚ -`Client(server)` åÆ¹äø¤ē§ęœåŠ”å™Øäø€č§†åŒä»ļ¼Œå› äøŗå®ƒä»¬**就是**åŒäø€äøŖåč®®ļ¼Œčæ™ę­£ę˜Æå…³é”®ę‰€åœØć€‚å†å¾€äø‹äø€å±‚ę ¹ęœ¬äøę˜Æäø€äøŖē±»ļ¼šå®ƒę˜Æ **[äø­é—“ä»¶](middleware.md)**怂 +å®¢ęˆ·ē«ÆåÆ¹äø¤ē§ęœåŠ”å™Øäø€č§†åŒä»ļ¼Œå› äøŗå®ƒä»¬**就是**åŒäø€äøŖåč®®ļ¼Œčæ™ę­£ę˜Æå…³é”®ę‰€åœØć€‚å†å¾€äø‹äø€å±‚ę ¹ęœ¬äøę˜Æäø€äøŖē±»ļ¼šå®ƒę˜Æ **[äø­é—“ä»¶](middleware.md)**怂 diff --git a/i18n/zh/pages/advanced/pagination.md b/i18n/zh/pages/advanced/pagination.md index b15bf411..3fd55ac3 100644 --- a/i18n/zh/pages/advanced/pagination.md +++ b/i18n/zh/pages/advanced/pagination.md @@ -1,6 +1,6 @@ --- translation: - sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9] + sections: [a9aba7a026c7bd85, 83e2a08b9d46a398, 9fd8a0aa384b3257, 22a0129ee78b3c63, d875373c06d8d2f9] tool: 1 --- # 分锵 {#pagination} @@ -29,9 +29,13 @@ translation: ### 试一试 {#try-it} -`Client(server)` åœØå†…å­˜äø­čæžęŽ„åŗ•å±‚ `Server` ēš„ę–¹å¼ļ¼Œå’ŒčæžęŽ„ `MCPServer` å®Œå…Øäø€ę ·ć€‚ +`mcp run` åŖęŽ„å— `MCPServer`ļ¼Œę‰€ä»„čæ™äøŖęœåŠ”å™Øå¾—č‡Ŗå·±åÆåŠØć€‚`server.py` ēš„ęœ€åŽäø€č”Œē”Øčæ™äøŖ `Server` ęž„å»ŗå‡ŗäø€äøŖę™®é€šēš„ ASGI åŗ”ē”Øļ¼Œäŗ¤ē»™ uvicorn 运蔌: -äøåø¦å‚ę•°č°ƒē”Ø `list_resources()`ć€‚å¾—åˆ°åäøŖčµ„ęŗļ¼Œä»Ž `book-1` 到 `book-10`,`next_cursor` ę˜Æå­—ē¬¦äø² `"10"`怂 +```console +uvicorn server:app --port 8000 +``` + +ęŠŠä»»ę„å®¢ęˆ·ē«Æļ¼ˆ**[客户端](../client/index.md)**ļ¼Œęˆ–č€… Inspectorļ¼‰ęŒ‡å‘ `http://localhost:8000/mcp`ļ¼Œäøåø¦å‚ę•°č°ƒē”Ø `list_resources()`ć€‚å¾—åˆ°åäøŖčµ„ęŗļ¼Œä»Ž `book-1` 到 `book-10`,`next_cursor` ę˜Æå­—ē¬¦äø² `"10"`怂 用 `list_resources(cursor="10")` ęŠŠå®ƒäŗ¤å›žåŽ»ļ¼Œē¬¬äø€äøŖčµ„ęŗå°±ę˜Æ `book-11`ļ¼Œę–°ēš„ `next_cursor` 是 `"20"`怂 @@ -41,7 +45,7 @@ translation: `Client` äøŠēš„ęÆäøŖ `list_*` ę–¹ę³•ļ¼ˆ`list_tools`态`list_resources`态`list_resource_templates`态`list_prompts`ļ¼‰éƒ½ęŽ„å—äø€äøŖ `cursor=` å…³é”®å­—å‚ę•°ć€‚å–å®Œäø€äøŖåˆ†é”µåˆ—č”ØåŖéœ€č¦äø€äøŖ `while True`: -```python title="client.py" hl_lines="26-32" +```python title="client.py" hl_lines="9-15" --8<-- "docs_src/pagination/tutorial002.py" ``` @@ -49,7 +53,7 @@ translation: * 先 extend,**再**ēœ‹ `next_cursor`ļ¼šęœ€åŽäø€é”µä¹Ÿęœ‰čµ„ęŗć€‚ * `next_cursor is None` ę˜Æå‡ŗå£ć€‚å…¶ä»–ä»»ä½•å€¼éƒ½åŽŸå°äøåŠØåœ°ē›“ęŽ„å”žå›ž `cursor=`怂 -čæč”Œå®ƒēš„ `main()`ļ¼Œä¼šę‰“å° `100 resources`ļ¼šåé”µć€ęÆé”µåäøŖļ¼Œē”±äø€äøŖä»Žå¤“åˆ°å°¾éƒ½äøēŸ„é“ęœ‰åé”µēš„å¾ŖēŽÆę‹¼åœØäø€čµ·ć€‚ +让 uvicorn 继续运蔌 `server.py`ļ¼ŒåœØē¬¬äŗŒäøŖē»ˆē«Æé‡Œčæč”Œ `python client.py`ć€‚å®ƒä¼šę‰“å° `100 resources`ļ¼šåé”µć€ęÆé”µåäøŖļ¼Œē”±äø€äøŖä»Žå¤“åˆ°å°¾éƒ½äøēŸ„é“ęœ‰åé”µēš„å¾ŖēŽÆę‹¼åœØäø€čµ·ć€‚ čæ™å’Œ **[客户端](../client/index.md)** äøŗęÆäøŖ `list_*` åŠØčÆå±•ē¤ŗēš„ę˜ÆåŒäø€äøŖå¾ŖēŽÆļ¼Œč€Œäø”é¢åÆ¹äøåˆ†é”µēš„ęœåŠ”å™Øä¹Ÿę²”ęœ‰ä»»ä½•ä»£ä»·ļ¼šē¬¬äø€äøŖå“åŗ”é‡Œ `next_cursor` 就是 `None`ļ¼Œå¾ŖēŽÆåŖč·‘äø€ę¬”ć€‚ diff --git a/i18n/zh/pages/client/caching.md b/i18n/zh/pages/client/caching.md index 07c40df6..85f42d00 100644 --- a/i18n/zh/pages/client/caching.md +++ b/i18n/zh/pages/client/caching.md @@ -1,6 +1,6 @@ --- translation: - sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800] + sections: [9e7b9a1710e5aeba, 66a2e9acc9101d54, d3d25aa802f5145a, 04db67a886b7271c, 857690fb8f876800] tool: 1 --- # ē¼“å­˜ęē¤ŗ {#caching-hints} @@ -27,13 +27,13 @@ translation: åœØåŗ•å±‚ `Server` äøŠļ¼Œå¤„ē†å‡½ę•°ę‰‹åŠØęž„å»ŗē»“ęžœļ¼Œ`ttl_ms` / `cache_scope` åŖę˜Æē»“ęžœęØ”åž‹äøŠēš„å­—ę®µć€‚ę˜¾å¼č®¾ē½®äŗ†å®ƒä»¬ēš„å¤„ē†å‡½ę•°ę€»ę˜Æé€å­—ę®µåœ°ä¼˜å…ˆäŗŽęž„é€ å‡½ę•°ę˜ å°„ļ¼š -```python title="server.py" hl_lines="10 16" +```python title="server.py" hl_lines="11 17" --8<-- "docs_src/caching/tutorial002.py" ``` å¤„ē†å‡½ę•°ęŒ‡å®šäŗ† `ttl_ms=1_000`ļ¼ŒåÆ¹ä½œē”ØåŸŸåŖå­—ęœŖęć€‚ēŗæč·ÆäøŠę˜Æļ¼š`ttlMs: 1000`ļ¼ˆå¤„ē†å‡½ę•°ēš„å€¼ļ¼Œč€Œäøę˜Æę˜ å°„é‡Œēš„ `60_000`ļ¼‰å’Œ `cacheScope: "public"`ļ¼ˆę˜ å°„ēš„å€¼ļ¼Œå› äøŗå¤„ē†å‡½ę•°ę²”ęœ‰č®¾ē½®ļ¼‰ć€‚ę˜¾å¼ä¼˜å…ˆäŗŽé…ē½®ļ¼Œé…ē½®ä¼˜å…ˆäŗŽé»˜č®¤ć€‚čæ™ę”č§„åˆ™ęŒ‰å­—ę®µē”Ÿę•ˆļ¼Œę‰€ä»„å¤„ē†å‡½ę•°åÆä»„å›ŗå®šäø€äøŖå­—ę®µļ¼ŒęŠŠå¦äø€äøŖē•™ē»™ęœåŠ”å™ØčŒƒå›“ēš„ē­–ē•„ć€‚ -čæ™ä¹Ÿę˜Æęž„é€ å‡½ę•°ę— ę³•é¢„ēŸ„ēš„åŠØę€ęƒ…å†µēš„å‡ŗå£ļ¼šäø€äøŖęŒ‰ē”Øęˆ·čæ‡ę»¤ `resources/read` ēš„å¤„ē†å‡½ę•°ļ¼ŒåÆä»„åœØå…¶ä»–ę–¹é¢éƒ½ę˜Æ public ēš„ęœåŠ”å™ØäøŠäøŗęŸäøŖ URI čæ”å›ž `cache_scope="private"`怂 +čæ™ä¹Ÿę˜Æåŗ”åÆ¹ęž„é€ å‡½ę•°ę— ę³•é¢„ēŸ„ēš„åŠØę€ęƒ…å†µēš„å‡ŗå£ļ¼šäø€äøŖęŒ‰ē”Øęˆ·čæ‡ę»¤ `resources/read` ēš„å¤„ē†å‡½ę•°ļ¼ŒåÆä»„åœØå…¶ä»–ę–¹é¢éƒ½ę˜Æ public ēš„ęœåŠ”å™ØäøŠäøŗęŸäøŖ URI čæ”å›ž `cache_scope="private"`怂 å…³äŗŽåˆ†é”µåˆ—č”Øęœ‰äø€ē‚¹č¦ę³Øę„ļ¼šåč®®č¦ę±‚åŒäø€åˆ—č”Øēš„**ęÆäø€é”µ `cacheScope` ē›øåŒ**ć€‚ęž„é€ å‡½ę•°ę˜ å°„å¤©ē„¶ę»”č¶³čæ™äø€ē‚¹ļ¼Œå› äøŗå®ƒęŒ‰ę–¹ę³•č€Œäøę˜ÆęŒ‰é”µä½œé”®ć€‚ä½†č‡Ŗč”Œč¦†ē›–ä½œē”ØåŸŸēš„å¤„ē†å‡½ę•°č¦č‡Ŗå·±č“Ÿč“£čæ™ē§äø€č‡“ę€§ļ¼šåœØ**ęÆäø€**é”µéƒ½č¦†ē›–ļ¼Œē»äøč¦åŖåœØęœ‰ęøøę ‡ę—¶č¦†ē›–ļ¼Œå¦åˆ™ē¬¬äø€é”µå’Œē¬¬äŗŒé”µä¼šäøäø€č‡“ć€‚ @@ -41,10 +41,24 @@ translation: 在 2026-07-28 ä¼ščÆäøŠļ¼Œ`Client` ę›æä½ éµä»Žčæ™äŗ›ęē¤ŗļ¼šå®ƒå†…ē½®äŗ†å“åŗ”ē¼“å­˜ļ¼Œé»˜č®¤å¼€åÆć€‚åø¦ē€ `ttlMs` åˆ°č¾¾ēš„ē»“ęžœä¼šč¢«å­˜čµ·ę„ļ¼ŒåœØ TTL å†…ēš„ē›øåŒč°ƒē”Øē›“ęŽ„ē”±ē¼“å­˜ęä¾›ļ¼Œäøå‘ē”Ÿå¾€čæ”ć€‚**äø**åø¦ęē¤ŗēš„ē»“ęžœäøä¼šč¢«ē¼“å­˜ļ¼šę— ęē¤ŗēš„ē»“ęžœä½æē”Ø `CacheConfig.default_ttl_ms`,它默认为 `0`ļ¼ˆē«‹å³čæ‡ęœŸļ¼‰ļ¼Œę‰€ä»„ä»€ä¹ˆéƒ½ę²”å£°ę˜Žēš„ęœåŠ”å™Øēœ‹åˆ°ēš„ęµé‡å’Œä»„å‰äø€ęØ”äø€ę ·ļ¼Œäø€ę¬”č°ƒē”ØåÆ¹åŗ”äø€ę¬”čÆ·ę±‚ć€‚ -```python title="client.py" hl_lines="33 35 38" +ęƒ³äŗ²ēœ¼ēœ‹ēœ‹ļ¼Œå°±ē”Ø uvicorn čæč”ŒäøŠäø€čŠ‚ēš„ `server.py`ļ¼ˆå®ƒēš„ęœ€åŽäø€č”Œęž„å»ŗäŗ† ASGI åŗ”ē”Øļ¼‰ć€‚å¤„ē†å‡½ę•°ęÆę¬”ēœŸę­£ę‰§č”Œę—¶éƒ½ä¼šę‰“å°äø€č”Œļ¼š + +```console +uvicorn server:app --port 8000 +``` + +```python title="client.py" hl_lines="20 23 28" --8<-- "docs_src/caching/tutorial003.py" ``` +åœØå¦äø€äøŖē»ˆē«Æčæč”Œ `python client.py`ć€‚å®ƒę‰“å°å‡ŗē¬¬äø€äøŖē»“ęžœęŗåø¦ēš„ęē¤ŗļ¼Œå¤„ē†å‡½ę•°ēš„ `ttlMs` ęŒØē€ę˜ å°„ēš„ `cacheScope`: + +```text +1000 public +``` + +å‰©äø‹ēš„ęƒ…å†µēœ‹ęœåŠ”å™Øé‚£č¾¹ēš„ē»ˆē«Æļ¼šåœØ uvicorn ēš„čÆ·ę±‚ę—„åæ—ä¹‹é—“ļ¼Œ`tools/list served` å‡ŗēŽ°äŗ†äø‰ę¬”ć€‚ + å››ę¬”č°ƒē”Øļ¼Œäø‰ę¬”ęŠ“å–ć€‚ē¬¬äŗŒę¬”č°ƒē”Øę‰¾åˆ°äŗ†ę–°é²œę”ē›®ļ¼Œę ¹ęœ¬ę²”åˆ°ęœåŠ”å™Øļ¼›ęŠŠļ¼ˆę³Øå…„ēš„ļ¼‰ę—¶é’Ÿę‹Øčæ‡ TTL č®©ē¬¬äø‰ę¬”é‡ę–°ęŠ“å–ļ¼›ē¬¬å››ę¬”ęŒ‡å®šäŗ† `cache_mode="refresh"`ć€‚čæ™äøŖå…³é”®å­—å‚ę•°å­˜åœØäŗŽäŗ”äøŖē¼“å­˜åŠØčÆäøŠļ¼ˆ`list_tools`态`list_prompts`态`list_resources`态`list_resource_templates`态`read_resource`ļ¼‰ļ¼š * `"use"`ļ¼ˆé»˜č®¤ļ¼‰ęœ‰ę–°é²œę”ē›®å°±čæ”å›žå®ƒļ¼Œę²”ęœ‰å°±ęŠ“å–å¹¶å­˜å‚Øć€‚ @@ -53,7 +67,7 @@ translation: ęœ‰äø€ę”č§„åˆ™å‡Œé©¾äŗŽ `"use"` ä¹‹äøŠļ¼š**åø¦ `meta` ēš„č°ƒē”Øę€»ä¼šåˆ°č¾¾ęœåŠ”å™Øć€‚** 设置了 `meta`ļ¼ˆčæ›åŗ¦ä»¤ē‰Œć€čæ½čøŖå­—ę®µļ¼‰ēš„čÆ·ę±‚ęœŸęœ›äŗ§ē”Ÿäø€ę¬”ēŗæč·ÆčÆ·ę±‚ļ¼Œę‰€ä»„åœØ `cache_mode="use"` äø‹å®ƒč¢«å½“ä½œ `"refresh"` å¤„ē†ļ¼šč·³čæ‡ē¼“å­˜čÆ»å–ļ¼ŒęŠ“å–åˆ°ēš„ē»“ęžœä»ä¼šę›æę¢ē¼“å­˜ę”ē›®ć€‚`"bypass"` å’Œę˜¾å¼ēš„ `"refresh"` č”Œäøŗē…§ę—§ć€‚ -č¦å®Œå…Øå…³é—­ē¼“å­˜ļ¼Œē”Ø `Client(server, cache=None)` ęž„é€ ļ¼šęÆę¬”č°ƒē”Øé‡ę–°å˜ęˆäø€ę¬”å¾€čæ”ļ¼Œ`cache_mode` č™½ē„¶ä»č¢«ęŽ„å—ļ¼Œä½†äøčµ·ä½œē”Øć€‚ +č¦å®Œå…Øå…³é—­ē¼“å­˜ļ¼Œęž„é€  `Client` ę—¶ä¼ å…„ `cache=None`ļ¼šęÆę¬”č°ƒē”Øé‡ę–°å˜ęˆäø€ę¬”å¾€čæ”ļ¼Œ`cache_mode` č™½ē„¶ä»č¢«ęŽ„å—ļ¼Œä½†äøčµ·ä½œē”Øć€‚ ä½œē”ØåŸŸåŒę ·č‡ŖåŠØéµä»Žļ¼š`"private"` ę”ē›®ęŒ‰ē¼“å­˜ēš„**åˆ†åŒŗ**ļ¼ˆč§äø‹ę–‡ļ¼‰ä½œé”®ļ¼Œč€Œ `"public"` ę”ē›®åÆä»„é€‰ę‹©ę›“å¤§čŒƒå›“ēš„å…±äŗ«ć€‚å¹¶äø”åÆ¹äŗŽé€šēŸ„ē‚¹åēš„é‚£äŗ›ę”ē›®ļ¼Œ**é€šēŸ„ä¼˜å…ˆäŗŽ TTL**:`list_changed` é€šēŸ„ä¼šé©±é€åÆ¹åŗ”ēš„å·²ē¼“å­˜åˆ—č”Øļ¼Œ`resources/updated` ä¼šé©±é€ę°å„½å­˜åœØå…¶ URI äø‹ēš„å·²ē¼“å­˜čÆ»å–ē»“ęžœļ¼Œäøē®”å®ƒä»¬å¤šę–°é²œć€‚åœØ 2026-07-28 čæžęŽ„äøŠļ¼Œčæ™äŗ›é€šēŸ„é€ščæ‡ä½ ē”Ø `client.listen(...)` ę‰“å¼€ēš„ `subscriptions/listen` ęµåˆ°č¾¾ļ¼Œé©±é€ä¼šåœØä½ ēš„č§‚åÆŸč€…ēœ‹åˆ°äŗ‹ä»¶ä¹‹å‰å®Œęˆļ¼›čÆ¦č§ **[č®¢é˜…](subscriptions.md)**怂 diff --git a/i18n/zh/pages/client/callbacks.md b/i18n/zh/pages/client/callbacks.md index 201c937c..35c6d137 100644 --- a/i18n/zh/pages/client/callbacks.md +++ b/i18n/zh/pages/client/callbacks.md @@ -1,6 +1,6 @@ --- translation: - sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0] + sections: [adf3c545b5be46b6, 916cd3ab1c03f461, 32ef568335dd95a7, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 0abc5ea5cb7ff6b3] tool: 1 --- # å®¢ęˆ·ē«Æå›žč°ƒ {#client-callbacks} @@ -58,7 +58,7 @@ result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.') ä½ å‘å‡ŗäø€äøŖ `tools/call`ļ¼ŒęœåŠ”å™Øå›žčæ‡ę„äø€äøŖ `elicitation/create`ļ¼Œē”±ä½ ēš„å‡½ę•°ä½œē­”ā€”ā€”å…ØéƒØå‘ē”ŸåœØäø€ę¬”å·„å…·č°ƒē”Øä¹‹å†…ć€‚ !!! info - `Client(...)` č°ƒē”Øé‡Œēš„ `mode="legacy"` ę˜ÆēœŸę­£čµ·ä½œē”Øēš„ć€‚é»˜č®¤ęƒ…å†µäø‹ `Client(...)` åå•†ēš„ę˜ÆēŽ°ä»£åč®®č·Æå¾„ļ¼Œč€Œé‚£ę”č·Æå¾„ę²”ęœ‰ä¾›ęœåŠ”å™Øå‘å®¢ęˆ·ē«Æå‘čÆ·ę±‚ēš„åå‘é€šé“ļ¼ˆback-channelļ¼‰ļ¼š`ctx.elicit` ä¼šåœØä½ ēš„å›žč°ƒčæč”Œä¹‹å‰å°±å¤±č“„ć€‚å†³å®ščæ™äø€ē‚¹ēš„äøę˜Æä¼ č¾“ę–¹å¼ļ¼Œč€Œę˜Æåå•†å‡ŗēš„åč®®ļ¼Œå†…å­˜ä¼ č¾“å’Œ URL ä¼ č¾“éƒ½äø€ę ·ć€‚åŖč¦ä½ ēš„å®¢ęˆ·ē«Æéœ€č¦å›žē­”čæ™ē±»čÆ·ę±‚ļ¼Œå°±å›ŗå®šä½æē”Ø `mode="legacy"`ļ¼›ęœ¬é”µčƒŒåŽēš„ęÆäøŖęµ‹čÆ•éƒ½ę˜Æčæ™ä¹ˆåšēš„ć€‚čÆ¦č§ **[åč®®ē‰ˆęœ¬](../protocol-versions.md)**怂 + `Client(...)` č°ƒē”Øé‡Œēš„ `mode="legacy"` ę˜ÆēœŸę­£čµ·ä½œē”Øēš„ć€‚é»˜č®¤ęƒ…å†µäø‹ `Client(...)` åå•†ēš„ę˜ÆēŽ°ä»£åč®®č·Æå¾„ļ¼Œč€Œé‚£ę”č·Æå¾„ę²”ęœ‰ä¾›ęœåŠ”å™Øå‘å®¢ęˆ·ē«Æå‘čÆ·ę±‚ēš„åå‘é€šé“ļ¼ˆback-channelļ¼‰ļ¼š`ctx.elicit` ä¼šåœØä½ ēš„å›žč°ƒčæč”Œä¹‹å‰å°±å¤±č“„ć€‚å†³å®ščæ™äø€ē‚¹ēš„äøę˜Æä¼ č¾“ę–¹å¼ļ¼Œč€Œę˜Æåå•†å‡ŗēš„åč®®ć€‚åŖč¦ä½ ēš„å®¢ęˆ·ē«Æéœ€č¦å›žē­”čæ™ē±»čÆ·ę±‚ļ¼Œå°±å›ŗå®šä½æē”Ø `mode="legacy"`ļ¼›ęœ¬é”µčƒŒåŽēš„ęÆäøŖęµ‹čÆ•éƒ½ę˜Æčæ™ä¹ˆåšēš„ć€‚čÆ¦č§ **[åč®®ē‰ˆęœ¬](../protocol-versions.md)**怂 在 2026-07-28 ä¼ščÆäøŠļ¼Œčæ™äøŖå›žč°ƒå¹¶ę²”ęœ‰å¤±ę•ˆļ¼ŒåŖę˜Æč§¦å‘ę–¹å¼äøåŒļ¼šå½“å·„å…·čæ”å›žäø€äøŖęŗåø¦ `ElicitRequest` ēš„ `InputRequiredResult` ę—¶ļ¼Œ`Client` ä¼šęŠŠčÆ„ę”ē›®åˆ†ę“¾ē»™åŒäø€äøŖ `elicitation_callback`ļ¼Œå¹¶ę›æä½ é‡čÆ•čæ™ę¬”č°ƒē”Øć€‚é‚£äøŖęµēØ‹č§ **[å¤šč½®å¾€čæ”čÆ·ę±‚](../handlers/multi-round-trip.md)**怂 @@ -139,4 +139,4 @@ result.structured_content # {'result': ['elicitation']} * `sampling_callback` 和 `list_roots_callback` ēš„å·„ä½œę–¹å¼ē›øåŒļ¼Œä½†ęœåŠ”ēš„ę˜Æå·²å¼ƒē”Øēš„åŠŸčƒ½ļ¼›ēŽ°ä»£ęœåŠ”å™Øę”¹ē”Øå¤šč½®å¾€čæ”čÆ·ę±‚ć€‚ * `logging_callback` 和 `message_handler` ęŽ„ę”¶é€šēŸ„ć€‚å®ƒä»¬äøå£°ę˜Žä»»ä½•äøœč„æć€‚ -`Client(...)` ēš„ē¬¬äø€äøŖå‚ę•°ę˜Æä¼ č¾“åÆ¹č±”ć€‚**[å®¢ęˆ·ē«Æä¼ č¾“](transports.md)** ę¶µē›–äŗ†ęÆäø€ē§ć€‚ +`Client(...)` ēš„ē¬¬äø€äøŖå‚ę•°å†³å®šē”Øå“Ŗē§ä¼ č¾“ę–¹å¼ć€‚**[å®¢ęˆ·ē«Æä¼ č¾“](transports.md)** ę¶µē›–äŗ†ęÆäø€ē§ć€‚ diff --git a/i18n/zh/pages/client/index.md b/i18n/zh/pages/client/index.md index f12c6334..aa421db5 100644 --- a/i18n/zh/pages/client/index.md +++ b/i18n/zh/pages/client/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [ebef1e7a0df854f4, 8355cfaf1f76c9d5, 8e79141fc2985342, 46bdb07c7537e8a5, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30] + sections: [ebef1e7a0df854f4, 7e16449f66e7dfd6, eeb0682f7d2a1079, 5713f0196a34e6e7, 0e844597859e4248, 3a97d9195ddcc92e, 1da08c483e59c141, 84702cc6e0a1fd42, 8dee7a31c86ffc5c, 83a5bce168ef23d7] tool: 1 --- # Client {#the-client} @@ -11,13 +11,23 @@ translation: ## ä½ ēš„ē¬¬äø€äøŖå®¢ęˆ·ē«Æ {#your-first-client} -```python title="client.py" hl_lines="14-18" +å®¢ęˆ·ē«Æéœ€č¦äø€äøŖåÆä»„åÆ¹čÆēš„ęœåŠ”å™Øć€‚ęœ¬é”µēš„ęÆę®µä»£ē čæžęŽ„ēš„éƒ½ę˜Æčæ™äøŖ Bookshopć€‚ęŠŠå®ƒäæå­˜äøŗ `server.py`ļ¼Œč®©å®ƒé€ščæ‡ HTTP äø€ē›“čæč”Œē€ļ¼š + +```python title="server.py" --8<-- "docs_src/client/tutorial001.py" ``` -é”¶éƒØēš„ęœåŠ”å™ØåŖę˜Æäøŗäŗ†č®©ä½ ęœ‰äøœč„æåÆčæžć€‚å®¢ęˆ·ē«Æå°±ę˜Æé«˜äŗ®ēš„é‚£äŗ”č”Œć€‚ +```console +uv run mcp run server.py --transport streamable-http +``` -* `Client(mcp)` ęŽ„ę”¶ēš„ę˜Æ**ęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«**ć€‚čæ™ę˜Æå†…å­˜ä¼ č¾“ļ¼šę²”ęœ‰å­čæ›ēØ‹ļ¼Œę²”ęœ‰ē«Æå£ļ¼Œę²”ęœ‰ HTTPć€‚ęœ¬é”µēš„ęÆäøŖē¤ŗä¾‹ļ¼Œä»„åŠä½ å†™ēš„ęÆäøŖęµ‹čÆ•ļ¼Œéƒ½ę˜Æčæ™ę ·čæžęŽ„ēš„ć€‚ +čæ™ę ·å®ƒå°±åœØ `http://localhost:8000/mcp` äøŠęä¾›ęœåŠ”ć€‚å®¢ęˆ·ē«Æę˜Æäø€äøŖē‹¬ē«‹ēš„ēØ‹åŗć€‚ęŠŠå®ƒäæå­˜äøŗ `client.py`ļ¼ŒåœØå¦äø€äøŖē»ˆē«Æé‡Œčæč”Œ `python client.py`: + +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" +``` + +* `Client("http://localhost:8000/mcp")` ęŽ„ę”¶ēš„ę˜Æäø€äøŖ **URL**ļ¼Œę‰€ä»„å®ƒé€ščæ‡ Streamable HTTP čæžęŽ„åˆ°ä½ åˆšåÆåŠØēš„ęœåŠ”å™Øć€‚ * `async with` 就是**ē”Ÿå‘½å‘ØęœŸ**ć€‚čæ›å…„ę—¶čæžęŽ„å¹¶åå•†ļ¼›ē¦»å¼€ę—¶ę–­å¼€ć€‚ę²”ęœ‰ `connect()` / `close()` čæ™ę ·ēš„é…åÆ¹ę–¹ę³•ļ¼Œč€Œäø” `Client` åœØä»£ē å—ē»“ęŸåŽäøčƒ½å¤ē”Øć€‚ * åœØä»£ē å—å†…éƒØļ¼ŒčæžęŽ„ē›øå…³ēš„äæ”ęÆå·²ē»ä½œäøŗę™®é€šå±žę€§ę‘†åœØé‚£é‡Œäŗ†ć€‚ @@ -25,10 +35,10 @@ translation: `Client` ęŽ„ę”¶äø€äøŖä½ē½®å‚ę•°ļ¼Œå¹¶ę ¹ę®å®ƒēš„ē±»åž‹ē”®å®šä¼ č¾“ę–¹å¼ļ¼š -* `MCPServer`ļ¼ˆęˆ–ä½Žå±‚ `Server`ļ¼‰å®žä¾‹ļ¼š**进程内**čæžęŽ„ć€‚ -* URL å­—ē¬¦äø²ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPļ¼Œē”Ÿäŗ§ēŽÆå¢ƒēš„č·Æå¾„ć€‚ -* `StdioServerParameters`:要作为**子进程**åÆåŠØēš„å‘½ä»¤ļ¼Œé€ščæ‡å®ƒēš„ stdin 和 stdout é€šäæ”ć€‚ +* URL å­—ē¬¦äø²ļ¼ˆ`Client("http://localhost:8000/mcp")`ļ¼‰ļ¼šStreamable HTTPļ¼ŒéƒØē½²ę—¶ę‰€ē”Øēš„ä¼ č¾“ę–¹å¼ć€‚ +* `StdioServerParameters`:要作为本地**子进程**åÆåŠØēš„å‘½ä»¤ļ¼Œé€ščæ‡å®ƒēš„ stdin 和 stdout é€šäæ”ć€‚ * **传输**ļ¼šä»»ä½•åÆä»„ `async with ... as (read, write)` ēš„åÆ¹č±”ļ¼ŒęÆ”å¦‚ē”Ø `streamable_http_client(url, http_client=...)` åŒ…č£…ä½ č‡Ŗå·±ēš„ HTTP å®¢ęˆ·ē«Æć€‚ +* `MCPServer`ļ¼ˆęˆ–ä½Žå±‚ `Server`ļ¼‰å®žä¾‹ļ¼š**进程内**čæžęŽ„ļ¼Œę²”ęœ‰å­čæ›ēØ‹ļ¼Œä¹Ÿę²”ęœ‰ē«Æå£ć€‚čæ™äø€ē§ę˜Æē»™ęµ‹čÆ•ē”Øēš„ļ¼Œ**[测试](../get-started/testing.md)** å°±å»ŗē«‹åœØå®ƒä¹‹äøŠć€‚ ęœ¬é”µå…¶ä½™å†…å®¹åœØčæ™å››ē§ę–¹å¼äø‹å®Œå…Øē›øåŒć€‚čÆ·ę±‚å¤“ć€å­čæ›ēØ‹ć€č¶…ę—¶ä»„åŠ `Transport` åč®®å¦ęœ‰äø“é”µļ¼š**[å®¢ęˆ·ē«Æä¼ č¾“](transports.md)**怂 @@ -48,11 +58,11 @@ translation: ## åˆ—å‡ŗå·„å…· {#listing-tools} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial002.py" ``` -`list_tools()` čæ”å›ž `ListToolsResult`ļ¼›å·„å…·åœØ `.tools` é‡Œć€‚ęÆäø€äøŖéƒ½ę˜Æå®æäø»ä¼šäŗ¤ē»™ęØ”åž‹ēš„å®Œę•“å®šä¹‰ļ¼š +`list_tools()` čæ”å›ž `ListToolsResult`ļ¼›å·„å…·åœØ `.tools` é‡Œć€‚ęÆäø€äøŖéƒ½ę˜Æå®æäø»ä¼šäŗ¤ē»™ęØ”åž‹ēš„å®Œę•“å®šä¹‰ć€‚äø‹é¢ę˜Æē¬¬äø€äøŖļ¼š ```python tool.name # 'search_books' @@ -76,6 +86,8 @@ tool.description # 'Search the catalog by title or author.' UI ęø²ęŸ“å‚ę•°č”Øå•ę‰€éœ€ēš„äø€åˆ‡ļ¼Œä»„åŠęØ”åž‹ē”Ÿęˆåˆę³•å‚ę•°ę‰€éœ€ēš„äø€åˆ‡ļ¼Œéƒ½åœØčæ™äøŖęØ”å¼é‡Œć€‚ +ē¬¬äŗŒäøŖå·„å…· `lookup_book` ę³Øå†Œę—¶ę²”ęœ‰ä¼  `title=`ļ¼Œę‰€ä»„å®ƒēš„ `tool.title` 是 `None`怂 + !!! tip `title` ę˜ÆåÆé€‰ēš„ļ¼Œę‰€ä»„ęŠŠå·„å…·å±•ē¤ŗē»™äŗŗēœ‹ēš„ UI åæ…é”»åšé€‰ę‹©ļ¼šęœ‰ `title` å°±ē”Øå®ƒļ¼Œę²”ęœ‰å°±ē”Ø `name`怂`from mcp.shared.metadata_utils import get_display_name` åšēš„ę­£ę˜Æčæ™ä»¶äŗ‹ļ¼Œé€‚ē”ØäŗŽå·„å…·ć€čµ„ęŗć€čµ„ęŗęØ”ęæå’Œęē¤ŗčÆć€‚ @@ -83,7 +95,7 @@ UI ęø²ęŸ“å‚ę•°č”Øå•ę‰€éœ€ēš„äø€åˆ‡ļ¼Œä»„åŠęØ”åž‹ē”Ÿęˆåˆę³•å‚ę•°ę‰€éœ€ēš„ `call_tool(name, arguments)` čæč”Œå·„å…·ļ¼Œčæ”å›ž `CallToolResult`怂 -```python title="client.py" hl_lines="27-34" +```python title="client.py" hl_lines="9-16" --8<-- "docs_src/client/tutorial003.py" ``` @@ -131,7 +143,7 @@ result.is_error # False čµ„ęŗåŠØčÆęˆåÆ¹å‡ŗēŽ°ļ¼šäø¤ē§åˆ—å‡ŗę–¹å¼ļ¼Œäø€ē§čÆ»å–ę–¹å¼ć€‚ -```python title="client.py" hl_lines="22-31" +```python title="client.py" hl_lines="9-18" --8<-- "docs_src/client/tutorial004.py" ``` @@ -145,7 +157,7 @@ result.is_error # False ## ęē¤ŗčÆ {#prompts} -```python title="client.py" hl_lines="15-20" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client/tutorial005.py" ``` @@ -170,7 +182,7 @@ message.content # TextContent(type='text', text='Recommend one poetry book from åø¦ęœ‰č”„å…Øå¤„ē†å‡½ę•°ēš„ęœåŠ”å™ØåÆä»„åœØē”Øęˆ·č¾“å…„ę—¶č‡ŖåŠØč”„å…Øęē¤ŗčÆå’Œčµ„ęŗęØ”ęæēš„å‚ę•°ć€‚ -```python title="client.py" hl_lines="27-31" +```python title="client.py" hl_lines="9-13" --8<-- "docs_src/client/tutorial006.py" ``` @@ -183,21 +195,21 @@ message.content # TextContent(type='text', text='Recommend one poetry book from ęÆäøŖ `list_*` ę–¹ę³•éƒ½ęŽ„ę”¶ `cursor=` å…³é”®å­—å‚ę•°ļ¼ŒęÆäøŖē»“ęžœéƒ½åø¦ `next_cursor`怂`next_cursor` äøŗ `None` ę—¶ļ¼ŒčÆ“ę˜Žå·²ē»ę‹æå…Øäŗ†ć€‚ -```python title="client.py" hl_lines="22-30" +```python title="client.py" hl_lines="7-15" --8<-- "docs_src/client/tutorial007.py" ``` -čæ™äøŖå¾ŖēŽÆåÆ¹ä»»ä½•ęœåŠ”å™Øéƒ½ę­£ē”®ć€‚`MCPServer` äø€é”µčæ”å›žå…ØéƒØå†…å®¹ļ¼Œę‰€ä»„ `next_cursor` 是 `None`ļ¼Œå¾ŖēŽÆåŖč·‘äø€ę¬”ļ¼Œčæ™ä¹Ÿę˜Æäøŗä»€ä¹ˆå¤§å¤šę•°ä»£ē ä»Žę„äøå†™å®ƒć€‚ēœŸę­£åˆ†é”µēš„ęœåŠ”å™Øļ¼Œä»„åŠęøøę ‡éµå®ˆēš„č§„åˆ™ļ¼Œč§ **[分锵](../advanced/pagination.md)**怂 +`list_all_tools` åÆ¹ä»»ä½•ęœåŠ”å™Øéƒ½ę­£ē”®ć€‚`MCPServer` äø€é”µčæ”å›žå…ØéƒØå†…å®¹ļ¼Œę‰€ä»„ `next_cursor` 是 `None`ļ¼Œå¾ŖēŽÆåŖč·‘äø€ę¬”ļ¼Œčæ™ä¹Ÿę˜Æäøŗä»€ä¹ˆå¤§å¤šę•°ä»£ē ä»Žę„äøå†™å®ƒć€‚ēœŸę­£åˆ†é”µēš„ęœåŠ”å™Øļ¼Œä»„åŠęøøę ‡éµå®ˆēš„č§„åˆ™ļ¼Œč§ **[分锵](../advanced/pagination.md)**怂 ## åœØęµ‹čÆ•äø­ {#in-tests} -ę²”ęœ‰čæ›ēØ‹ć€ę²”ęœ‰ē«Æå£ēš„ `Client(mcp)`ļ¼Œęœ¬čŗ«å°±ę˜ÆęœåŠ”å™Øēš„ęµ‹čÆ•å·„å…·ć€‚ +ęœ¬é”µēš„ęÆäøŖ `client.py` éƒ½ę˜Æé€ščæ‡ HTTP 访问 `server.py` ēš„ć€‚åœØęµ‹čÆ•é‡ŒåÆä»„č·³čæ‡ē½‘ē»œļ¼ŒęŠŠęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«äŗ¤ē»™ `Client`ļ¼šå…ˆ `from server import mcp`ļ¼Œå† `Client(mcp)`ć€‚ę²”ęœ‰čæ›ēØ‹ļ¼Œę²”ęœ‰ē«Æå£ļ¼ŒäøŠé¢ēš„ęÆäøŖę–¹ę³•ē”Øę³•éƒ½äø€ę ·ć€‚ -ęœ‰äø€äøŖęž„é€ å‚ę•°äø“äøŗę­¤č€Œč®¾ļ¼š`Client(mcp, raise_exceptions=True)`ć€‚å®ƒåŖåÆ¹å†…å­˜čæžęŽ„ē”Ÿę•ˆļ¼Œ**[测试](../get-started/testing.md)** é”µé¢ä¼šč§£é‡Šå®ƒļ¼Œå¹¶å›“ē»•å®ƒę­å»ŗå®Œę•“ēš„ęØ”å¼ć€‚ +ęœ‰äø€äøŖęž„é€ å‚ę•°äø“äøŗę­¤č€Œč®¾ļ¼š`Client(mcp, raise_exceptions=True)`ć€‚å®ƒåŖåÆ¹čæ›ēØ‹å†…čæžęŽ„ē”Ÿę•ˆļ¼Œ**[测试](../get-started/testing.md)** é”µé¢ä¼šč§£é‡Šå®ƒļ¼Œå¹¶å›“ē»•å®ƒę­å»ŗå®Œę•“ēš„ęØ”å¼ć€‚ ## 回锾 {#recap} -* `Client(x)` ä¼ å…„ęœåŠ”å™ØåÆ¹č±”ę—¶čµ°å†…å­˜čæžęŽ„ļ¼Œä¼ å…„ URL 字符串时走 Streamable HTTPļ¼Œå…¶ä»–ęƒ…å†µé€ščæ‡ä¼ č¾“čæžęŽ„ć€‚ +* `Client(x)` ä¼ å…„ URL 字符串时走 Streamable HTTPļ¼Œä¼ å…„ `StdioServerParameters` ę—¶åÆåŠØå­čæ›ēØ‹ļ¼Œä¼ å…„ä¼ č¾“ę—¶ē›“ęŽ„čæ›å…„å®ƒļ¼ŒåœØęµ‹čÆ•é‡Œåˆ™ęŽ„ę”¶ęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«ć€‚ * `async with` å°±ę˜Æå…ØéƒØē”Ÿå‘½å‘ØęœŸć€‚åœØå®ƒå†…éƒØļ¼Œ`server_capabilities` 和 `protocol_version` å·²ē»å”«å„½ļ¼›ęœåŠ”å™Øęä¾›ę—¶ļ¼Œ`server_info` 和 `instructions` ä¹Ÿå·²å”«å„½ć€‚ * `list_tools()` ē»™å‡ŗęÆäøŖå·„å…·ēš„ `name`态`title`态`description` 和 `input_schema`怂 * `call_tool()` čæ”å›žē»™ęØ”åž‹ēš„ `content`ć€ē»™ä»£ē ēš„ `structured_content`ļ¼Œä»„åŠ `is_error`ć€‚ęŠ›å¼‚åøøēš„å·„å…·ę˜Æäø€äøŖē»“ęžœļ¼Œäøę˜Æå¼‚åøøć€‚ diff --git a/i18n/zh/pages/client/oauth-clients.md b/i18n/zh/pages/client/oauth-clients.md index c57558d3..3efc0075 100644 --- a/i18n/zh/pages/client/oauth-clients.md +++ b/i18n/zh/pages/client/oauth-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b] + sections: [c6899d3892bd9fa0, 79372cff3cc48a88, c2dae1ebe2ebd543, 13175843d3588af4, df06056fb16b3846, 758f06399b513c1f, a05d7278487d610b] tool: 1 --- # OAuth 客户端 {#oauth-clients} @@ -76,18 +76,20 @@ translation: `Client` ē¬¬äø€ę¬”å‘é€čÆ·ę±‚ę—¶ļ¼ŒęœåŠ”å™Øå›žē­” `401`ć€‚ęä¾›č€…ęŽ„ę‰‹ļ¼š -1. **å‘ēŽ°ć€‚** å®ƒčÆ»å– `WWW-Authenticate` å¤“ļ¼Œä»Ž `/.well-known/oauth-protected-resource` čŽ·å–ęœåŠ”å™Øēš„å—äæęŠ¤čµ„ęŗå…ƒę•°ę®ļ¼Œå¾—ēŸ„ę˜Æå“ŖäøŖęŽˆęƒęœåŠ”å™ØåœØäæęŠ¤čæ™äøŖčµ„ęŗļ¼Œå†åŽ»čŽ·å–**那个**ęœåŠ”å™Øēš„å…ƒę•°ę®ć€‚ +1. **å‘ēŽ°ć€‚** å®ƒčÆ»å– `WWW-Authenticate` å¤“ļ¼Œä»Ž `/.well-known/oauth-protected-resource` čŽ·å–ęœåŠ”å™Øēš„å—äæęŠ¤čµ„ęŗå…ƒę•°ę®ļ¼Œå¾—ēŸ„ę˜Æå“ŖäøŖęŽˆęƒęœåŠ”å™ØåœØäæęŠ¤čæ™äøŖčµ„ęŗļ¼Œå†åŽ»čŽ·å–**那个**ęœåŠ”å™Øēš„å…ƒę•°ę®ć€‚ļ¼ˆč¾ƒę—§ēš„ęœåŠ”å™Øå¦‚ęžœę²”ęœ‰å‘åøƒčµ„ęŗå…ƒę•°ę®ļ¼Œåˆ™ę”¹äøŗåœØå®ƒč‡Ŗå·±ēš„ęŗäøŠčÆ·ę±‚ęŽˆęƒęœåŠ”å™Øå…ƒę•°ę®ć€‚ļ¼‰ę— č®ŗå“Ŗē§ę–¹å¼ļ¼Œå…ƒę•°ę®é‡Œēš„ `issuer` éƒ½åæ…é”»ę­£ę˜ÆčŽ·å–å®ƒę‰€é’ˆåÆ¹ēš„é‚£äøŖęœåŠ”å™Øļ¼›å…¶ä»–ä»»ä½•å€¼äø€å¾‹ę‹’ē»ć€‚ 2. **ę³Øå†Œć€‚** å­˜å‚Øé‡Œä»€ä¹ˆéƒ½ę²”ęœ‰ļ¼Ÿå®ƒē”Øä½ ēš„ `OAuthClientMetadata` åŠØę€ę³Øå†Œļ¼Œå¹¶ęŠŠē»“ęžœå­˜čµ·ę„ć€‚ 3. **ꎈꝃ怂** å®ƒē”Ÿęˆ PKCE åÆ¹å’Œäø€äøŖ `state`ļ¼Œęž„å»ŗęŽˆęƒ URL,await ä½ ēš„ `redirect_handler`ļ¼Œē„¶åŽ await ä½ ēš„ `callback_handler` ę‹æåˆ°ęŽˆęƒē ć€‚ 4. **äŗ¤ę¢ć€‚** å®ƒē”ØęŽˆęƒē ę¢ę„ `OAuthToken`ļ¼Œå­˜čµ·ę„ļ¼Œå†åø¦äøŠ `Authorization: Bearer ...` é‡ę”¾ä½ ęœ€åˆēš„čÆ·ę±‚ć€‚ ä¹‹åŽå®ƒå°±å®‰é™äŗ†ć€‚ä»¤ē‰Œä»Žå­˜å‚Øé‡Œå–å‡ŗļ¼Œčæ‡ęœŸēš„č®æé—®ä»¤ē‰Œē”Øåˆ·ę–°ä»¤ē‰Œåˆ·ę–°ļ¼ŒåŖęœ‰čæ™äŗ›éƒ½č”Œäøé€šę—¶ę‰ä¼šé‡ę–°č·‘äø€éęµēØ‹ć€‚ +ęœ‰äø€ę”ä¼ č¾“č§„åˆ™é€‚ē”ØäŗŽę‰€ęœ‰čæ™äŗ›čÆ·ę±‚ļ¼šå’Œå®ƒä»¬ę‰€åµŒå„—ēš„é‚£äøŖ MCP čÆ·ę±‚äø€ę ·ļ¼ŒåŖęœ‰å½“é‡å®šå‘ē•™åœØåŒäø€äøŖęŗå¹¶äø”äæęŒčÆ·ę±‚ę–¹ę³•äøå˜ę—¶ļ¼ˆęÆ”å¦‚č”„å°¾éƒØę–œę ēš„ 307/308ļ¼‰ę‰ä¼šč·Ÿéšļ¼Œå…¶ä»–ä»»ä½•é‡å®šå‘éƒ½č§†äøŗčÆ„ URL ę²”ęœ‰åŗ”ē­”ć€‚ + čæ™äŗ›ä½ äø€č”Œéƒ½ę²”å†™ć€‚čæ˜å‰©äø¤äøŖå…³é”®å­—å‚ę•°ļ¼ˆ`client_metadata_url` 和 `validate_resource_url`ļ¼‰ļ¼Œčæ™äøŖę–‡ä»¶éƒ½ē”Øäøåˆ°ć€‚å€¼å¾—äŗ†č§£ēš„ę˜Æ `client_metadata_url`ļ¼Œäø‹é¢å•ē‹¬ęœ‰äø€čŠ‚č®²å®ƒć€‚ ### 试一试 {#try-it} -čæ™ä»½ę–‡ę”£é‡Œēš„å¤§å¤šę•°ē¤ŗä¾‹éƒ½åÆä»„ē”Øå†…å­˜äø­ēš„ `Client(server)` éŖŒčÆć€‚čæ™äøŖäøč”Œļ¼šę•“äøŖęµēØ‹ēš„ę øåæƒå°±ę˜Æäø€äøŖ HTTP `401`ļ¼Œč€Œå†…å­˜äø­ēš„å®¢ęˆ·ē«Æå’Œå®ƒēš„ęœåŠ”å™Øä¹‹é—“ę²”ęœ‰ HTTP怂 +ęµ‹čÆ•é‡Œē”Øēš„å†…å­˜äø­ēš„ `Client(server)` åœØčæ™é‡Œåø®äøäøŠåæ™ļ¼šę•“äøŖęµēØ‹ēš„ę øåæƒå°±ę˜Æäø€äøŖ HTTP `401`ļ¼Œč€Œå†…å­˜äø­ēš„å®¢ęˆ·ē«Æå’Œå®ƒēš„ęœåŠ”å™Øä¹‹é—“ę²”ęœ‰ HTTP怂 ä»“åŗ“é‡Œé™„åø¦äŗ†åÆå®žé™…čæč”Œēš„ē‰ˆęœ¬ć€‚`examples/servers/simple-auth/` čæč”Œäø€äøŖē‹¬ē«‹ēš„ęŽˆęƒęœåŠ”å™Øå’Œäø€äøŖå—äæęŠ¤ēš„ MCP ęœåŠ”å™Øļ¼›`examples/clients/simple-auth-client/` ę˜Æęœ¬é”µēš„å®¢ęˆ·ē«Æę‰©å±•ęˆēš„äø€äøŖå° CLIć€‚å®ƒēš„ README é‡Œęœ‰äø¤ę”å‘½ä»¤ļ¼šåÆåŠØęœåŠ”å™Øļ¼ŒåÆ¹ē€å®ƒä»¬čæč”Œå®¢ęˆ·ē«Æļ¼Œå°±čƒ½ēœ‹åˆ°čæ™å››äøŖę­„éŖ¤ä¾ę¬”čµ°čæ‡ć€‚ @@ -105,13 +107,14 @@ URL åæ…é”»ę˜Æ HTTPS äø”č·Æå¾„äøčƒ½ę˜Æę ¹č·Æå¾„ļ¼›å¦åˆ™åœØęž„é€ ę—¶å°±ę˜Æ `Valu `ClientCredentialsOAuthProvider` ę˜ÆåŒäø€äøŖ `httpx2.Auth`ļ¼ŒåŖę˜ÆåŽ»ęŽ‰äŗ†äŗŗļ¼š -```python title="client.py" hl_lines="4 27-33" +```python title="client.py" hl_lines="4 27-34" --8<-- "docs_src/oauth_clients/tutorial002.py" ``` å˜äŗ†ä»€ä¹ˆļ¼š * ę²”ęœ‰ `OAuthClientMetadata`ļ¼Œę²”ęœ‰å¤„ē†å‡½ę•°ć€‚ä¼ å…„ `client_id` 和 `client_secret`ļ¼›ęä¾›č€…å›“ē»•å®ƒä»¬ęž„å»ŗäø€äøŖęœ€å°ēš„ `client_credentials` ę³Øå†Œļ¼Œå®Œå…Øč·³čæ‡åŠØę€ę³Øå†Œć€‚ +* `issuer` ęŒ‡ę˜Žé¢å‘čæ™äŗ›å‡­ę®ēš„ęŽˆęƒęœåŠ”å™Øļ¼›ä½æē”Øå®ƒēš„ `/.well-known/oauth-authorization-server` ę–‡ę”£čæ”å›žēš„ `issuer` å€¼ć€‚å‘ēŽ°ä»ęŒ‰äøŠé¢ēš„ę–¹å¼čæ›č”Œļ¼Œä½†ä»¤ē‰ŒčÆ·ę±‚åŖä¼šåŸŗäŗŽ**那个**é¢å‘č€…ēš„å…ƒę•°ę®ęž„å»ŗļ¼›å¦‚ęžœ MCP ęœåŠ”å™ØęŒ‡å‘åˆ«å¤„ļ¼ŒęµēØ‹ä¼šä»„ `OAuthFlowError` äø­ę­¢ć€‚ēœē•„å®ƒå·²å¼ƒē”Øļ¼Œåˆ° 3.0 ä¼šå˜äøŗåæ…å”«ļ¼ˆč§ **[å·²å¼ƒē”Øēš„åŠŸčƒ½](../deprecated.md#deprecated-sdk-helpers)**ļ¼‰ļ¼›åœØę­¤ä¹‹å‰ļ¼Œęä¾›č€…ä¼šå‘å‡ŗč­¦å‘Šļ¼Œå¹¶ä½æē”Øå‘ēŽ°ę‰¾åˆ°ēš„é‚£äøŖęŽˆęƒęœåŠ”å™Øć€‚ * `scope` ę˜Æē©ŗę ¼åˆ†éš”ēš„å­—ē¬¦äø²ļ¼Œå³ OAuth ēš„ēŗæč·Æę ¼å¼ć€‚ * äø‹ęøøēš„äø€åˆ‡å®Œå…Øē›øåŒļ¼šåŒę ·ēš„ `TokenStorage`ć€åŒę ·ēš„ `httpx2.AsyncClient(auth=...)`ć€åŒę ·ēš„ `streamable_http_client`怂 @@ -121,7 +124,7 @@ URL åæ…é”»ę˜Æ HTTPS äø”č·Æå¾„äøčƒ½ę˜Æę ¹č·Æå¾„ļ¼›å¦åˆ™åœØęž„é€ ę—¶å°±ę˜Æ `Valu ä»ŽēŽÆå¢ƒå˜é‡ęˆ–åÆ†é’„ē®”ē†å™ØčÆ»å– `client_secret`ļ¼Œē»äøč¦ä»Žęŗē ē‰ˆęœ¬ęŽ§åˆ¶é‡ŒčÆ»ć€‚ !!! info - `mcp.client.auth.extensions.client_credentials` é‡Œčæ˜ęœ‰äø€äøŖęä¾›č€…ļ¼š**`PrivateKeyJWTOAuthProvider`**ļ¼Œē”ØäŗŽä»„ JWT č€Œéžå…±äŗ«åÆ†é’„čæ›č”Œč®¤čÆēš„å®¢ęˆ·ē«Æļ¼ˆ`private_key_jwt`ļ¼Œå³åÆ†é’„åÆ¹å’Œå·„ä½œč“Ÿč½½čŗ«ä»½é‚£äø€ē±»ļ¼‰ć€‚å®ƒéµå¾ŖåŒę ·ēš„ęØ”å¼ļ¼šęž„é€ äø€äøŖļ¼Œę”¾åˆ° `auth=` äøŠć€‚åŒäø€äøŖęØ”å—čæ˜é™„åø¦ `SignedJWTParameters` 和 `static_assertion_provider`ļ¼Œäø¤äøŖē”Øę„ęž„å»ŗå…¶ę–­čØ€ēš„č¾…åŠ©å·„å…·ć€‚ + `mcp.client.auth.extensions.client_credentials` é‡Œčæ˜ęœ‰äø€äøŖęä¾›č€…ļ¼š**`PrivateKeyJWTOAuthProvider`**ļ¼Œē”ØäŗŽä»„ JWT č€Œéžå…±äŗ«åÆ†é’„čæ›č”Œč®¤čÆēš„å®¢ęˆ·ē«Æļ¼ˆ`private_key_jwt`ļ¼Œå³åÆ†é’„åÆ¹å’Œå·„ä½œč“Ÿč½½čŗ«ä»½é‚£äø€ē±»ļ¼‰ć€‚å®ƒéµå¾ŖåŒę ·ēš„ęØ”å¼ļ¼šęž„é€ äø€äøŖļ¼ˆå®ƒęŽ„å—åŒę ·åÆé€‰ēš„ `issuer`ļ¼‰ļ¼Œę”¾åˆ° `auth=` äøŠć€‚åŒäø€äøŖęØ”å—čæ˜é™„åø¦ `SignedJWTParameters` 和 `static_assertion_provider`ļ¼Œäø¤äøŖē”Øę„ęž„å»ŗå…¶ę–­čØ€ēš„č¾…åŠ©å·„å…·ć€‚ čæ˜ęœ‰äø€ē§ę²”ęœ‰äŗŗå‚äøŽēš„ęƒ…å½¢ļ¼šå®¢ęˆ·ē«Æå±žäŗŽęŸäøŖä¼äøšļ¼Œē”±ä¼äøšēš„čŗ«ä»½ęä¾›č€…č€Œäøę˜Æē”Øęˆ·ę„å†³å®šå®ƒåÆä»„č®æé—®å“Ŗäŗ› MCP ęœåŠ”å™Øć€‚é‚£ę˜Æå¦äø€ē§ęŽˆęƒę–¹å¼ļ¼Œęœ‰č‡Ŗå·±ēš„äæ”ä»»ęØ”åž‹å’Œč‡Ŗå·±ēš„é”µé¢ļ¼Œ**[身份断言](identity-assertion.md)**怂 diff --git a/i18n/zh/pages/client/session-groups.md b/i18n/zh/pages/client/session-groups.md index 1f68793f..1a5ecb7c 100644 --- a/i18n/zh/pages/client/session-groups.md +++ b/i18n/zh/pages/client/session-groups.md @@ -1,6 +1,6 @@ --- translation: - sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73] + sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, ed4d17e894864056] tool: 1 --- # ä¼ščÆē»„ {#session-groups} @@ -76,7 +76,7 @@ translation: ## 回锾 {#recap} * `ClientSessionGroup` ęŒęœ‰å¤šę”ęœåŠ”å™ØčæžęŽ„ļ¼Œå¹¶ęŠŠå®ƒä»¬ēš„å·„å…·ć€čµ„ęŗå’Œęē¤ŗčÆå„č‡Ŗåˆå¹¶ęˆäø€äøŖ `dict`怂 -* ęÆäøŖęœåŠ”å™Øč°ƒē”Øäø€ę¬” `connect_to_server(params)`ć€‚å®ƒęŽ„å—ä¼ č¾“å‚ę•°ļ¼Œä»ŽäøęŽ„å— `Client` ę‰€ęŽ„å—ēš„ęœåŠ”å™ØåÆ¹č±”ęˆ– URL怂 +* ęÆäøŖęœåŠ”å™Øč°ƒē”Øäø€ę¬” `connect_to_server(params)`ć€‚å®ƒęŽ„å—ēš„ę˜Æä¼ č¾“å‚ę•°ļ¼Œä»ŽäøęŽ„å— `Client` ę‰€ęŽ„å—ēš„ URL ꈖ `Transport`怂 * `group.call_tool(name, arguments)` ę›æä½ č·Æē”±åˆ°ę‹„ęœ‰čÆ„å·„å…·ēš„ęœåŠ”å™Øć€‚ * åē§°åæ…é”»åœØę•“äøŖē»„å†…å”Æäø€ļ¼›äø¤äøŖéƒ½ęœ‰ `search` å·„å…·ēš„ęœåŠ”å™Øę— ę³•ē›“ęŽ„å…±å­˜ć€‚ * `component_name_hook=` ę”¹å†™ęÆäøŖę³Øå†Œēš„åē§°ć€‚ę”¹å˜ēš„ę˜Æ dict ēš„é”®ļ¼Œēŗæč·ÆäøŠēš„åē§°äøå˜ć€‚ diff --git a/i18n/zh/pages/client/transports.md b/i18n/zh/pages/client/transports.md index ecfd8ac8..d505e945 100644 --- a/i18n/zh/pages/client/transports.md +++ b/i18n/zh/pages/client/transports.md @@ -1,6 +1,6 @@ --- translation: - sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, 40b4916d82eaf1d4, 3d0832f39b0d7059, dfa4446556badef0, 5bd93be2ab2ecb9c] + sections: [9cac816674181eb0, 7c157764133fea1f, 40b4916d82eaf1d4, 10d151f2cc75317f, 3d0832f39b0d7059, 92742ba36533633d, 0aeca6145e7bd302] tool: 1 --- # å®¢ęˆ·ē«Æä¼ č¾“ {#client-transports} @@ -11,30 +11,15 @@ translation: ęÆē§ä¼ č¾“ēš„**ęœåŠ”å™Ø**äø€ä¾§ļ¼ˆ`mcp.run()` åšä»€ä¹ˆć€ä½ éƒØē½²ä»€ä¹ˆļ¼‰č§ **[čæč”Œä½ ēš„ęœåŠ”å™Ø](../run/index.md)**怂 -## å†…å­˜äø­ {#in-memory} - -ä¼ å…„ęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«ļ¼š - -```python title="client.py" hl_lines="14" ---8<-- "docs_src/client_transports/tutorial001.py" -``` - -ę²”ęœ‰å­čæ›ēØ‹ļ¼Œę²”ęœ‰ē«Æå£ļ¼Œēŗæč·ÆäøŠę²”ęœ‰ä»»ä½•å­—čŠ‚ć€‚å®¢ęˆ·ē«Æå’ŒęœåŠ”å™Øę˜ÆåŒäø€äøŖčæ›ēØ‹é‡Œēš„äø¤äøŖåÆ¹č±”ļ¼Œč€Œč°ƒē”Øä»ē„¶čµ°ēœŸå®žēš„åč®®å±‚ļ¼š`search_books` ēš„åˆ—å‡ŗć€ę ”éŖŒå’Œč°ƒē”Øļ¼Œå’Œčµ° HTTP ę—¶å®Œå…Øäø€ę ·ć€‚ - -čæ™č®©å®ƒåŒę—¶å…·ęœ‰äø¤ē§ē”Øé€”ļ¼š - -* **ęµ‹čÆ•ę”Æęž¶ć€‚** ęœ¬ę–‡ę”£äø­ēš„ęÆäøŖē¤ŗä¾‹éƒ½ę˜Æčæ™ę ·č·‘é€šēš„ļ¼Œ**[测试](../get-started/testing.md)** é”µé¢å›“ē»•å®ƒęž„å»ŗäŗ†ę•“å„—ęØ”å¼ć€‚ -* **嵌兄 API怂** č‡Ŗå·±ęž„é€ ęœåŠ”å™Øēš„åŗ”ē”Øäøéœ€č¦ē»čæ‡ē½‘ē»œå°±čƒ½č°ƒē”Øå®ƒēš„å·„å…·ć€‚ - ## Streamable HTTP {#streamable-http} -传兄一个 URL å­—ē¬¦äø²ļ¼Œå¾—åˆ°ēš„å°±ę˜Æ **Streamable HTTP**ļ¼Œä¹Ÿå°±ę˜ÆéƒØē½²ę—¶ē”Øēš„ä¼ č¾“ę–¹å¼ļ¼š +传兄一个 URL å­—ē¬¦äø²ļ¼Œå¾—åˆ°ēš„å°±ę˜Æ **Streamable HTTP**ļ¼šéƒØē½²ę—¶ē”Øēš„ä¼ č¾“ę–¹å¼ļ¼Œä¹Ÿę˜Æåŗ”čÆ„é¦–é€‰ēš„é‚£äø€ē§ļ¼š ```python title="client.py" hl_lines="5" --8<-- "docs_src/client_transports/tutorial002.py" ``` -čæ™å°±ę˜Æå®Œę•“ēš„ē”Ÿäŗ§ēŽÆå¢ƒå®¢ęˆ·ē«Æć€‚`Client` ę›æä½ ęŠŠ URL åŒ…čæ› `streamable_http_client(...)`ļ¼Œåŗ•å±‚ę˜Æäø€äøŖęŒ‰ MCP ēš„éœ€č¦é…ē½®å„½ēš„ `httpx2.AsyncClient`:`follow_redirects=True`,connect/write/pool č¶…ę—¶ 30 ē§’ļ¼ŒčÆ»č¶…ę—¶ 300 ē§’ļ¼Œå› äøŗęœåŠ”å™ØåÆčƒ½ä¼šäø€ē›“äæęŒå“åŗ”ęµę‰“å¼€ć€‚ +čæ™å°±ę˜Æå®Œę•“ēš„ē”Ÿäŗ§ēŽÆå¢ƒå®¢ęˆ·ē«Æć€‚`Client` ę›æä½ ęŠŠ URL åŒ…čæ› `streamable_http_client(...)`ļ¼Œåŗ•å±‚ę˜Æäø€äøŖęŒ‰ MCP ēš„éœ€č¦é…ē½®å„½ēš„ `httpx2.AsyncClient`:connect/write/pool č¶…ę—¶ 30 ē§’ļ¼ŒčÆ»č¶…ę—¶ 300 ē§’ļ¼Œå› äøŗęœåŠ”å™ØåÆčƒ½ä¼šäø€ē›“äæęŒå“åŗ”ęµę‰“å¼€ć€‚ !!! check ęž„é€ å‡ŗę„ēš„ `Client` **并未**čæžęŽ„ć€‚ęž„é€ åŖę˜Æé€‰å®šä¼ č¾“ę–¹å¼ļ¼›ę‰“å¼€å®ƒēš„ę˜Æ `async with`ć€‚åœØčæ›å…„ä¹‹å‰å°±åŽ»å–čæžęŽ„ļ¼ŒSDK ä¼šę˜Žē”®å‘ŠčÆ‰ä½ ļ¼š @@ -49,7 +34,7 @@ translation: äø€ę—¦éœ€č¦ `Authorization` 夓、cookie、代理、mTLS ęˆ–äøåŒēš„č¶…ę—¶ļ¼Œå°±č‡Ŗå·±ęž„å»ŗ `httpx2.AsyncClient`ļ¼Œå†ęŠŠå®ƒäŗ¤ē»™ `streamable_http_client`: -```python title="client.py" hl_lines="8-14" +```python title="client.py" hl_lines="8-13" --8<-- "docs_src/client_transports/tutorial003.py" ``` @@ -58,10 +43,7 @@ translation: * `httpx2.AsyncClient` å½’ä½ ę‰€ęœ‰ļ¼Œę‰€ä»„ē”±**ä½ **čæ›å…„å’Œé€€å‡ŗå®ƒć€‚SDK ä»Žäøå…³é—­äøę˜Æå®ƒč‡Ŗå·±åˆ›å»ŗēš„å®¢ęˆ·ē«Æć€‚ * `streamable_http_client(url, http_client=...)` čæ”å›žäø€äøŖä¼ č¾“ļ¼Œ`Client(transport)` åƒęŽ„å—å…¶ä»–ä»»ä½•äøœč„æäø€ę ·ęŽ„å—å®ƒć€‚ -å…³äŗŽ TLS ēš„äø€ē‚¹čÆ“ę˜Žļ¼š`httpx2` ä¾ę®ę“ä½œē³»ē»Ÿēš„äæ”ä»»åŗ“ļ¼ˆé€ščæ‡ -[`truststore`](https://pypi.org/project/truststore/)ļ¼‰ę ”éŖŒčÆä¹¦ļ¼Œč€Œäøę˜Æč‡Ŗåø¦ēš„ CA åˆ—č”Øć€‚åœØę²”ęœ‰åÆē”Øē³»ē»Ÿ CA åŗ“ēš„ēŽÆå¢ƒļ¼ˆęŸäŗ›ē²¾ē®€å®¹å™Øļ¼‰äø­ļ¼Œč®¾ē½®ę ‡å‡†ēš„ `SSL_CERT_FILE`/`SSL_CERT_DIR` -ēŽÆå¢ƒå˜é‡ļ¼Œęˆ–č€…ē»™ä½ ēš„ `httpx2.AsyncClient` ę˜¾å¼ä¼ å…„ `verify=ssl_context`ļ¼ˆčƒŒę™Æč§ -[`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。 +å…³äŗŽ TLS ēš„äø€ē‚¹čÆ“ę˜Žļ¼š`httpx2` ä¾ę®ę“ä½œē³»ē»Ÿēš„äæ”ä»»åŗ“ļ¼ˆé€ščæ‡ [`truststore`](https://pypi.org/project/truststore/)ļ¼‰ę ”éŖŒčÆä¹¦ļ¼Œč€Œäøę˜Æč‡Ŗåø¦ēš„ CA åˆ—č”Øć€‚åœØę²”ęœ‰åÆē”Øē³»ē»Ÿ CA åŗ“ēš„ēŽÆå¢ƒļ¼ˆęŸäŗ›ē²¾ē®€å®¹å™Øļ¼‰äø­ļ¼Œč®¾ē½®ę ‡å‡†ēš„ `SSL_CERT_FILE`/`SSL_CERT_DIR` ēŽÆå¢ƒå˜é‡ļ¼Œęˆ–č€…ē»™ä½ ēš„ `httpx2.AsyncClient` ę˜¾å¼ä¼ å…„ `verify=ssl_context`ļ¼ˆčƒŒę™Æč§ [`httpx` 和 `httpx-sse` 被 `httpx2` 取代](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2))。 !!! warning `streamable_http_client` čæ‡åŽ»åÆä»„ē›“ęŽ„ęŽ„å— `headers=` 和 `timeout=`ć€‚ēŽ°åœØäøč”Œäŗ†ļ¼šå®ƒåŖęœ‰ `url`态`http_client` 和 `terminate_on_close` äø‰äøŖå‚ę•°ć€‚ä¹ ęƒÆę€§åœ°åŽ»ē”Ø `headers=`ļ¼Œä¼šå¾—åˆ°ļ¼š @@ -73,7 +55,25 @@ translation: ꉀ꜉ HTTP å±‚é¢ēš„äøœč„æēŽ°åœØéƒ½ę”¾åœØä½ ä¼ å…„ēš„é‚£äø€äøŖ `httpx2.AsyncClient` äøŠć€‚ !!! info - `httpx2` äæē•™äŗ†ē†Ÿę‚‰ēš„ `httpx` APIļ¼Œę‰€ä»„åŖč¦ä¼š `httpx`ļ¼Œå°±å·²ē»ēŸ„é“åœØčæ™é‡Œę€Žä¹ˆåšč®¤čÆć€ä»£ē†ć€äŗ‹ä»¶é’©å­ć€é‡čÆ•å’ŒčæžęŽ„é™åˆ¶ć€‚SDK ę—¢äøåœØäøŠé¢åŠ äøœč„æļ¼Œä¹Ÿäøę‹æčµ°ä»€ä¹ˆć€‚OAuth ä¹Ÿę˜ÆåœØčæ™é‡ŒęŽ„å…„ēš„ļ¼š`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。敓个流程见 **[OAuth 客户端](oauth-clients.md)**怂 + `httpx2` äæē•™äŗ†ē†Ÿę‚‰ēš„ `httpx` APIļ¼Œę‰€ä»„åŖč¦ä¼š `httpx`ļ¼Œå°±å·²ē»ēŸ„é“åœØčæ™é‡Œę€Žä¹ˆåšč®¤čÆć€ä»£ē†ć€äŗ‹ä»¶é’©å­ć€é‡čÆ•å’ŒčæžęŽ„é™åˆ¶ć€‚SDK ę—¢äøåœØäøŠé¢åŠ äøœč„æļ¼Œä¹Ÿäøę‹æčµ°ä»€ä¹ˆļ¼Œ[é‡å®šå‘å¤„ē†](#redirects)除外。OAuth ä¹Ÿę˜ÆåœØčæ™é‡ŒęŽ„å…„ēš„ļ¼š`httpx2.AsyncClient(auth=OAuthClientProvider(...))`。敓个流程见 **[OAuth 客户端](oauth-clients.md)**怂 + +### 重定向 {#redirects} + +ä¼ č¾“åŖčæžęŽ„ä½ ē»™å®ƒēš„é‚£äøŖ URLļ¼Œč€Œäø”åŖčæžęŽ„é‚£äøŖęŗļ¼ˆorigin)。 + +* åœē•™åœØåŒäø€ schemeć€äø»ęœŗå’Œē«Æå£äøŠēš„ `307`/`308` é‡å®šå‘ä¼šč¢«č·Ÿéšļ¼ŒåŒäø€äø»ęœŗäøŠēš„ `http://` → `https://` ä¹Ÿä¼šć€‚čæ™č¦†ē›–äŗ†åøøč§ēš„ `/mcp` → `/mcp/` å°¾éƒØę–œę é‡å®šå‘ć€‚ +* ęŒ‡å‘å…¶ä»–ä»»ä½•åœ°ę–¹ēš„é‡å®šå‘éƒ½**äøä¼š**č¢«č·Ÿéšć€‚č°ƒē”Øä¼šå¤±č“„å¹¶ęŠ„é”™ļ¼š + + ```text + MCPError: Redirect to https://other.example.com/mcp not followed; use that URL as the endpoint if it is the intended server + ``` + + å¦‚ęžœé‚£äøŖ URL å°±ę˜Æä½ č¦čæžēš„ęœåŠ”å™Øļ¼ŒęŠŠå®ƒå†™čæ›é…ē½®ć€‚å¦‚ęžœäøę˜Æļ¼ŒčÆ“ę˜ŽęœåŠ”å™Øęˆ–å®ƒå‰é¢ēš„ä»£ē†é…ē½®ęœ‰čÆÆć€‚ + +čæ™åÆ¹ä½ ä¼ å…„ēš„ä»»ä½• `httpx2.AsyncClient` éƒ½ęˆē«‹ļ¼šMCP čÆ·ę±‚äøä¼šå‚č€ƒå®ƒēš„ `follow_redirects` č®¾ē½®ļ¼Œę— č®ŗč®¾ęˆå“Ŗäø€ē§ć€‚SDK ēš„ OAuth provider åÆ¹å®ƒä»¬č‡Ŗå·±ēš„čÆ·ę±‚ä¹Ÿåŗ”ē”ØåŒę ·ēš„č§„åˆ™ć€‚ + +!!! tip + `Redirect to http://… not followed: it would downgrade this HTTPS endpoint to plain HTTP` ēš„ę„ę€ę˜Æļ¼šęœåŠ”å™Øå‰é¢ęœ‰äø€äøŖå®ƒäøēŸ„é“ēš„ TLS ē»ˆę­¢ä»£ē†ļ¼ŒäŗŽę˜Æå®ƒå‘å‡ŗēš„ę˜Æ `http://` é‡å®šå‘ć€‚čæ™č¦åœØęœåŠ”å™Øē«Æäæ®å¤ļ¼ˆ**[éƒØē½²äøŽę‰©å±•](../run/deploy.md#behind-a-tls-terminating-proxy)**ļ¼‰ļ¼Œęˆ–č€…ē›“ęŽ„ä½æē”ØęŠ„é”™äæ”ęÆå»ŗč®®ēš„é‚£äøŖē²¾ē”®ēš„ `https://…/` URL怂 ## stdio {#stdio} @@ -94,6 +94,18 @@ translation: éœ€č¦ API key ēš„ęœåŠ”å™ØåœØé‚£é‡Œę‰¾äøåˆ°å®ƒć€‚ē”Ø `env=` ę˜¾å¼ä¼ å…„ļ¼›čæ™äŗ›å˜é‡ä¼šåˆå¹¶åˆ°å…č®øåˆ—č”Øä¹‹äøŠć€‚äøŠé¢ēš„ `BOOKSHOP_API_KEY` åšēš„å°±ę˜Æčæ™ä»¶äŗ‹ć€‚ +## å†…å­˜äø­ {#in-memory} + +åœØęµ‹čÆ•é‡Œę²”ęœ‰ä»€ä¹ˆč¦éƒØē½²ļ¼Œä¹Ÿę²”ęœ‰ä»€ä¹ˆč¦åÆåŠØć€‚ä¼ å…„ęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«ļ¼š + +```python hl_lines="14" +--8<-- "docs_src/client_transports/tutorial001.py" +``` + +ę²”ęœ‰å­čæ›ēØ‹ļ¼Œę²”ęœ‰ē«Æå£ļ¼Œēŗæč·ÆäøŠę²”ęœ‰ä»»ä½•å­—čŠ‚ć€‚å®¢ęˆ·ē«Æå’ŒęœåŠ”å™Øę˜ÆåŒäø€äøŖčæ›ēØ‹é‡Œēš„äø¤äøŖåÆ¹č±”ļ¼Œč€Œč°ƒē”Øä»ē„¶čµ°ēœŸå®žēš„åč®®å±‚ļ¼š`search_books` ēš„åˆ—å‡ŗć€ę ”éŖŒå’Œč°ƒē”Øļ¼Œå’Œčµ° HTTP ę—¶å®Œå…Øäø€ę ·ć€‚**[测试](../get-started/testing.md)** é”µé¢å›“ē»•å®ƒęž„å»ŗäŗ†ę•“å„—ęØ”å¼ć€‚ + +åŒę ·ēš„å½¢å¼ä¹Ÿå…¼ä½œåµŒå…„ APIļ¼šč‡Ŗå·±ęž„é€ ęœåŠ”å™Øēš„åŗ”ē”Øäøéœ€č¦ē»čæ‡ē½‘ē»œå°±čƒ½č°ƒē”Øå®ƒēš„å·„å…·ć€‚ + ## SSE {#sse} `sse_client(url)` ę„č‡Ŗ `mcp.client.sse`,是被 Streamable HTTP å–ä»£ēš„é‚£äøŖ HTTP ä¼ č¾“ć€‚ē”ØåŒę ·ēš„ę–¹å¼åŒ…äø€å±‚ļ¼Œ`Client(sse_client("http://localhost:8000/sse"))`ļ¼Œå°±čƒ½å’Œä»åœØä½æē”Øå®ƒēš„ęœåŠ”å™Øé€šäæ”ļ¼›äøč¦åœØå®ƒä¹‹äøŠęž„å»ŗä»»ä½•ę–°äøœč„æć€‚ @@ -102,15 +114,16 @@ translation: 对 `Client` ę„čÆ“ļ¼ŒäøŠé¢čæ™äŗ›éƒ½ę˜ÆåŒäø€ē§äøœč„æć€‚ -**传输**ę˜Æä»»ä½•čƒ½äŗ§å‡ŗäø€åÆ¹ `(read, write)` ę¶ˆęÆęµēš„å¼‚ę­„äøŠäø‹ę–‡ē®”ē†å™Øļ¼šę­£å¼åœ°čÆ“ļ¼Œå°±ę˜Æ `mcp.client` äø­ēš„ `Transport` åč®®ć€‚`Client` ęŒ‰ē±»åž‹č§£ęžå®ƒēš„å‚ę•°ļ¼šęœåŠ”å™ØåÆ¹č±”åœØčæ›ēØ‹å†…čæžęŽ„ļ¼Œ`str` å˜ęˆ `streamable_http_client(url)`,`StdioServerParameters` å˜ęˆ `stdio_client(params)`ļ¼Œå…¶ä»–ä»»ä½•äøœč„æéƒ½ē›“ęŽ„ä½œäøŗä¼ č¾“čæ›å…„ć€‚ę­£ę˜Æęœ€åŽčæ™ę”č§„åˆ™č®© `stdio_client(...)`态`streamable_http_client(...)` 和 `sse_client(...)` éƒ½čƒ½ę”¾čæ›åŒäø€äøŖä½ē½®ļ¼Œä¹Ÿč®©ä½ åÆä»„č‡Ŗå·±å†™äø€äøŖć€‚ +**传输**ę˜Æä»»ä½•čƒ½äŗ§å‡ŗäø€åÆ¹ `(read, write)` ę¶ˆęÆęµēš„å¼‚ę­„äøŠäø‹ę–‡ē®”ē†å™Øļ¼šę­£å¼åœ°čÆ“ļ¼Œå°±ę˜Æ `mcp.client` äø­ēš„ `Transport` åč®®ć€‚`Client` ęŒ‰ē±»åž‹č§£ęžå®ƒēš„å‚ę•°ļ¼š`str` å˜ęˆ `streamable_http_client(url)`,`StdioServerParameters` å˜ęˆ `stdio_client(params)`ļ¼ŒęœåŠ”å™ØåÆ¹č±”åœØčæ›ēØ‹å†…čæžęŽ„ļ¼Œå…¶ä»–ä»»ä½•äøœč„æéƒ½ē›“ęŽ„ä½œäøŗä¼ č¾“čæ›å…„ć€‚ę­£ę˜Æęœ€åŽčæ™ę”č§„åˆ™č®© `stdio_client(...)`态`streamable_http_client(...)` 和 `sse_client(...)` éƒ½čƒ½ę”¾čæ›åŒäø€äøŖä½ē½®ļ¼Œä¹Ÿč®©ä½ åÆä»„č‡Ŗå·±å†™äø€äøŖć€‚ ## 回锾 {#recap} -* `Client(mcp)`ļ¼ˆęœåŠ”å™ØåÆ¹č±”ļ¼‰åœØå†…å­˜äø­čæžęŽ„ć€‚ē”ØäŗŽęµ‹čÆ•å’ŒåµŒå…„ć€‚ * `Client("http://.../mcp")`(URLļ¼‰é€ščæ‡ Streamable HTTP čæžęŽ„ļ¼Œå³ē”Ÿäŗ§ēŽÆå¢ƒēš„ä¼ č¾“ę–¹å¼ć€‚ * čÆ·ę±‚å¤“ć€č®¤čÆć€ä»£ē†å’Œč¶…ę—¶åŗ”ę”¾åœØ `httpx2.AsyncClient` äøŠļ¼Œå†ä¼ ē»™ `streamable_http_client(url, http_client=...)`ć€‚ę²”ęœ‰ `headers=` å…³é”®å­—å‚ę•°ć€‚ +* é‡å®šå‘åŖåœØ URL č‡Ŗå·±ēš„ęŗä¹‹å†…č¢«č·Ÿéšļ¼ˆå°¾éƒØę–œę ēš„ `307`/`308`ļ¼‰ļ¼Œå¤–åŠ åŒäø€äø»ęœŗäøŠēš„ `http`→`https`ć€‚å…¶ä»–ęƒ…å†µéƒ½ä¼šä»„ `Redirect to … not followed` å¤±č“„ļ¼›ęŠŠęœ€ē»ˆēš„ URL å†™čæ›é…ē½®ć€‚ * stdio 是 `Client(StdioServerParameters(...))`ć€‚åŖęœ‰åœØéœ€č¦é‡å®šå‘å­čæ›ēØ‹ēš„ stderr ę—¶ļ¼Œę‰č‡Ŗå·±ē”Ø `stdio_client(...)` åŒ…äø€å±‚ć€‚ * å­čæ›ēØ‹ę‹æåˆ°ēš„ę˜Æå…č®øåˆ—č”Øé‡Œēš„ēŽÆå¢ƒļ¼Œäøę˜Æä½ ēš„ēŽÆå¢ƒļ¼›`env=` å¾€é‡Œę·»åŠ ć€‚ +* `Client(mcp)`ļ¼ˆęœåŠ”å™ØåÆ¹č±”ļ¼‰åœØå†…å­˜äø­čæžęŽ„ć€‚ē”ØäŗŽęµ‹čÆ•ļ¼Œęˆ–č€…ęŠŠęœåŠ”å™ØåµŒå…„ęž„å»ŗå®ƒēš„é‚£äøŖåŗ”ē”Øć€‚ * ä¼ č¾“å°±ę˜Æä»»ä½•åÆä»„ `async with x as (read, write)` ēš„äøœč„æć€‚å‡”äøę˜ÆęœåŠ”å™ØåÆ¹č±”ć€URL ꈖ `StdioServerParameters` ēš„å‚ę•°ļ¼Œ`Client` éƒ½ē›“ęŽ„äŗ¤ē»™čæ™äøŖåč®®ć€‚ * Ꞅ造 `Client` é€‰å®šä¼ č¾“ę–¹å¼ć€‚`async with` ę‰“å¼€å®ƒć€‚ diff --git a/i18n/zh/pages/deprecated.md b/i18n/zh/pages/deprecated.md index 9785fdf2..7d01c3ef 100644 --- a/i18n/zh/pages/deprecated.md +++ b/i18n/zh/pages/deprecated.md @@ -1,11 +1,11 @@ --- translation: - sections: [490237e61c3a7a44, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, 4068f23e371bf0b3, eaef75b8725bc931] + sections: [4c1dea378b2b1bf7, 01262a123ad9501d, 429db5b574a2ac08, e2d0d273fbd2d74b, 64ab0331e868f3d4, 6c8878ce2d1f6d56, c3d1099701156881, e185a1b8e53669f6] tool: 1 --- # å·²å¼ƒē”Øēš„åŠŸčƒ½ {#deprecated-features} -2026-07-28 č§„čŒƒč®©äŗ”é”¹å†…å®¹é€€å½¹ć€‚SDK ä»ē„¶å®žēŽ°äŗ†å…¶äø­ęÆäø€é”¹ļ¼Œč€Œäø”ęÆäø€é”¹ēŽ°åœØéƒ½åø¦ęœ‰**å¼ƒē”Øč­¦å‘Š**ć€‚å¦ęœ‰äø€äøŖ SDK č¾…åŠ©å‡½ę•°å› č‡Ŗčŗ«åŽŸå› č¢«å¼ƒē”Øļ¼Œåˆ—åœØ[本锵末尾](#deprecated-sdk-helpers)怂 +2026-07-28 č§„čŒƒč®©äŗ”é”¹å†…å®¹é€€å½¹ć€‚SDK ä»ē„¶å®žēŽ°äŗ†å…¶äø­ęÆäø€é”¹ļ¼Œč€Œäø”ęÆäø€é”¹ēŽ°åœØéƒ½åø¦ęœ‰**å¼ƒē”Øč­¦å‘Š**ć€‚å¦ęœ‰å‡ é”¹ SDK å±‚é¢ēš„å¼ƒē”Øå‡ŗäŗŽč‡Ŗčŗ«åŽŸå› ļ¼Œåˆ—åœØ[本锵末尾](#deprecated-sdk-helpers)怂 äø‹č”Øåˆ—å‡ŗäŗ†ęÆäø€é”¹å·²å¼ƒē”Øēš„åŠŸčƒ½ć€å®ƒäøŗä»€ä¹ˆč¦é€€åœŗļ¼Œä»„åŠåŗ”čÆ„ę”¹ē”Øēš„ę›æä»£ę–¹ę”ˆć€‚ @@ -125,11 +125,13 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) ## å·²å¼ƒē”Øēš„ SDK č¾…åŠ©å‡½ę•° {#deprecated-sdk-helpers} -čæ™äŗ›äøę˜Æč§„čŒƒå˜ę›“ļ¼ŒåŖę˜Æęœ‰äŗ†ę›“å„½ę›æä»£ēš„ SDK å†…éƒØå®žēŽ°ć€‚å®ƒä»¬ē”ØåŒäø€äøŖ `MCPDeprecationWarning` å‘å‡ŗč­¦å‘Šļ¼Œå¹¶å°†åœØ 3.0 中移除。 +čæ™äŗ›äøę˜Æč§„čŒƒå˜ę›“ļ¼ŒåŖę˜Æęœ‰äŗ†ę›“å„½ę›æä»£ēš„ SDK ē”Øę³•ć€‚å®ƒä»¬ē”ØåŒäø€äøŖ `MCPDeprecationWarning` å‘å‡ŗč­¦å‘Šļ¼Œ3.0 ä¼šē§»é™¤ę—§å½¢å¼ć€‚ | å·²å¼ƒē”Ø | ę›æä»£åšę³• | |---|---| | `FuncMetadata.call_fn_with_arg_validation()` | å…ˆč°ƒē”Ø `FuncMetadata.validate_arguments()`ļ¼Œå†č°ƒē”Ø `FuncMetadata.call_fn()`ć€‚åŖęœ‰ē›“ęŽ„é©±åŠØ `FuncMetadata` ēš„ä»£ē ļ¼ˆęÆ”å¦‚č‡Ŗå®šä¹‰ēš„ `Tool` å­ē±»ļ¼‰ę‰č°ƒē”Øčæ‡å®ƒć€‚ | +| äøåø¦ `validate_token_resource=` ēš„ `AuthSettings(resource_server_url=...)` | 设置它:`True` č®©ęœåŠ”å™Øę‹’ē»ä½ ēš„éŖŒčÆå™ØęœŖęŠ„å‘Šäøŗé’ˆåÆ¹ `resource_server_url` ē­¾å‘ēš„ bearer ä»¤ē‰Œļ¼Œ`False` č”Øē¤ŗä½ ēš„éŖŒčÆå™Øč‡Ŗå·±ę£€ęŸ„ä»¤ē‰Œēš„å—ä¼—ļ¼ˆč§ **[ꎈꝃ](run/authorization.md#a-token-verifier)**ļ¼‰ć€‚äøč®¾ē½®ę—¶č”Œäøŗē­‰åŒäŗŽ `False`ļ¼›3.0 čµ·ļ¼ŒåŖč¦č®¾ē½®äŗ† `resource_server_url`ļ¼Œé»˜č®¤å€¼å°±ę˜Æ `True`怂 | +| äøåø¦ `issuer=` ēš„ `ClientCredentialsOAuthProvider(...)` ꈖ `PrivateKeyJWTOAuthProvider(...)` | ä¼ å…„ `issuer=`ļ¼ŒęŒ‡ę˜Žē­¾å‘čæ™äŗ›å‡­ę®ēš„ęŽˆęƒęœåŠ”å™Øļ¼ˆč§ **[编写 OAuth 客户端](client/oauth-clients.md#machine-to-machine)**ļ¼‰ć€‚äøä¼ ēš„čÆļ¼Œē”± MCP ęœåŠ”å™Øå†³å®šå“ŖäøŖęŽˆęƒęœåŠ”å™Øę”¶åˆ°čæ™äŗ›å‡­ę®ļ¼›3.0 čµ·čæ™äøŖå…³é”®å­—å‚ę•°åæ…å”«ć€‚ | ## 回锾 {#recap} @@ -138,7 +140,7 @@ warnings.filterwarnings("ignore", category=MCPDeprecationWarning) * å¼ƒē”ØåŖę˜Æå»ŗč®®ę€§ēš„ļ¼šēŗæč·ÆäøŠę²”ęœ‰å˜åŒ–ļ¼ŒåœØ 2026 ä¹‹å‰ēš„ä¼ščÆäøŠäø€åˆ‡ē…§åøøå·„ä½œļ¼Œä½ ä¼šēœ‹åˆ°äø€ę”é†’ē›®ēš„ `MCPDeprecationWarning`(它是 `UserWarning`ļ¼Œę‰€ä»„é»˜č®¤å¼€åÆļ¼‰ć€‚ * é‡‡ę ·å’Œę ¹ē›®å½•čæ˜éœ€č¦äø€ę”åå‘é€šé“ļ¼Œč€Œ 2026-07-28 ä¼ščÆę²”ęœ‰ć€‚åœØēŽ°ä»£čæžęŽ„äøŠļ¼Œå®ƒä»¬å…ˆč­¦å‘Šļ¼Œē„¶åŽęŠ›å‡ŗå¼‚åøøć€‚ * `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` č®©ę•“äøŖē±»åˆ«é™éŸ³ļ¼›pytest äø­ēš„ `"error::mcp.MCPDeprecationWarning"` ęŠŠå®ƒå˜ęˆęµ‹čÆ•å¤±č“„ć€‚ -* 一个 SDK č¾…åŠ©å‡½ę•° `FuncMetadata.call_fn_with_arg_validation()` å•ē‹¬č¢«å¼ƒē”Øļ¼Œå°†åœØ 3.0 中移除。 +* [SDK å±‚é¢ēš„å¼ƒē”Ø](#deprecated-sdk-helpers)éµå¾ŖåŒę ·ēš„č§„åˆ™ļ¼šēŽ°åœØå‘å‡ŗč­¦å‘Šļ¼Œ3.0 ē§»é™¤ę—§å½¢å¼ć€‚ * ę–°ä»£ē äøåŗ”å»ŗē«‹åœØå…¶äø­ä»»ä½•äø€é”¹ä¹‹äøŠć€‚ ęœ¬ę–‡ę”£ēš„å…¶ä»–ęÆäø€é”µč®²ēš„éƒ½ę˜Æå½“å‰ēš„ API怂 diff --git a/i18n/zh/pages/get-started/first-steps.md b/i18n/zh/pages/get-started/first-steps.md index a93641a4..7cb46def 100644 --- a/i18n/zh/pages/get-started/first-steps.md +++ b/i18n/zh/pages/get-started/first-steps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686] + sections: [0d6c05bcbf836bf3, 9a78b5f6b44b18ab, 7114d8d6daba203f, e8bbb56a98ba7bc9, bfd2fd1153e71dac, 1615a994ef071fdd, 65c599fae991f245] tool: 1 --- # 第一歄 {#first-steps} @@ -17,7 +17,7 @@ translation: * **客户端** ä½äŗŽå®æäø»å†…éƒØļ¼Œč®² MCPć€‚å®æäø»ęÆčæžęŽ„äø€äøŖęœåŠ”å™Øļ¼Œå°±čæč”Œäø€äøŖå®¢ęˆ·ē«Æć€‚ * **ęœåŠ”å™Ø** ę˜Æä½ ē”Øčæ™äøŖ SDK ęž„å»ŗēš„äøœč„æć€‚å®ƒå‘å®¢ęˆ·ē«Æęš“éœ²å†…å®¹ļ¼Œä»Žäøē›“ęŽ„å’ŒęØ”åž‹åÆ¹čÆć€‚ -ä½ å†™ēš„ę˜ÆęœåŠ”å™Øć€‚å®æäø»ę˜Æåˆ«äŗŗēš„äŗ§å“ć€‚SDK čæ˜ęä¾›äŗ†äø€äøŖ `Client`ļ¼Œä½ ä¼šē”Øå®ƒę„ęµ‹čÆ•č‡Ŗå·±ēš„ęœåŠ”å™Øļ¼Œęœ¬é”µåŽé¢å°±ä¼šē”Øåˆ°ć€‚ +ä½ å†™ēš„ę˜ÆęœåŠ”å™Øć€‚å®æäø»ę˜Æåˆ«äŗŗēš„äŗ§å“ć€‚SDK čæ˜ęä¾›äŗ†äø€äøŖ `Client`ļ¼Œå®æäø»é€ščæ‡ URL čæžęŽ„ęœåŠ”å™Øęˆ–ęŠŠęœåŠ”å™Øä½œäøŗå­čæ›ēØ‹åÆåŠØę—¶ē”Øēš„å°±ę˜Æčæ™äøŖē±»ć€‚ęœ¬é”µåŽé¢ä¼šē”Øåˆ°å®ƒļ¼Œęµ‹čÆ•č‡Ŗå·±ēš„ęœåŠ”å™Øä¹Ÿé å®ƒć€‚ ## äø‰ē§åŽŸčÆ­ {#the-three-primitives} @@ -79,22 +79,20 @@ Inspector ę˜Æé€ščæ‡ **stdio** čæč”Œä½ ēš„ęœåŠ”å™Øēš„ļ¼Œčæ™ę˜Æ MCP ęœåŠ”å™ØåÆ å®¢ęˆ·ē«ÆčæžęŽ„ę—¶ļ¼ŒęœåŠ”å™Øä¼šå£°ę˜Žč‡Ŗå·±ēš„ **čƒ½åŠ›**ļ¼šå®ƒä¼šå“åŗ”å“Ŗå‡ ē±»čÆ·ę±‚ć€‚å®¢ęˆ·ē«Æę ¹ę®čæ™ä»½å£°ę˜Žę„å†³å®ščÆ„čÆ·ę±‚ä»€ä¹ˆć€‚čæ™ä»½å£°ę˜Žä½ ä»Žę²”å†™čæ‡ļ¼›ę˜Æ `MCPServer` ę›æä½ å£°ę˜Žēš„ć€‚ -č‡Ŗå·±ēœ‹äø€äø‹ć€‚SDK ēš„ `Client` åÆä»„ē›“ęŽ„ęŽ„å—ęœåŠ”å™ØåÆ¹č±”ļ¼Œå¹¶åœØ **å†…å­˜äø­** äøŽä¹‹čæžęŽ„ļ¼ˆę²”ęœ‰å­čæ›ēØ‹ļ¼Œę²”ęœ‰ē«Æå£ļ¼‰ļ¼š +č‡Ŗå·±ēœ‹äø€äø‹ć€‚åœØäø€äøŖē»ˆē«Æé‡Œč®© `server.py` é€ščæ‡ HTTP čæč”Œē€ļ¼š -```python -import asyncio +```console +uv run mcp run server.py --transport streamable-http +``` -from mcp import Client +ē„¶åŽåœØå¦äø€äøŖē»ˆē«Æé‡Œē”Øå®¢ęˆ·ē«ÆčæžäøŠå®ƒļ¼š -from server import mcp +```python title="client.py" hl_lines="7-8" +--8<-- "docs_src/first_steps/tutorial001_client.py" +``` - -async def main() -> None: - async with Client(mcp) as client: - print(client.server_capabilities.model_dump(exclude_none=True)) - - -asyncio.run(main()) +```console +python client.py ``` ```text @@ -114,7 +112,7 @@ asyncio.run(main()) ę³Øę„čæ™é‡Œē¼ŗäŗ†ä»€ä¹ˆć€‚`completions`ļ¼ˆčµ„ęŗęØ”ęæå’Œęē¤ŗčÆēš„å‚ę•°č‡ŖåŠØč”„å…Øļ¼‰éœ€č¦äø€äøŖē”±ä½ ē¼–å†™ēš„å¤„ē†å‡½ę•°ļ¼Œč€Œčæ™äøŖęœåŠ”å™Øę²”ęœ‰ļ¼Œę‰€ä»„čæ™é”¹čƒ½åŠ›äøä¼šå‡ŗēŽ°ļ¼Œč”Œäøŗč§„čŒƒēš„å®¢ęˆ·ē«Æä¹Ÿå°±äøä¼šåŽ»é—®ć€‚ę‰€ęœ‰åÆé€‰é”¹éƒ½éµå¾Ŗčæ™ę”č§„åˆ™ļ¼šę³Øå†Œäŗ†åÆ¹åŗ”ēš„äøœč„æļ¼Œčƒ½åŠ›å°±å‡ŗēŽ°ļ¼›**[蔄全](../servers/completions.md)** ä¼ščÆę˜Žčæ™äø€ē‚¹ć€‚ !!! info - `Client(mcp)` ę­£ę˜Æčæ™äŗ›ę–‡ę”£é‡ŒęÆäøŖē¤ŗä¾‹ęµ‹čÆ•ę—¶ę‰€ē”Øēš„é‚£äøŖå†…å­˜å®¢ęˆ·ē«Æļ¼Œä½ ęµ‹čÆ•č‡Ŗå·±ēš„ęœåŠ”å™Øä¹Ÿä¼šē”Øå®ƒć€‚å®ƒęœ‰ę•“ę•“äø€é”µļ¼š**[测试](testing.md)**怂 + 这个 `client.py` å°±ę˜Æäø€äøŖå®Œę•“ēš„ MCP 客户端,**[客户端](../client/index.md)** ę˜Æå®ƒēš„äø“é”µć€‚ęµ‹čÆ•ę—¶åÆä»„ēœęŽ‰ē»ˆē«Æå’Œē«Æå£ļ¼ŒęŠŠęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«äŗ¤ē»™ `Client`ļ¼Œå³ `Client(mcp)`ć€‚å®ƒä¹Ÿęœ‰ę•“ę•“äø€é”µļ¼š**[测试](testing.md)**怂 ## ä½ ę²”ęœ‰å†™ēš„äøœč„æ {#what-you-did-not-write} @@ -123,7 +121,7 @@ asyncio.run(main()) * JSON Schema怂`a: int, b: int` **就是** `add` ēš„ęØ”å¼ć€‚ * 请求处理函数。`tools/list`态`resources/read`态`prompts/get`ļ¼šå…Øéƒ½ę›æä½ å¤„ē†å„½äŗ†ć€‚ * čƒ½åŠ›å£°ę˜Žć€‚`MCPServer` ę›æä½ ē”Ÿęˆäŗ†ć€‚ -* äø€č”Œåč®®ä»£ē ć€‚ē‰ˆęœ¬åå•†ć€JSON-RPC åˆ†åø§ć€čƒ½åŠ›äŗ¤ę¢ļ¼šå…Øéƒ½å‘ē”ŸåœØ `mcp dev` 和 `Client(mcp)` å†…éƒØļ¼Œä½ äø€ēœ¼éƒ½ę²”č§åˆ°ć€‚ +* äø€č”Œåč®®ä»£ē ć€‚ē‰ˆęœ¬åå•†ć€JSON-RPC åˆ†åø§ć€čƒ½åŠ›äŗ¤ę¢ļ¼šå…Øéƒ½å‘ē”ŸåœØ `mcp dev` 和 `client.py` å†…éƒØļ¼Œä½ äø€ēœ¼éƒ½ę²”č§åˆ°ć€‚ čæ™äøŖęÆ”ä¾‹ļ¼Œę­£ę˜Æčæ™äøŖ SDK ēš„ę„ä¹‰ę‰€åœØć€‚ @@ -134,6 +132,6 @@ asyncio.run(main()) * ęÆē§åŽŸčÆ­äø€äøŖč£…é„°å™Øļ¼š`@mcp.tool()`态`@mcp.resource(uri)`态`@mcp.prompt()`ć€‚åē§°ć€ęčæ°å’ŒęØ”å¼éƒ½ę„č‡Ŗå‡½ę•°ęœ¬čŗ«ć€‚ * åø¦ `{param}` ēš„ URI ē”Ÿęˆēš„ę˜Æčµ„ęŗ **ęØ”ęæ**ļ¼ŒäøŽå…·ä½“čµ„ęŗåˆ†å¼€åˆ—å‡ŗć€‚ * ęœåŠ”å™Øēš„ **čƒ½åŠ›** ä¼šę›æä½ å£°ę˜Žå„½ļ¼Œč€Œå®¢ęˆ·ē«ÆåŖä¼ščÆ·ę±‚ęœåŠ”å™Øå£°ę˜Žčæ‡ēš„å†…å®¹ć€‚ -* `Client(mcp)` åœØå†…å­˜äø­čæžęŽ„ęœåŠ”å™ØåÆ¹č±”ļ¼šä»Žē¬¬äø€å¤©čµ·ļ¼Œå®ƒå°±ę˜Æä½ ēš„ęµ‹čÆ•å·„å…·ć€‚ +* `Client("http://localhost:8000/mcp")` äøŽę­£åœØčæč”Œēš„ęœåŠ”å™ØåÆ¹čÆć€‚ę”¹äøŗęŠŠęœåŠ”å™ØåÆ¹č±”äŗ¤ē»™å®ƒļ¼Œå³ `Client(mcp)`ļ¼Œå®ƒä»Žē¬¬äø€å¤©čµ·å°±ę˜Æä½ ēš„ęµ‹čÆ•å·„å…·ć€‚ ęŽ„äø‹ę„ę˜Æ **[čæžęŽ„åˆ°ēœŸå®žå®æäø»](real-host.md)**ļ¼šęŠŠčæ™äøŖęœåŠ”å™ØēœŸę­£ę”¾čæ› Claude Desktop ꈖ IDE é‡Œć€‚ē„¶åŽę˜Æ **[测试](testing.md)**ļ¼šäø€é”µå†…å®¹ļ¼Œäø€äøŖå†…å­˜å®¢ęˆ·ē«Æļ¼Œä»Žę­¤äøē”Øå†ēŒœå®ƒåˆ°åŗ•čƒ½äøčƒ½ē”Øć€‚å†ä¹‹åŽļ¼ŒęÆē§åŽŸčÆ­å„ęœ‰č‡Ŗå·±ēš„äø€é”µļ¼Œä»ŽęØ”åž‹é©±åŠØēš„é‚£äø€ē§å¼€å§‹ļ¼š**[å·„å…·](../servers/tools.md)**怂 diff --git a/i18n/zh/pages/get-started/testing.md b/i18n/zh/pages/get-started/testing.md index 234d378b..6e839044 100644 --- a/i18n/zh/pages/get-started/testing.md +++ b/i18n/zh/pages/get-started/testing.md @@ -1,13 +1,13 @@ --- translation: - sections: ['4926721070127497', c52a1de2b6b32f40, 8e792bf8c7489ec6, 627195f7159e24ef] + sections: [5d13c2f0ba42c0d2, c52a1de2b6b32f40, 8e792bf8c7489ec6, 38552ea228b0a04f] tool: 1 --- # 测试 {#testing} -Python SDK ęä¾›äŗ†äø€äøŖåø¦**å†…å­˜ä¼ č¾“**ēš„ `Client` ē±»ļ¼šęŠŠęœåŠ”å™ØåÆ¹č±”ä¼ ē»™å®ƒļ¼Œå®ƒå°±ä¼šē›“ęŽ„čæžęŽ„äøŠåŽ»ć€‚ +SDK ēš„ `Client` ē±»ļ¼Œä¹Ÿå°±ę˜ÆčæžęŽ„ URL ęˆ–åÆåŠØå­čæ›ēØ‹ēš„é‚£äøŖē±»ļ¼Œčæ˜čƒ½åœØ**å†…å­˜äø­**čæžęŽ„ļ¼šęŠŠęœåŠ”å™ØåÆ¹č±”ä¼ ē»™å®ƒļ¼Œå®ƒå°±ē›“ęŽ„å’ŒęœåŠ”å™ØåÆ¹čÆć€‚ -äøē”Øå­čæ›ēØ‹ļ¼Œäøå ē«Æå£ļ¼Œę ¹ęœ¬äøčµ°ä»»ä½•ä¼ č¾“ć€‚ę€č·Æå’Œ FastAPI ēš„ `TestClient` 一样。 +äøē”Øå­čæ›ēØ‹ļ¼Œäøå ē«Æå£ļ¼Œēŗæč·ÆäøŠä»€ä¹ˆéƒ½ę²”ęœ‰ć€‚ę€č·Æå’Œ FastAPI ēš„ `TestClient` 一样。 ## åŸŗęœ¬ē”Øę³• {#basic-usage} @@ -86,7 +86,7 @@ async def test_call_add_tool(client: Client): ęµ‹čÆ•é‡Œå°±č®©å®ƒå¼€ē€ć€‚å®ƒåœØē”Ÿäŗ§ä»£ē äø­ę²”ęœ‰ę„ä¹‰ć€‚ -## é»˜č®¤åœØčæ›ēØ‹å†… {#in-process-by-default} +## é»˜č®¤äøåŒŗåˆ†åč®®ę—¶ä»£ {#era-neutral-by-default} !!! note `Client(mcp)` åœØčæ›ēØ‹å†…čæžęŽ„ļ¼Œé»˜č®¤**äøåŒŗåˆ†åč®®ę—¶ä»£**ļ¼šå®ƒä¼šå…ˆęŽ¢ęµ‹ęœåŠ”å™Øļ¼Œå†é€‰ę‹©åˆé€‚ēš„åč®®č·Æå¾„ć€‚å¦‚ęžœęµ‹čÆ•č¦éŖŒčÆę—§ē‰ˆļ¼ˆlegacyļ¼‰ē‰¹ęœ‰ēš„čÆ­ä¹‰ļ¼ˆé‡‡ę ·ļ¼ˆsamplingļ¼‰ęˆ–å¾čÆ¢ļ¼ˆelicitationļ¼‰ēš„ęŽØé€ć€`message_handler`ļ¼‰ļ¼Œå°±å›ŗå®šä½æē”Ø `mode="legacy"`ļ¼Œå¹¶åœØčæ™ē§ęƒ…å†µäø‹åŽ»ęŽ‰ `raise_exceptions=True`ļ¼šę—§ē‰ˆčæžęŽ„ęœ¬ę„å°±äøåšč„±ę•ļ¼Œč€Œčæ™äøŖę ‡åæ—ä¼šč®©å¤±č“„åœØęœåŠ”å™Øä»»åŠ”å†…éƒØé‡ę–°ęŠ›å‡ŗļ¼Œč€Œäøę˜ÆęŠ›åˆ°ä½ ēš„ęµ‹čÆ•é‡Œć€‚ diff --git a/i18n/zh/pages/handlers/multi-round-trip.md b/i18n/zh/pages/handlers/multi-round-trip.md index 2c5b0e7f..8f64990d 100644 --- a/i18n/zh/pages/handlers/multi-round-trip.md +++ b/i18n/zh/pages/handlers/multi-round-trip.md @@ -1,6 +1,6 @@ --- translation: - sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, 8dd027377d46448b, f81491125dcbfe8b] + sections: [74011e683045eea9, 9b64cc175c18b6a9, 4b41be4824030397, e3b1502da786ec33, 71e41161f143c6a9, 9ec2c1eeb8c36378, b47667184ca5b516, f81491125dcbfe8b] tool: 1 --- # å¤šč½®å¾€čæ”ļ¼ˆmulti-round-trip)请求 {#multi-round-trip-requests} @@ -162,7 +162,7 @@ TTLć€äø»ä½“ē»‘å®šå’ŒčÆ·ę±‚ē»‘å®š**äøę˜Æ** codec ēš„å·„ä½œļ¼šåÆ¹ęÆäøŖ codec, ## 一个 2026-07-28 ēš„ē»“ęžœ {#a-2026-07-28-result} -`InputRequiredResult` åŖå­˜åœØäŗŽåč®®ē‰ˆęœ¬ **2026-07-28**ć€‚å†…å­˜äø­ēš„ `Client(server)` ę›æä½ åå•†å®ƒļ¼›čµ°ēŗæč·Æę—¶ļ¼Œ`mode="auto"` ä¼šå‘ēŽ°å®ƒć€‚čæžęŽ„ä¹‹åŽļ¼Œ`client.protocol_version` å‘ŠčÆ‰ä½ ę‹æåˆ°ēš„ę˜Æä»€ä¹ˆć€‚ +`InputRequiredResult` åŖå­˜åœØäŗŽåč®®ē‰ˆęœ¬ **2026-07-28**怂`Client` é»˜č®¤ēš„ `mode="auto"` åœØä»»ä½•čæžęŽ„äøŠéƒ½ä¼šå‘ēŽ°å®ƒć€‚čæžęŽ„ä¹‹åŽļ¼Œ`client.protocol_version` å‘ŠčÆ‰ä½ ę‹æåˆ°ēš„ę˜Æä»€ä¹ˆć€‚ !!! warning 2026 ä¹‹å‰ēš„ä¼ščÆę²”ęœ‰åœ°ę–¹ę”¾ `InputRequiredResult`ć€‚åœØ `mode="legacy"` čæžęŽ„äøŠä»Žå¤„ē†å‡½ę•°čæ”å›žäø€äøŖļ¼Œčæč”Œå™Øę— ę³•ęŠŠå®ƒåŗåˆ—åŒ–åˆ°åå•†å„½ēš„ē‰ˆęœ¬ļ¼›å®¢ęˆ·ē«Æę”¶åˆ°ēš„ę˜Æ `-32603`ā€œHandler returned an invalid resultā€é”™čÆÆć€‚åŒę—¶ęœåŠ”äø¤äøŖę—¶ä»£ēš„ęœåŠ”å™ØåœØē”Øå®ƒä¹‹å‰åæ…é”»ę£€ęŸ„ `ctx.protocol_version`怂 diff --git a/i18n/zh/pages/handlers/progress.md b/i18n/zh/pages/handlers/progress.md index a39d3be2..1461f326 100644 --- a/i18n/zh/pages/handlers/progress.md +++ b/i18n/zh/pages/handlers/progress.md @@ -1,6 +1,6 @@ --- translation: - sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 0284b215e85366c4, 8534d8dbb4053a70, 2966fac6fe697007] + sections: [5315262fe26b33e1, 9d8e98840f1b78f0, 52d6009a07e770ea, 8534d8dbb4053a70, 2966fac6fe697007] tool: 1 --- # 进度 {#progress} @@ -29,19 +29,17 @@ translation: 客户端**ęŒ‰č°ƒē”Ø**é€‰ę‹©ęŽ„ę”¶ļ¼Œę–¹ę³•ę˜Æē»™ `call_tool` ä¼  `progress_callback=`: -```python title="client.py" hl_lines="7 16" +```python title="client.py" hl_lines="5 14" import anyio from mcp import Client -from server import mcp - async def show(progress: float, total: float | None, message: str | None) -> None: print(f"{message} ({progress}/{total})") async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool( "import_catalog", {"urls": ["https://example.com/a.json", "https://example.com/b.json"]}, @@ -56,23 +54,27 @@ anyio.run(main) å›žč°ƒę˜Æäø€äøŖ `async` å‡½ę•°ļ¼ŒęŽ„ę”¶ēš„ę­£ę˜ÆęœåŠ”å™ØęŠ„å‘Šēš„å†…å®¹ļ¼š`progress`态`total`态`message`怂 !!! info - `Client(mcp)` ē›“ęŽ„åœØå†…å­˜äø­čæžęŽ„åˆ°ęœåŠ”å™ØåÆ¹č±”ļ¼Œå’Œ **[测试](../get-started/testing.md)** é”µé¢ę‰€ē”Øēš„ę˜ÆåŒäø€äøŖå®¢ęˆ·ē«Æć€‚ę— č®ŗ `Client` ē”Øå“Ŗē§ä¼ č¾“ę–¹å¼ļ¼Œ`progress_callback` éƒ½ę˜ÆåŒäø€äøŖå‚ę•°ļ¼›ęŽ„äø‹ę„ēœ‹åˆ°ēš„**ę—¶åŗ**åˆ™ę˜Æå†…å­˜čæžęŽ„ē‰¹ęœ‰ēš„ć€‚å®ƒä»„å†…č”ę–¹å¼čæč”Œä½ ēš„å›žč°ƒļ¼Œę‰€ä»„ęÆę”ęŠ„å‘Šéƒ½åœØ `call_tool` čæ”å›žä¹‹å‰é€č¾¾ć€‚ę¢ęˆēœŸå®žēš„ä¼ č¾“ę–¹å¼ļ¼Œé€šēŸ„ä¼šå’Œē»“ęžœē«žé€Ÿļ¼Œ`call_tool` å·²ē»čæ”å›žä¹‹åŽļ¼Œäø€äøŖę…¢ēš„å›žč°ƒåÆčƒ½čæ˜åœØčæč”Œć€‚ + 无论交给 `Client` ēš„ę˜Æä»€ä¹ˆā€”ā€”åƒčæ™é‡Œčæ™ę ·ēš„ URL态`StdioServerParameters`ļ¼Œčæ˜ę˜Æęµ‹čÆ•é‡Œēš„ęœåŠ”å™ØåÆ¹č±”ā€”ā€”`progress_callback` éƒ½ę˜ÆåŒäø€äøŖå‚ę•°ć€‚äøčæ‡ļ¼Œčµ°ēœŸå®žä¼ č¾“ę–¹å¼ę—¶č¦ē•™ę„ę—¶åŗć€‚ęÆę”é€šēŸ„éƒ½ę˜Æå•ē‹¬é€č¾¾ēš„ļ¼Œå’Œå“åŗ”å„čµ°å„ēš„ļ¼Œę‰€ä»„ `call_tool` å·²ē»čæ”å›žä¹‹åŽļ¼Œäø€äøŖę…¢ēš„å›žč°ƒåÆčƒ½čæ˜åœØčæč”Œć€‚åŖęœ‰čæ›ēØ‹å†…ēš„ęµ‹čÆ•čæžęŽ„ä¼šä»„å†…č”ę–¹å¼čæč”Œå›žč°ƒļ¼Œå¹¶äæčÆęÆę”ęŠ„å‘Šéƒ½å…ˆé€č¾¾ć€‚ ### 试一试 {#try-it} -把 `client.py` ę”¾åœØ `server.py` ę—č¾¹ļ¼Œē„¶åŽčæč”Œļ¼š +é€ščæ‡ HTTP 启动 `server.py`ļ¼Œē„¶åŽåœØå¦äø€äøŖē»ˆē«Æčæč”Œå®¢ęˆ·ē«Æļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` ```console python client.py ``` ```text -Imported https://example.com/a.json (1/2) -Imported https://example.com/b.json (2/2) +Imported https://example.com/a.json (1.0/2.0) +Imported https://example.com/b.json (2.0/2.0) {'result': 'Imported 2 records.'} ``` -ęœåŠ”å™ØäøŠēš„ęÆäø€ę¬” `await ctx.report_progress(...)` éƒ½å˜ęˆäŗ†å®¢ęˆ·ē«ÆäøŠåÆ¹ `show` ēš„äø€ę¬”č°ƒē”Øļ¼Œé”ŗåŗäøå˜ļ¼Œč€Œäø”äø¤č”Œéƒ½åœØ `call_tool` čæ”å›ž**之前**ę‰“å°äŗ†å‡ŗę„ć€‚čæ›åŗ¦äøä¼šę‰“åŒ…čæ›ē»“ęžœé‡Œļ¼›å®ƒåœØå·„å…·čæ˜åœØå¹²ę“»ēš„ę—¶å€™å°±ęµå¼é€å‡ŗć€‚ +ęœåŠ”å™ØäøŠēš„ęÆäø€ę¬” `await ctx.report_progress(...)` éƒ½å˜ęˆäŗ†å®¢ęˆ·ē«ÆäøŠåÆ¹ `show` ēš„äø€ę¬”č°ƒē”Øļ¼Œé”ŗåŗäøå˜ć€‚čæ›åŗ¦äøä¼šę‰“åŒ…čæ›ē»“ęžœé‡Œļ¼Œč€Œę˜ÆåœØå·„å…·čæ˜åœØå¹²ę“»ēš„ę—¶å€™å°±ęµå¼é€å‡ŗć€‚ !!! warning `progress_callback` å±žäŗŽ**č°ƒē”Ø**ļ¼Œč€Œäøę˜Æ `Client`ć€‚ę²”ęœ‰åÆ¹åŗ”ēš„ęž„é€ å‡½ę•°å‚ę•°ļ¼Œå› äøŗäøåŒēš„č°ƒē”Øęƒ³č¦äøåŒēš„å›žč°ƒļ¼ščæ™äø€ę¬”é©±åŠØäø‹č½½čæ›åŗ¦ę”ļ¼Œäø‹äø€ę¬”ę˜Æäø€č”Œę—„åæ—ć€‚ diff --git a/i18n/zh/pages/protocol-versions.md b/i18n/zh/pages/protocol-versions.md index c2be6477..8f9a128c 100644 --- a/i18n/zh/pages/protocol-versions.md +++ b/i18n/zh/pages/protocol-versions.md @@ -1,6 +1,6 @@ --- translation: - sections: [478fd619e5f90ef8, aef094a00e44e248, bab8cbf3449fa7e9, df1809b15a58335b, 5f9d8c2336ed0239, f54974398e43ddef, b24443dd78584870] + sections: [d98e337bf28845e0, dd642acbba36f615, f547b3e76da19c94, 149513378588da5c, 4e149ed992046709, f54974398e43ddef, b24443dd78584870] tool: 1 --- # åč®®ē‰ˆęœ¬ {#protocol-versions} @@ -11,9 +11,17 @@ MCP ęœ‰äø¤äøŖę—¶ä»£ć€‚ ä½ å‡ ä¹Žäøéœ€č¦å…³åæƒčæ™äŗ›ļ¼Œå› äøŗ `Client` ä¼šę›æä½ åå•†ć€‚ęœ¬é”µč®²ēš„ę˜ÆęŽ§åˆ¶čæ™äø€č”Œäøŗēš„å”Æäø€äø€äøŖęž„é€ å‚ę•° `mode=`ļ¼Œä»„åŠéœ€č¦ę”¹åŠØå®ƒēš„äø‰ē§ęƒ…å½¢ć€‚ +ęœ¬é”µēš„ęÆę®µä»£ē éƒ½ę˜Æäø€äøŖ `client.py`ļ¼ŒčæžęŽ„ēš„ę˜Æ **[客户端](client/index.md)** é‡Œé‚£äøŖ Bookshop ēš„ `server.py`ć€‚å…ˆåœØäø€äøŖē»ˆē«Æé‡ŒåÆåŠØé‚£äøŖęœåŠ”å™Øļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +ē„¶åŽåœØå¦äø€äøŖē»ˆē«Æé‡Œē”Ø `python client.py` čæč”ŒęÆę®µä»£ē ć€‚ + ## `mode="auto"` {#modeauto} -```python title="client.py" hl_lines="14-15" +```python title="client.py" hl_lines="7-8" --8<-- "docs_src/protocol_versions/tutorial001.py" ``` @@ -31,11 +39,11 @@ MCP ęœ‰äø¤äøŖę—¶ä»£ć€‚ ę•“äøŖåŠŸčƒ½å°±čæ™äŗ›ć€‚äø€äøŖ `Client`ļ¼Œä»»ę„ę—¶ä»£ēš„ęœåŠ”å™Øļ¼Œä»£ē é‡Œäøéœ€č¦åˆ†ę”Æć€‚ !!! info - `MCPServer` åœØęÆē§ä¼ č¾“ę–¹å¼äøŠéƒ½ä¼šå›žē­” `server/discover`ā€”ā€”å†…å­˜ć€stdio态Streamable HTTPā€”ā€”ę‰€ä»„čæžęŽ„ä½ č‡Ŗå·±ēš„ęœåŠ”å™Øę—¶ļ¼Œ`auto` ę€»ę˜Æč½åœØ `2026-07-28`ć€‚å›žé€€åŖä¼šåœØé¢åÆ¹ēœŸę­£ēš„ 2026 å¹“ä¹‹å‰ēš„ęœåŠ”å™Øę—¶č§¦å‘ļ¼Œč€Œé‚£ę­£ę˜Æä½ éœ€č¦å®ƒēš„ę—¶å€™ć€‚ + `MCPServer` åœØęÆē§ä¼ č¾“ę–¹å¼äøŠéƒ½ä¼šå›žē­” `server/discover`——Streamable HTTP态stdioļ¼Œä»„åŠęµ‹čÆ•ē”Øēš„čæ›ēØ‹å†…čæžęŽ„ā€”ā€”ę‰€ä»„čæžęŽ„ä½ č‡Ŗå·±ēš„ęœåŠ”å™Øę—¶ļ¼Œ`auto` ę€»ę˜Æč½åœØ `2026-07-28`ć€‚å›žé€€åŖä¼šåœØé¢åÆ¹ēœŸę­£ēš„ 2026 å¹“ä¹‹å‰ēš„ęœåŠ”å™Øę—¶č§¦å‘ļ¼Œč€Œé‚£ę­£ę˜Æä½ éœ€č¦å®ƒēš„ę—¶å€™ć€‚ ## `mode="legacy"` {#modelegacy} -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial002.py" ``` @@ -59,7 +67,7 @@ MCP ęœ‰äø¤äøŖę—¶ä»£ć€‚ `mode` ä¹ŸęŽ„å—äø€äøŖę–°ē‰ˆåč®®ē‰ˆęœ¬å­—ē¬¦äø²ć€‚ē›®å‰čæ™äøŖé›†åˆę­£å„½ę˜Æ `["2026-07-28"]`怂 -```python title="client.py" hl_lines="14" +```python title="client.py" hl_lines="7" --8<-- "docs_src/protocol_versions/tutorial003.py" ``` @@ -90,7 +98,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0 ę‰€ä»„ęŠŠå®ƒå­˜äø‹ę„ć€‚äø€ę¬” `auto` čæžęŽ„ä¹‹åŽļ¼Œ`client.session.discover_result` äæå­˜ē€ęœåŠ”å™Øå‘ę„ēš„é‚£äøŖ `DiscoverResult` åŽŸę ·ļ¼šå®ƒēš„ `supported_versions`ć€å®ƒēš„ `capabilities`ć€å®ƒēš„ `instructions`ļ¼Œä»„åŠęœåŠ”å™Øå†™čæ›ē»“ęžœ `_meta` é‡Œēš„čŗ«ä»½äæ”ęÆć€‚äø‹ę¬”ęŠŠå®ƒä½œäøŗ `prior_discover=` ä¼ å›žåŽ»ļ¼š -```python title="client.py" hl_lines="15 17" +```python title="client.py" hl_lines="8 10" --8<-- "docs_src/protocol_versions/tutorial004.py" ``` diff --git a/i18n/zh/pages/run/asgi.md b/i18n/zh/pages/run/asgi.md index 40d01513..a8331504 100644 --- a/i18n/zh/pages/run/asgi.md +++ b/i18n/zh/pages/run/asgi.md @@ -1,6 +1,6 @@ --- translation: - sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 70b9ece244ca1b0c, cba78e052898c3f6, f06bdb541cb0b469, fb82d526320b7cc3] + sections: [1062ef792791488a, 4be2b831547184a9, 374b049e770385f2, b72f6947089e6de0, b172c9db7831bb31, 10394c6f16601638, cba78e052898c3f6, f06bdb541cb0b469, 6dc898ccc5a903f9] tool: 1 --- # ę·»åŠ åˆ°ēŽ°ęœ‰åŗ”ē”Ø {#add-to-an-existing-app} @@ -86,7 +86,7 @@ Starlette ēš„ `Host` č·Æē”±ē”Øę³•ē›øåŒļ¼šęŠŠ `Mount("/", ...)` ę¢ęˆ `Host("mc --8<-- "docs_src/asgi/tutorial004.py" ``` -ēŽ°åœØå®¢ęˆ·ē«ÆčæžęŽ„ `/notes`ļ¼Œč€Œäøę˜Æ `/notes/mcp`怂 +ēŽ°åœØå®¢ęˆ·ē«ÆčæžęŽ„ `/notes/`ļ¼Œč€Œäøę˜Æ `/notes/mcp`怂 ## é¢å‘ęµč§ˆå™Øå®¢ęˆ·ē«Æēš„ CORS {#cors-for-browser-clients} @@ -127,4 +127,4 @@ Starlette ēš„ `Host` č·Æē”±ē”Øę³•ē›øåŒļ¼šęŠŠ `Mount("/", ...)` ę¢ęˆ `Host("mc * ęµč§ˆå™Øå®¢ęˆ·ē«Æéœ€č¦ CORS:`allow_headers` ę”¾č”Œ `Mcp-*` čÆ·ę±‚å¤“ļ¼Œ`expose_headers=["Mcp-Session-Id"]` å…¬å¼€å“åŗ”å¤“ć€‚ * `@mcp.custom_route()` 在 `/mcp` ę—č¾¹ę·»åŠ ę™®é€šēš„ć€äøåšč®¤čÆēš„ HTTP 端点。 -ęœåŠ”å™Øäø€ę—¦čƒ½é€ščæ‡ēœŸå®ž URL č®æé—®ļ¼Œ**[客户端](../client/index.md)** å°±åÆä»„ē”Øčæ™äøŖ URL č€Œäøę˜ÆęœåŠ”å™ØåÆ¹č±”ę„čæžęŽ„å®ƒć€‚ +ęœåŠ”å™Øäø€ę—¦čƒ½é€ščæ‡ēœŸå®ž URL č®æé—®ļ¼Œ**[客户端](../client/index.md)** 就用这个 URL ę„čæžęŽ„å®ƒć€‚ diff --git a/i18n/zh/pages/run/authorization.md b/i18n/zh/pages/run/authorization.md index 5b7de60d..f3d2a3de 100644 --- a/i18n/zh/pages/run/authorization.md +++ b/i18n/zh/pages/run/authorization.md @@ -1,6 +1,6 @@ --- translation: - sections: [d62c13457fc4a534, 80e73abaca6e0652, d1dc4c54cd00ec9c, 14ad3bc7904036bb, 5225f127bc1b9c77, fe1626fdd5aad1da, 4556cb7ea1a04a31] + sections: [d62c13457fc4a534, 80e73abaca6e0652, 128a492d18295f64, 14ad3bc7904036bb, 54a6697833fcc1d9, fe1626fdd5aad1da, 811d083c1da8bcf6] tool: 1 --- # ꎈꝃ {#authorization} @@ -23,12 +23,12 @@ translation: ęœ‰ę•ˆēš„ token é•æä»€ä¹ˆę ·ļ¼ŒSDK ę²”ęœ‰ä»»ä½•é¢„č®¾ć€‚čæ™ē”±ä½ ę„å†³å®šļ¼Œę–¹å¼ę˜Æå®žēŽ° **`TokenVerifier`**: -```python title="server.py" hl_lines="12-14 19-24" +```python title="server.py" hl_lines="14-16 21-27" --8<-- "docs_src/authorization/tutorial001.py" ``` * `TokenVerifier` ę˜Æäø€äøŖåŖęœ‰äø€äøŖå¼‚ę­„ę–¹ę³•ēš„åč®®ć€‚`verify_token` ꎄꔶ `Authorization` å¤“é‡Œēš„åŽŸå§‹ tokenļ¼Œęœ‰ę•ˆę—¶čæ”å›žäø€äøŖ **`AccessToken`**ļ¼Œę— ę•ˆę—¶čæ”å›ž `None`ć€‚é™¤ę­¤ä¹‹å¤–ę²”ęœ‰åˆ«ēš„č¦å®žēŽ°ć€‚ -* čæ™äøŖä¾‹å­ę˜ÆåœØäø€å¼ č”Øé‡ŒęŸ„ę‰¾ tokenć€‚ēœŸå®žēš„å®žēŽ°ä¼šéŖŒčÆ JWT ē­¾åļ¼Œęˆ–č€…č°ƒē”ØęŽˆęƒęœåŠ”å™Øēš„ token č‡Ŗēœē«Æē‚¹ć€‚é‚£éƒØåˆ†ä»£ē ę˜Æä½ ēš„ļ¼ŒSDK åŖč“Ÿč“£č°ƒē”Øå®ƒć€‚ +* čæ™äøŖä¾‹å­ę˜ÆåœØäø€å¼ č”Øé‡ŒęŸ„ę‰¾ tokenļ¼Œč”Øäø­ęÆäø€é”¹éƒ½č®°å½•äŗ†å®ƒę˜Æäøŗå“ŖäøŖčµ„ęŗē­¾å‘ēš„ć€‚ēœŸå®žēš„å®žēŽ°ä¼šéŖŒčÆ JWT ē­¾åļ¼Œęˆ–č€…č°ƒē”ØęŽˆęƒęœåŠ”å™Øēš„ token č‡Ŗēœē«Æē‚¹ļ¼Œå¹¶åœØ `AccessToken.resource` é‡ŒęŠ„å‘Š token ę˜Æē­¾å‘ē»™č°ēš„ļ¼ˆå®ƒēš„ `aud`ļ¼‰ć€‚é‚£éƒØåˆ†ä»£ē ę˜Æä½ ēš„ļ¼ŒSDK åŖč“Ÿč“£č°ƒē”Øå®ƒć€‚ * `token_verifier=` 和 `auth=` ę°øčæœęˆåÆ¹å‡ŗēŽ°ć€‚åŖä¼ å…¶äø­äø€äøŖļ¼Œ`MCPServer(...)` ä¼šåœØå¤„ē†ä»»ä½•čÆ·ę±‚ä¹‹å‰å°±ęŠ›å‡ŗ `ValueError`怂 `AuthSettings` ę˜Æä½ ēš„čµ„ęŗęœåŠ”å™ØåÆ¹å¤–ēš„é—Øé¢ļ¼š @@ -36,6 +36,10 @@ translation: * `issuer_url`ļ¼šē­¾å‘ä½ ēš„ token ēš„ęŽˆęƒęœåŠ”å™Øć€‚ * `resource_server_url`ļ¼ščæ™äøŖ MCP ē«Æē‚¹ēš„å…¬å¼€ URLć€‚å®ƒęŒ‡ę˜Ž token ę˜Æé’ˆåÆ¹**哪一个**čµ„ęŗēš„ļ¼Œå‘ēŽ°ę–‡ę”£ä¹Ÿä½äŗŽčæ™é‡Œć€‚ * `required_scopes`ļ¼šęÆäøŖ token éƒ½åæ…é”»ęŗåø¦å…¶äø­å…ØéƒØ scope怂 +* `validate_token_resource`ļ¼šę‹’ē»ä»»ä½• `AccessToken.resource` äøē­‰äŗŽ `resource_server_url` ēš„ token。设置了 `resource_server_url` å“äøč®¾ē½®å®ƒļ¼Œä¼šå‘å‡ŗč­¦å‘Šļ¼ˆ`MCPDeprecationWarning`ļ¼‰ļ¼Œč”Œäøŗē­‰åŒäŗŽ `False`ļ¼›3.0 čµ·čµ„ęŗęœåŠ”å™Øēš„é»˜č®¤å€¼ä¼šå˜ęˆ `True`怂 + * å¦‚ęžœä½ ēš„ęŽˆęƒęœåŠ”å™ØęŠŠ token ē»‘å®šåˆ°å®¢ęˆ·ē«ÆčÆ·ę±‚ēš„ `resource`(MCP å®¢ęˆ·ē«Æę€»ę˜Æä¼šå‘é€å®ƒļ¼‰ļ¼Œå°±ę‰“å¼€å®ƒć€‚č®© `resource_server_url` äøŽå®¢ęˆ·ē«ÆčæžęŽ„ēš„ URL å®Œå…Øäø€č‡“ć€‚ + * å¦‚ęžœä½ ēš„ęŽˆęƒęœåŠ”å™Øä½æē”Øč‡Ŗå·±ēš„å—ä¼—ę ‡čÆ†ē¬¦ļ¼ˆAuth0 ēš„ API 标识符、Entra ēš„åŗ”ē”Ø IDļ¼‰ļ¼Œå°±äæęŒå…³é—­ļ¼Œę”¹äøŗåœØéŖŒčÆå™Øé‡Œę£€ęŸ„ `aud`ļ¼ŒåÆ¹äøå±žäŗŽęœ¬ęœåŠ”å™Øēš„ token čæ”å›ž `None`怂 + * å¦‚ęžœ `aud` ę˜Æåˆ—č”Øļ¼ŒęŠŠå…¶äø­ē­‰äŗŽ `resource_server_url` ēš„é‚£äø€é”¹ę”¾čæ› `resource`怂 !!! tip "ęē¤ŗ" SDK ä»“åŗ“äø­ēš„ `examples/servers/simple-auth/` ęœ‰äø€äøŖ `IntrospectionTokenVerifier`ļ¼Œå®ƒä¼šč°ƒē”ØēœŸå®žęŽˆęƒęœåŠ”å™Øēš„ [RFC 7662](https://datatracker.ietf.org/doc/html/rfc7662) ē«Æē‚¹ć€‚å¤§å¤šę•°ē”Ÿäŗ§ēŽÆå¢ƒēš„éŖŒčÆå™Øéƒ½ę˜Æčæ™äøŖę ·å­ć€‚ @@ -85,7 +89,7 @@ translation: åœØä»»ä½•å¤„ē†å‡½ę•°å†…éƒØļ¼Œ**`get_access_token()`** å°±ę˜Æä½ ēš„éŖŒčÆå™Øäøŗå½“å‰čÆ·ę±‚čæ”å›žēš„é‚£äøŖ `AccessToken`: -```python title="server.py" hl_lines="4 32-35" +```python title="server.py" hl_lines="4 35-38" --8<-- "docs_src/authorization/tutorial002.py" ``` @@ -117,6 +121,6 @@ SDK ē»™ä½ ēš„ę˜Æčµ„ęŗęœåŠ”å™Øčæ™äø€åŠļ¼šéŖŒčÆć€å…¬åøƒć€ę‹’ē»ć€‚å®ƒäøę * `token_verifier=` 和 `auth=AuthSettings(issuer_url=..., resource_server_url=..., required_scopes=[...])` ę°øčæœęˆåÆ¹å‡ŗēŽ°ć€‚ * SDK 在 `/.well-known/oauth-protected-resource/...` å‘åøƒ [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) Protected Resource Metadataļ¼Œå¹¶åÆ¹ęœŖč®¤čÆēš„čÆ·ę±‚å›žåŗ” 401ļ¼Œå…¶ `WWW-Authenticate` å¤“ęŒ‡å‘čÆ„ę–‡ę”£ć€‚ę•“äøŖå‘ēŽ°čæ‡ēØ‹å°±ę˜Æčæ™ę ·ć€‚ * åœØä»»ä½•å¤„ē†å‡½ę•°é‡Œļ¼Œ`get_access_token()` å°±ę˜Æč°ƒē”Øč€…ę˜Æč°ć€‚ -* ęŽˆęƒę˜Æ HTTP å±‚é¢ēš„äŗ‹ć€‚`stdio` å’Œå†…å­˜å†…å®¢ęˆ·ē«Æę°øčæœēœ‹äøåˆ°å®ƒć€‚ +* ęŽˆęƒę˜Æ HTTP å±‚é¢ēš„äŗ‹ć€‚`stdio` å’Œå†…å­˜å†…ęµ‹čÆ•å®¢ęˆ·ē«Æę°øčæœēœ‹äøåˆ°å®ƒć€‚ å®¢ęˆ·ē«Æé‚£äø€åŠļ¼ˆå‘ēŽ°ä½ ēš„ęŽˆęƒęœåŠ”å™Øå¹¶ę›æä½ čŽ·å– token)见 **[OAuth 客户端](../client/oauth-clients.md)**ć€‚č€Œäø€äøŖ**断言**čŗ«ä»½ć€č€Œäøę˜Æå‘ē”Øęˆ·ē“¢č¦čŗ«ä»½ēš„å®¢ęˆ·ē«Æļ¼Œč§ **[身份断言](../client/identity-assertion.md)**怂 diff --git a/i18n/zh/pages/run/deploy.md b/i18n/zh/pages/run/deploy.md index fedd4a5d..d6dc95d0 100644 --- a/i18n/zh/pages/run/deploy.md +++ b/i18n/zh/pages/run/deploy.md @@ -1,6 +1,6 @@ --- translation: - sections: [28221886b198784f, f88ea1f1614f3a1d, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, e758745df6fb7b0a] + sections: [28221886b198784f, f88ea1f1614f3a1d, 2e76f5cb9df15042, ce926d686730b6d0, 3be24f8ad8bb5ab9, 3fad24032b2224ff, f25a7f860e579ecb, 697b01d95080880d] tool: 1 --- # éƒØē½²äøŽę‰©å±• {#deploy-scale} @@ -42,6 +42,22 @@ translation: åœØå®¢ęˆ·ē«Æé‚£č¾¹ä½ ę‰¾äøåˆ°čæ™å‡ äøŖå­—ć€‚`421` ę˜ÆēŗÆę–‡ęœ¬ēš„ HTTP å“åŗ”ļ¼Œäøę˜Æ JSON-RPC é”™čÆÆļ¼Œę‰€ä»„ MCP å®¢ęˆ·ē«ÆęŠ›å‡ŗēš„ę˜Æäø€äøŖę³›ę³›ēš„ä¼ č¾“é”™čÆÆļ¼›å®ƒäøč®¤åÆēš„é‚£äøŖäø»ęœŗååŖå‡ŗēŽ°åœØ**ęœåŠ”å™Ø**ēš„ę—„åæ—é‡Œļ¼Œę˜Æäø€ę”č­¦å‘Šć€‚äø€äøŖåˆšéƒØē½²å„½ć€ę‹’ē»ę‰€ęœ‰čæžęŽ„ēš„ęœåŠ”å™Øļ¼ŒåœØčÆę˜Žę˜Æåˆ«ēš„åŽŸå› ä¹‹å‰ļ¼Œå°±ę˜Æ Host ē™½åå•ēš„é—®é¢˜ć€‚**[ę•…éšœęŽ’ęŸ„](../troubleshooting.md)** ä¹Ÿä»Žčæ™é‡Œč®²čµ·ć€‚ +## 在终止 TLS ēš„ä»£ē†åŽé¢ {#behind-a-tls-terminating-proxy} + +å¦‚ęžœ TLS åœØä»£ē†å¤„ē»ˆę­¢ļ¼ˆingressć€č“Ÿč½½å‡č””å™Øć€Caddy态nginxļ¼‰ļ¼Œuvicorn åœØå®ƒåŽé¢ęä¾›ę˜Žę–‡ HTTP,就要让 uvicorn äæ”ä»»ä»£ē†ēš„ `X-Forwarded-*` 夓: + +```console +uvicorn server:app --proxy-headers --forwarded-allow-ips='' +``` + +äøčæ™ę ·åšļ¼Œåŗ”ē”Øä¼šä»„äøŗč‡Ŗå·±ę˜Æé€ščæ‡ `http://` ęä¾›ęœåŠ”ēš„ļ¼Œå®ƒå‘å‡ŗēš„ä»»ä½•é‡å®šå‘ļ¼ˆęœ€åøøč§ēš„ę˜Æ `/mcp` → `/mcp/`ļ¼‰éƒ½ęŒ‡å‘ `http://…`怂Python å®¢ęˆ·ē«Æę‹’ē»ä»Ž HTTPS ē«Æē‚¹č·Ÿéšåˆ°ę˜Žę–‡ HTTPļ¼Œå¹¶ę˜Žē”®ęŠ„é”™ļ¼š + +```text +MCPError: Redirect to http://mcp.example.com/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +``` + +å®¢ęˆ·ē«Æäø€ä¾§ēš„ęƒå®œä¹‹č®”ę˜Æé…ē½®ęœåŠ”å™Øå®žé™…ęä¾›ēš„ē”®åˆ‡ URL(`https://mcp.example.com/mcp/`ļ¼ŒåŒ…å«ē»“å°¾ę–œę ļ¼‰ļ¼Œčæ™ę ·å°±äøä¼šå‘ē”Ÿé‡å®šå‘ć€‚ēœŸę­£ēš„äæ®å¤ę˜ÆäøŠé¢ēš„ę ‡åæ—ć€‚`FORWARDED_ALLOW_IPS` ę˜Æå®ƒēš„ēŽÆå¢ƒå˜é‡å†™ę³•ļ¼›`*` äæ”ä»»ęÆäø€č·³ļ¼ŒåŖęœ‰åœØé™¤äŗ†ä»£ē†ä¹‹å¤–ę²”ęœ‰åˆ«ēš„äøœč„æčƒ½č®æé—®åˆ° uvicorn ę—¶ę‰åˆé€‚ć€‚ + ## Workerļ¼Œä»„åŠč°éœ€č¦ē²˜ę€§ {#workers-and-who-has-to-be-sticky} äø»ęœŗåčƒ½å“åŗ”ä¹‹åŽļ¼Œå°±åœØåŽé¢ę”¾äøę­¢äø€äøŖ worker怂SDK ę²”ęœ‰čæ™ę–¹é¢ēš„å¼€å…³ļ¼›ę‰©å±•äø€äøŖ Starlette åŗ”ē”Øå’Œę‰©å±•ä»»ä½• ASGI åŗ”ē”Øäø€ę ·ļ¼ŒęŠŠåÆ¹č±”äŗ¤ē»™äø€äøŖä¼š fork ēš„äøœč„æļ¼š @@ -154,6 +170,7 @@ python -c "import secrets; print(secrets.token_hex(32))" ## 回锾 {#recap} * é»˜č®¤ęƒ…å†µäø‹ļ¼Œčæ™äøŖåŗ”ē”ØåŖå“åŗ”å‘å¾€ localhost ēš„čÆ·ę±‚ć€‚`transport_security=TransportSecuritySettings(allowed_hosts=[...], allowed_origins=[...])` ę˜ÆäøŠēŗæēš„å…³å”ļ¼šåœØä½ ä¼ å…„å®ƒä¹‹å‰ļ¼ŒēœŸå®žäø»ęœŗååŽé¢ēš„ęÆäøŖčÆ·ę±‚éƒ½ę˜Æ `421`ļ¼ŒåŽŸå› åŖåœØęœåŠ”å™Øę—„åæ—é‡Œć€‚ +* 在终止 TLS ēš„ä»£ē†åŽé¢ļ¼Œē”Ø `--proxy-headers --forwarded-allow-ips=...` 运蔌 uvicornļ¼Œå¦åˆ™å®ƒēš„é‡å®šå‘ęŒ‡å‘ `http://`ļ¼Œå®¢ęˆ·ē«Æä¼šę‹’ē»č·Ÿéšć€‚ * 在 2026-07-28 äøŠę²”ęœ‰ä¼ščÆļ¼Œč“Ÿč½½å‡č””å™Øę²”ęœ‰ä»€ä¹ˆåÆē²˜ēš„ć€‚`stateless_http=True` ę˜ÆåŖåÆ¹ę—§ē‰ˆęœ‰ę•ˆēš„å¼€å…³ļ¼Œå› äøŗēŽ°ä»£čÆ·ę±‚åœØčÆ»åˆ°čæ™äøŖę ‡åæ—ä¹‹å‰å°±å·²ē»č¢«č·Æē”±å¹¶å“åŗ”äŗ†ć€‚ * é»˜č®¤ēš„ `requestState` åÆ†é’„ę˜Æ `os.urandom(32)`ļ¼ŒęŒ‰čæ›ēØ‹é“øé€ ć€‚åˆ°č¾¾å¦äø€äøŖ worker ēš„å¤šč½®å¾€čæ”é‡čÆ•ä¼šä»„ `-32602` ā€œInvalid or expired requestStateā€ 失蓄。 * äæ®å¤åŠžę³•ę˜Æ `RequestStateSecurity(keys=[...])` **å¹¶äø”**ęÆäøŖå®žä¾‹ä½æē”Øē›øåŒēš„ęœåŠ”å™Øåå­—ć€‚åå­—ę˜Æä»¤ē‰Œé»˜č®¤ēš„ audience å£°ę˜Žć€‚ē›øåŒēš„åÆ†é’„ļ¼Œē›øåŒēš„åå­—ć€‚ diff --git a/i18n/zh/pages/run/index.md b/i18n/zh/pages/run/index.md index fb8425f5..b2914c11 100644 --- a/i18n/zh/pages/run/index.md +++ b/i18n/zh/pages/run/index.md @@ -1,6 +1,6 @@ --- translation: - sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 2fd7cf825e6d2b2c, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] + sections: [fea8d769ff9edeba, ce8e2ad42f29ef71, 0d705efb19cf99c2, 9fd2357154a5b7e7, 9adc400e8c88e854, 318893ad8e2e9924, 6b63ab96b34476c0] tool: 1 --- # čæč”ŒęœåŠ”å™Ø {#running-your-server} @@ -71,6 +71,8 @@ Inspector åšēš„äŗ‹å’ŒēœŸå®žå®æäø»å®Œå…Øäø€ę ·ļ¼šå®ƒęŠŠ `server.py` ä½œäøŗå­čæ› * `json_response=True`ļ¼šē”Øå•äøŖ JSON ę­£ę–‡å›žåŗ”ęÆäøŖ POSTļ¼Œč€Œäøę˜Æ SSE ęµć€‚čæ™äøŖę­£ę–‡åŖå®¹å¾—äø‹å“åŗ”ęœ¬čŗ«ļ¼Œåˆ«ēš„ä»€ä¹ˆéƒ½ę”¾äøäø‹ļ¼Œę‰€ä»„åœØčÆ·ę±‚äø­é€”å›žč°ƒå®¢ęˆ·ē«Æēš„å·„å…·ļ¼ˆ`ctx.elicit()`ć€é‡‡ę ·ļ¼ˆsamplingļ¼‰ļ¼‰ä¼šåœØčæ™äø€ę®µęŠ›å‡ŗ `NoBackChannelError`ļ¼›äøŽčæ›č”Œäø­ēš„č°ƒē”Øē»‘å®šēš„é€šēŸ„ļ¼ˆ`ctx.report_progress()` ēš„čæ›åŗ¦ć€ęÆę¬”č°ƒē”Øēš„ę—„åæ—ę¶ˆęÆļ¼‰ä¼šč¢«äø¢å¼ƒļ¼›ē‹¬ē«‹ēš„ `GET` ęµä»ē„¶ę‰æč½½äøŽä¹‹ę— å…³ēš„é€šēŸ„ć€‚ * `stateless_http=True`ļ¼šęÆäøŖčÆ·ę±‚äø€äøŖå…Øę–°ēš„ä¼ č¾“ļ¼Œäøč·ŸčøŖä¼ščÆć€‚ * `max_request_body_size`ļ¼šęŽ„å—ēš„ęœ€å¤§čÆ·ę±‚ę­£ę–‡å¤§å°ļ¼Œå•ä½äøŗå­—čŠ‚ć€‚é»˜č®¤ 4 MiBļ¼›ę›“å¤§ēš„čÆ·ę±‚åœØč§£ęžęˆ–åˆ›å»ŗä¼ščÆä¹‹å‰å°±ä¼šę”¶åˆ° HTTP 413ć€‚åŖęœ‰å½“åˆę³•ēš„ MCP ę¶ˆęÆē”®å®žč¶…čæ‡čæ™äøŖå¤§å°ę—¶ę‰č°ƒé«˜å®ƒć€‚ +* `session_idle_timeout`ļ¼šę—§ē‰ˆä¼ščÆåœØę²”ęœ‰ä»»ä½•čæ›č”Œäø­čÆ·ę±‚ēš„ęƒ…å†µäø‹åÆä»„é—²ē½®å¤šå°‘ē§’ļ¼Œä¹‹åŽęœåŠ”å™Øä¼šå…³é—­å®ƒć€‚é»˜č®¤ 1800怂`None` 蔨示禁用。见[ä¼ščÆē”Ÿå­˜ęœŸäøŽé™åˆ¶](legacy-clients.md#session-lifetime-and-limits)怂 +* `max_sessions`ļ¼šäø€äøŖčæ›ēØ‹åŒę—¶ęŒęœ‰å¤šå°‘äøŖę—§ē‰ˆä¼ščÆć€‚é»˜č®¤ 10 000怂`None` č”Øē¤ŗäøč®¾äøŠé™ć€‚åŒäø€čŠ‚é‡Œęœ‰ä»‹ē»ć€‚ * `event_store`态`retry_interval`态`transport_security`ļ¼šåÆę¢å¤ę€§å’Œ DNS é‡ē»‘å®šé˜²ęŠ¤ć€‚å®ƒä»¬åÆä»„å…ˆę”¾äø€ę”¾ļ¼Œē­‰éƒØē½²åˆ° localhost ä»„å¤–ēš„åœ°ę–¹å†čÆ“ļ¼›**[éƒØē½²äøŽę‰©å±•](deploy.md)** ä»‹ē»äŗ† `transport_security`怂 !!! warning diff --git a/i18n/zh/pages/run/legacy-clients.md b/i18n/zh/pages/run/legacy-clients.md index 2fe246dc..ce821f75 100644 --- a/i18n/zh/pages/run/legacy-clients.md +++ b/i18n/zh/pages/run/legacy-clients.md @@ -1,6 +1,6 @@ --- translation: - sections: [3d1663c18edc824c, d4fd37009a13f03d, af9f398a5a8b679a, 470c2dd144294d69, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] + sections: [3d1663c18edc824c, 90956965ae6a1ca1, af9f398a5a8b679a, 5ce83b1f9d88da62, 0d9b5d13fffc94e5, 8e45827e6d24e8c8, 91dfd0ce98ebb03c] tool: 1 --- # ęœåŠ”ę—§ē‰ˆå®¢ęˆ·ē«Æ {#serving-legacy-clients} @@ -18,15 +18,25 @@ SDK ꌉ `MCP-Protocol-Version` å¤“č·Æē”±ęÆäøŖčÆ·ę±‚ć€‚å£°ę˜Ž `2026-07-28` ēš„ ## äø€äøŖå¤„ē†å‡½ę•°ļ¼Œäø¤äøŖę—¶ä»£ {#one-handler-both-eras} -äø‹é¢ę˜Æäø€äøŖéœ€č¦å‘ē”Øęˆ·ęé—®ēš„å·„å…·ļ¼Œä»„åŠäø¤äøŖę—¶ä»£ēš„å®¢ęˆ·ē«Æåˆ†åˆ«č°ƒē”Øå®ƒļ¼š +äø‹é¢ę˜Æäø€äøŖéœ€č¦å‘ē”Øęˆ·ęé—®ēš„å·„å…·ļ¼š -```python title="server.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` `reserve` éœ€č¦äø€ę ·ęØ”åž‹ę²”ęœ‰ęä¾›ēš„äøœč„æļ¼šč¦å‡ ęœ¬ć€‚å·„å…·ē”Ø `Annotated[..., Resolve(ask_quantity)]` ę„å£°ę˜Žčæ™äø€ē‚¹ļ¼ˆčÆ¦č§ **[ä¾čµ–](../handlers/dependencies.md)**)。`reserve` é‡Œę²”ęœ‰ä»»ä½•åœ°ę–¹ęåˆ°ē‰ˆęœ¬ć€ę£€ęŸ„čƒ½åŠ›ęˆ–åšåˆ†ę”Æć€‚ -两个客户端**åŒę—¶**ę‰“å¼€ļ¼Œčæžēš„ę˜ÆåŒäø€äøŖ `mcp` 对豔。`mode="legacy"` ä¼šę‰§č”Œ `initialize` ę”ę‰‹ļ¼ščæ™ę­£ę˜Æ 2026 ä¹‹å‰ēš„å®¢ęˆ·ē«Æę‰“å¼€ēš„é‚£ē§čæžęŽ„ć€‚å¦äø€äøŖå–é»˜č®¤å€¼ļ¼Œč½åœØ `2026-07-28` äøŠć€‚ +é€ščæ‡ HTTP ęä¾›ęœåŠ”ļ¼Œäø‹é¢ę˜Æäø¤äøŖę—¶ä»£ēš„å®¢ęˆ·ē«Æåˆ†åˆ«č°ƒē”Øå®ƒļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +两个客户端**åŒę—¶**ę‰“å¼€ļ¼Œčæžēš„ę˜ÆåŒäø€äøŖę­£åœØčæč”Œēš„ęœåŠ”å™Øć€‚`mode="legacy"` ä¼šę‰§č”Œ `initialize` ę”ę‰‹ļ¼ščæ™ę­£ę˜Æ 2026 ä¹‹å‰ēš„å®¢ęˆ·ē«Æę‰“å¼€ēš„é‚£ē§čæžęŽ„ć€‚å¦äø€äøŖå–é»˜č®¤å€¼ļ¼Œč½åœØ `2026-07-28` äøŠć€‚åœØē¬¬äŗŒäøŖē»ˆē«Æčæč”Œ `python client.py`: ```text 2025-11-25 {'result': "Reserved 2 of 'Dune'."} @@ -53,6 +63,31 @@ SDK ꌉ `MCP-Protocol-Version` å¤“č·Æē”±ęÆäøŖčÆ·ę±‚ć€‚å£°ę˜Ž `2026-07-28` ēš„ !!! warning `event_store=` ēœ‹čµ·ę„åƒę˜Æč§£å†³åŠžę³•ļ¼Œå…¶å®žäøę˜Æć€‚å®ƒę˜Æ**åÆę¢å¤ę€§**ļ¼ˆå‘é‡čæžåˆ°**åŒäø€äøŖ**ä¼ščÆēš„å®¢ęˆ·ē«Æé‡ę”¾é”™čæ‡ēš„ SSE äŗ‹ä»¶ļ¼‰ļ¼Œäøę˜Æä¼ščÆå­˜å‚Øć€‚å®ƒę°øčæœäøä¼šč®©äø€äøŖä¼ščÆčƒ½ä»Žå¦äø€äøŖčæ›ēØ‹č®æé—®åˆ°ć€‚ +## ä¼ščÆē”Ÿå­˜ęœŸäøŽäøŠé™ {#session-lifetime-and-limits} + +ę—§ē‰ˆä¼ščÆäøä¼šę°øčæœå­˜ę“»ļ¼Œäø€äøŖčæ›ēØ‹ä¹Ÿäøä¼šęŒęœ‰ę— é™å¤šäøŖä¼ščÆć€‚ęœ‰äø¤äøŖč®¾ē½®ęŽ§åˆ¶čæ™äø€ē‚¹ć€‚äø¤č€…éƒ½ę˜Æ `run()`态`streamable_http_app()` 和 `Server.streamable_http_app()` äøŠēš„å…³é”®å­—å‚ę•°ć€‚ēŽ°ä»£ļ¼ˆ`2026-07-28`ļ¼‰čæžęŽ„å’Œ `stateless_http=True` ę²”ęœ‰ä¼ščÆļ¼Œę‰€ä»„čæ™äø¤äøŖč®¾ē½®åÆ¹å®ƒä»¬éƒ½äøé€‚ē”Øć€‚ + +| 设置 | é»˜č®¤å€¼ | ä½œē”Ø | å®¢ęˆ·ē«Æēœ‹åˆ°ä»€ä¹ˆ | å…³é—­ę–¹å¼ | +|---|---|---|---|---| +| `session_idle_timeout` | `1800`(30 åˆ†é’Ÿļ¼‰ | å…³é—­åœØčæ™ä¹ˆé•æę—¶é—“é‡Œę²”ęœ‰ä»»ä½•čæ›č”Œäø­äŗ‹åŠ”ēš„ä¼ščÆć€‚ | `404 Session not found`ć€‚å®ƒåæ…é”»é‡ę–° `initialize`怂 | `None` | +| `max_sessions` | `10_000` | č¶…čæ‡čæ™äøŖę•°é‡å°±ę‹’ē»å†ę‰“å¼€ä¼ščÆć€‚ēŽ°ęœ‰ä¼ščÆäøå—å½±å“ļ¼Œä¹Ÿäøä¼šé©±é€ä»»ä½•ä¼ščÆć€‚ | `503 Too many open sessions`,JSON-RPC 代码为 `-32603`怂 | `None` | + +ä»€ä¹ˆē®—ā€œčæ›č”Œäø­ā€ļ¼š + +* äø€äøŖę‰“å¼€ēš„ `GET` 굁怂SDK å®¢ęˆ·ē«Æä¼šäæęŒäø€äøŖę‰“å¼€ļ¼Œę‰€ä»„å·²čæžęŽ„å®¢ęˆ·ē«Æēš„ä¼ščÆę°øčæœäøä¼ščæ‡ęœŸć€‚ +* äø€äøŖä»åœØåŗ”ē­”äø­ēš„čÆ·ę±‚ć€‚čæč”Œę—¶é—“č¶…čæ‡č¶…ę—¶ēš„å·„å…·č°ƒē”Øäøä¼šč¢«äø­ę–­ļ¼Œå€’č®”ę—¶åœØå®ƒå®Œęˆä¹‹åŽę‰å¼€å§‹ć€‚ +* ę²”ęœ‰åˆ«ēš„äŗ†ć€‚čÆ·ę±‚ä¹‹é—“ę—¶é’Ÿē…§čµ°ć€‚ä¼ščÆäøŠēš„ä»»ä½•čÆ·ę±‚éƒ½ä¼šé‡ē½®å®ƒļ¼Œ`ping` ä¹Ÿē®—ć€‚ä¼ščÆäø€ę—¦čæ‡ęœŸļ¼Œä»€ä¹ˆéƒ½ę•‘äøå›žę„ć€‚ + +用 `DELETE` ē»“ęŸä¼ščÆēš„å®¢ęˆ·ē«Æä¼šē«‹å³é‡Šę”¾å®ƒć€‚å¼€åœŗčÆ·ę±‚č¢«ę‹’ē»ēš„å®¢ęˆ·ē«Æä¹Ÿę˜Æå¦‚ę­¤ć€‚ + +```python +mcp.run(transport="streamable-http", session_idle_timeout=None, max_sessions=50_000) +``` + +äø¤ē§äŗ‹ä»¶éƒ½ä¼šå‡ŗēŽ°åœØęœåŠ”å™Øę—„åæ—é‡Œć€‚čæ‡ęœŸę˜Æ `INFO` ēŗ§åˆ«ēš„ `Session idle timeout`ć€‚ę‹’ē»ę‰“å¼€ę˜Æ `WARNING` ēŗ§åˆ«ēš„ `Refusing to open a new session: sessions are already open`怂 + +čæ™äŗ›äøŠé™ęŒ‰čæ›ēØ‹č®”ć€‚ęœ‰å››äøŖ worker ę—¶äøŠé™ę˜Æ `max_sessions` ēš„å››å€ļ¼ŒęÆäøŖ worker å„č‡Ŗč®©č‡Ŗå·±ēš„ä¼ščÆčæ‡ęœŸć€‚ + ## å”Æäø€ēš„å¼€å…³ļ¼š`stateless_http` {#the-one-knob-stateless_http} å¦‚ęžœē²˜ę€§ę˜Æä½ äøę„æä»˜ēš„ä»£ä»·ļ¼Œé‚£ä¹ˆę°å„½ęœ‰äø€ę ·äøœč„æåÆä»„ę”¹ć€‚ @@ -73,7 +108,7 @@ SDK ꌉ `MCP-Protocol-Version` å¤“č·Æē”±ęÆäøŖčÆ·ę±‚ć€‚å£°ę˜Ž `2026-07-28` ēš„ `json_response=True` äøę˜Æé‚£äøŖå¼€å…³ļ¼Œä½†å®ƒåœØ**ęÆäø€äøŖ**ę—§ē‰ˆä¼ščÆäøŠéƒ½č¦ä»˜äø€åŠåŒę ·ēš„ä»£ä»·ļ¼šē”Øäø€äøŖ JSON ę­£ę–‡å›žē­”ēš„ `POST` ę²”ęœ‰ä¾›čÆ·ę±‚čŒƒå›“é€šé“ä½æē”Øēš„ęµļ¼Œę‰€ä»„čÆ·ę±‚äø­é€”ēš„ `ctx.elicit()` ä¼šęŠ›å‡ŗåŒę ·ēš„ `NoBackChannelError`ļ¼ŒäøŽčÆ„čÆ·ę±‚ē»‘å®šēš„é€šēŸ„ä¼šč¢«äø¢å¼ƒć€‚ä¼ščÆēš„ē‹¬ē«‹ęµäøå—å½±å“ļ¼šę— å…³ēš„é€šēŸ„ä»ē„¶čƒ½åˆ°č¾¾ć€‚ !!! check - ę•…ę„åšé”™äø€ę¬”ć€‚`reserve` å°±ę˜Æåˆšę‰åŒę—¶ęœåŠ”äø¤äøŖå®¢ęˆ·ē«Æēš„é‚£äøŖå·„å…·ć€‚ē”Ø `stateless_http=True` éƒØē½²å®ƒļ¼Œé€ščæ‡ HTTP čæžäøŠåŒę ·ēš„äø¤äøŖå®¢ęˆ·ē«Æļ¼Œåˆ†åˆ«č°ƒē”Øå®ƒć€‚ + ę•…ę„åšé”™äø€ę¬”ć€‚`reserve` å°±ę˜Æåˆšę‰åŒę—¶ęœåŠ”äø¤äøŖå®¢ęˆ·ē«Æēš„é‚£äøŖå·„å…·ć€‚ē”Ø `stateless_http=True` éƒØē½²å®ƒļ¼ŒčæžäøŠåŒę ·ēš„äø¤äøŖå®¢ęˆ·ē«Æļ¼Œåˆ†åˆ«č°ƒē”Øå®ƒć€‚ ēŽ°ä»£å®¢ęˆ·ē«Æä»ē„¶ę”¶åˆ° `Reserved 2 of 'Dune'.`ļ¼ŒēŽ°ä»£čæ™äø€č·Æę²”å˜ć€‚ diff --git a/i18n/zh/pages/troubleshooting.md b/i18n/zh/pages/troubleshooting.md index ca4cfc78..ee6f7a6b 100644 --- a/i18n/zh/pages/troubleshooting.md +++ b/i18n/zh/pages/troubleshooting.md @@ -1,6 +1,6 @@ --- translation: - sections: [2efaecdef109a5c5, fcacd3e66b8635a4, 25323d737dcf0261, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 323ef84f6b4bebde, 30fd31be74169d9a, 656943c6cb567218, c2dc3b1007d2e987, 7cf5386b997d04e9, 0b59feed8384456e, 0cba47bae78d04eb, e4355f4c7cf4fb2e] + sections: [3d58228e81b99543, 170514ce901c4139, 17d61fad0a50d62b, 8a6e351ec756904d, 137454d469c867f5, 6392596bd6df54f0, 41126fa9c4fe432f, 480b6d7897e30ab4, d83bb682e708dde0, ebbed3449c499db4, 525cdf1755e29d4c, 30fd31be74169d9a, d2e88333d4f7841f, c2dc3b1007d2e987, d6eabf60cc366341, f798e815252852c2, 0cba47bae78d04eb, 2c218ba829abf74e] tool: 1 --- # ę•…éšœęŽ’ęŸ„ {#troubleshooting} @@ -13,6 +13,12 @@ translation: --8<-- "docs_src/troubleshooting/tutorial001.py" ``` +čæ™äŗ›ę”ē›®é€ščæ‡ `http://localhost:8000/mcp` č®æé—®å®ƒļ¼Œę‰€ä»„č®©å®ƒä»„ HTTP ę–¹å¼äæęŒčæč”Œļ¼š + +```console +uv run mcp run server.py --transport streamable-http +``` + ęœ¬é”µå¼•ē”Øēš„é”™čÆÆéƒ½ę˜ÆēœŸå®žēš„ļ¼šSDK č‡Ŗå·±ēš„ęµ‹čÆ•å„—ä»¶å¤ēŽ°äŗ†å…¶äø­ęÆäø€ę”ć€‚ ## `ExceptionGroup: unhandled errors in a TaskGroup (1 sub-exception)` {#exceptiongroup-unhandled-errors-in-a-taskgroup-1-sub-exception} @@ -23,7 +29,7 @@ translation: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.read_resource("weather://Atlantis") ``` @@ -49,7 +55,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: try: await client.read_resource("weather://Atlantis") except MCPError as e: @@ -65,7 +71,7 @@ async def main() -> None: ```python async def main() -> None: - client = Client(mcp) + client = Client("http://localhost:8000/mcp") tools = await client.list_tools() # RuntimeError ``` @@ -73,7 +79,7 @@ async def main() -> None: ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: tools = await client.list_tools() ``` @@ -245,7 +251,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ## `MCPError: Session not found` {#mcperror-session-not-found} -ęœåŠ”å™Øäøč®¤čÆ†å®¢ęˆ·ē«Æå‘ę„ēš„ `Mcp-Session-Id`ļ¼Œå‡ ä¹Žę€»ę˜Æå› äøŗęœåŠ”å™Ø**é‡åÆäŗ†**ļ¼ˆęˆ–č€…ä½ č¢«č·Æē”±åˆ°äŗ†å¦äø€äøŖå®žä¾‹ļ¼‰ć€‚ä¼ščÆå­˜ę“»åœØé‚£äø€äøŖčæ›ēØ‹ēš„å†…å­˜é‡Œć€‚ +ęœåŠ”å™Øäøč®¤čÆ†å®¢ęˆ·ē«Æå‘ę„ēš„ `Mcp-Session-Id`ć€‚č¦ä¹ˆęœåŠ”å™Ø**é‡åÆäŗ†**ļ¼ˆęˆ–č€…ä½ č¢«č·Æē”±åˆ°äŗ†å¦äø€äøŖå®žä¾‹ļ¼‰ļ¼Œč¦ä¹ˆä¼ščÆ**čæ‡ęœŸäŗ†**ā€”ā€”åœØ `session_idle_timeout`(默认 30 åˆ†é’Ÿļ¼‰å†…ę²”ęœ‰ä»»ä½•čæ›č”Œäø­ēš„čÆ·ę±‚ć€‚č§[ä¼ščÆē”Ÿå­˜ęœŸäøŽé™åˆ¶](run/legacy-clients.md#session-lifetime-and-limits)ć€‚ä¼ščÆå­˜ę“»åœØé‚£äø€äøŖčæ›ēØ‹ēš„å†…å­˜é‡Œć€‚ ę²”ęœ‰ęœåŠ”å™Ø bug åÆę‰¾ć€‚HTTP å“åŗ”ę˜Æ `404`ļ¼Œå®ƒēš„å“åŗ”ä½“**是** JSON-RPCļ¼Œę‰€ä»„å’ŒäøŠé¢ēš„ `421` äøåŒļ¼Œpython `Client` ä¼šęŠŠčæ™äø€ę”åŽŸę ·å±•ē¤ŗå‡ŗę„ļ¼š @@ -255,9 +261,9 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif äæ®å¤ę˜Æé‡čæžļ¼šē¦»å¼€ `async with Client(...)` å—ļ¼Œčæ›å…„äø€äøŖę–°ēš„ļ¼Œå®ƒä¼šåå•†äø€äøŖå…Øę–°ēš„ä¼ščÆć€‚åÆ¹äŗŽé•æę—¶é—“čæč”Œēš„å®¢ęˆ·ē«Æļ¼Œčæ™ę„å‘³ē€åœØč°ƒē”Øå¤–é¢ę•čŽ· `MCPError`ļ¼Œé‡åˆ°čæ™ę”ę¶ˆęÆå°±é‡čæžļ¼Œč€Œäøę˜ÆåœØę­»ęŽ‰ēš„ä¼ščÆé‡Œé‡čÆ•ć€‚ -å¦‚ęžœ**ę²”ęœ‰**é‡åÆä¹Ÿå‘ē”Ÿļ¼ŒčÆ“ę˜Žä½ č·‘äŗ†äøę­¢äø€äøŖ worker å“ę²”ęœ‰ē²˜ę€§ä¼ščÆļ¼šęÆäøŖ worker ęŒęœ‰č‡Ŗå·±ēš„ä¼ščÆč”Øļ¼Œę‰€ä»„č·Æē”±åˆ°é”™čÆÆ worker ēš„čÆ·ę±‚å°±č½åˆ°čæ™é‡Œć€‚čæ™ä»¶äŗ‹ä»„åŠå®ƒēš„äø¤ē§äæ®å¤ļ¼ˆē²˜ę€§č·Æē”±ļ¼Œęˆ– `stateless_http=True`)归 **[éƒØē½²äøŽę‰©å±•](run/deploy.md)** 和 **[ęœåŠ”ę—§ē‰ˆå®¢ęˆ·ē«Æ](run/legacy-clients.md)** 箔。 +å¦‚ęžœ**ę²”ęœ‰**é‡åÆć€å®¢ęˆ·ē«Æä¹Ÿę²”ęœ‰ę²‰é»˜é‚£ä¹ˆä¹…å“ä»ē„¶å‘ē”Ÿļ¼ŒčÆ“ę˜Žä½ č·‘äŗ†äøę­¢äø€äøŖ worker å“ę²”ęœ‰ē²˜ę€§ä¼ščÆļ¼šęÆäøŖ worker ęŒęœ‰č‡Ŗå·±ēš„ä¼ščÆč”Øļ¼Œę‰€ä»„č·Æē”±åˆ°é”™čÆÆ worker ēš„čÆ·ę±‚å°±č½åˆ°čæ™é‡Œć€‚čæ™ä»¶äŗ‹ä»„åŠå®ƒēš„äø¤ē§äæ®å¤ļ¼ˆē²˜ę€§č·Æē”±ļ¼Œęˆ– `stateless_http=True`)归 **[éƒØē½²äøŽę‰©å±•](run/deploy.md)** 和 **[ęœåŠ”ę—§ē‰ˆå®¢ęˆ·ē«Æ](run/legacy-clients.md)** 箔。 -åÆ¹ęœåŠ”å™Øčæē»“ę–¹ę„čÆ“ļ¼ŒåÆ¹åŗ”ēš„ę—„åæ—č”Œę˜Æ `Rejected request with unknown or expired session ID: `ć€‚å®ƒä»„ `INFO` ēŗ§åˆ«č®°å½•ļ¼Œę‰€ä»„åœØåøøē”Øēš„ `WARNING` é˜ˆå€¼äø‹ēœ‹äøåˆ°ć€‚éƒØē½²åŽé©¬äøŠęˆę‰¹å‡ŗēŽ°ę˜Æę­£åøøēš„ļ¼›ęÆäøŖå·²čæžęŽ„ēš„å®¢ęˆ·ē«Æéƒ½åœØé‡čæžć€‚ +åÆ¹ęœåŠ”å™Øčæē»“ę–¹ę„čÆ“ļ¼ŒåÆ¹åŗ”ēš„ę—„åæ—č”Œę˜Æ `Rejected request with unknown or expired session ID: `ć€‚å®ƒä»„ `INFO` ēŗ§åˆ«č®°å½•ļ¼Œę‰€ä»„åœØåøøē”Øēš„ `WARNING` é˜ˆå€¼äø‹ēœ‹äøåˆ°ć€‚éƒØē½²åŽé©¬äøŠęˆę‰¹å‡ŗēŽ°ę˜Æę­£åøøēš„ļ¼›ęÆäøŖå·²čæžęŽ„ēš„å®¢ęˆ·ē«Æéƒ½åœØé‡čæžć€‚å¦‚ęžœę˜Æä¼ščÆčæ‡ęœŸļ¼Œé‚£äø€č”Œå‰é¢ä¼šå…ˆęœ‰äø€ę” `Session idle timeout`,同样是 `INFO` ēŗ§åˆ«ć€‚ ## `MCPError: Method not found` {#mcperror-method-not-found} @@ -269,7 +275,13 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ęœåŠ”å™Øęƒ³é—®ē”Øęˆ·ē‚¹ä»€ä¹ˆļ¼Œč€Œčæ™äøŖå®¢ęˆ·ē«Æä»Žę²”čÆ“čæ‡č‡Ŗå·±åÆä»„č¢«é—®ć€‚ -征询(elicitationļ¼‰č§£ęžå™ØåœØå·²čæžęŽ„ēš„å®¢ęˆ·ē«Æę²”ęœ‰å£°ę˜Žč”Øå•å¾čÆ¢ę—¶ä¼šäø€å¼€å§‹å°±ę‹’ē»ļ¼Œ`e.error.data` ä¼šå‡†ē”®ęŒ‡å‡ŗē¼ŗäŗ†ä»€ä¹ˆļ¼š +这家 Bistro åœØč®¢ä½ä¹‹å‰ä¼šé€ščæ‡äø€äøŖč§£ęžå™Øå…ˆé—®äø€å„ļ¼š + +```python title="server.py" hl_lines="15-17 21" +--8<-- "docs_src/troubleshooting/tutorial007.py" +``` + +ē”Øå®ƒę›æę¢ Weather ęœåŠ”å™Øę„čæč”Œļ¼Œē„¶åŽä»Žäø€äøŖę²”ęœ‰ä¼  `elicitation_callback` ēš„å®¢ęˆ·ē«Æč°ƒē”Ø `book_table`ć€‚č§£ęžå™Øä¼šäø€å¼€å§‹å°±ę‹’ē»ļ¼Œå› äøŗå·²čæžęŽ„ēš„å®¢ęˆ·ē«Æä»Žę²”å£°ę˜Žčæ‡č”Øå•å¾čÆ¢ļ¼ˆelicitationļ¼‰ļ¼Œ`e.error.data` ä¼šå‡†ē”®ęŒ‡å‡ŗē¼ŗäŗ†ä»€ä¹ˆļ¼š ```json { @@ -283,7 +295,7 @@ app = Starlette(routes=[Mount("/", app=mcp.streamable_http_app())], lifespan=lif ```python async def main() -> None: - async with Client(mcp, elicitation_callback=handle_elicitation) as client: + async with Client("http://localhost:8000/mcp", elicitation_callback=handle_elicitation) as client: result = await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -302,14 +314,14 @@ async def main() -> None: å¤„ē†å‡½ę•°čÆ•å›¾åœØčÆ·ę±‚äø­é€”č”ē³»å®¢ęˆ·ē«Æļ¼Œč€ŒåœØčæ™ę”čæžęŽ„äøŠļ¼Œčæ™ę¬”č°ƒē”Øę²”ęœ‰ä»»ä½•čƒ½ę‰æč½½ęœåŠ”å™Øå‘å‡ŗčÆ·ę±‚ēš„é€šé“ć€‚ęœ‰äø‰ē§ęœåŠ”å™Øé…ē½®ä¼šęŠŠč°ƒē”Øē½®äŗŽčæ™ē§å¢ƒåœ°ć€‚ -**`2026-07-28` čæžęŽ„ļ¼šä»»ä½•ä¼ č¾“ę–¹å¼ļ¼Œę°øčæœå¦‚ę­¤ć€‚** ēŽ°ä»£åč®®ę ¹ęœ¬ę²”ęœ‰ęœåŠ”å™Øå‘čµ·ēš„čÆ·ę±‚ļ¼Œę‰€ä»„ęœåŠ”å™ØåœØå‘é€ä»»ä½•äøœč„æä¹‹å‰å°±ę‹’ē»äŗ†ć€‚å·„å…·é‡Œēš„ `ctx.elicit()` ę˜Æé‡åˆ°å®ƒēš„ē»å…øę–¹å¼ļ¼ˆå°±åœØē¬¬äø€ę¬”å†…å­˜ęµ‹čÆ•é‡Œļ¼Œå› äøŗ `Client(server)` äøē”Øč¦ę±‚å°±ä¼šåå•† `2026-07-28`ļ¼‰ļ¼Œč€Œä¼ å…„ `elicitation_callback=` ä»€ä¹ˆä¹Ÿę”¹å˜äøäŗ†ļ¼Œå› äøŗę ¹ęœ¬ę²”ęœ‰čÆ·ę±‚åˆ°č¾¾å®¢ęˆ·ē«Æč®©å®ƒåŽ»å›žē­”ļ¼š +**`2026-07-28` čæžęŽ„ļ¼šä»»ä½•ä¼ č¾“ę–¹å¼ļ¼Œę°øčæœå¦‚ę­¤ć€‚** ēŽ°ä»£åč®®ę ¹ęœ¬ę²”ęœ‰ęœåŠ”å™Øå‘čµ·ēš„čÆ·ę±‚ļ¼Œę‰€ä»„ęœåŠ”å™ØåœØå‘é€ä»»ä½•äøœč„æä¹‹å‰å°±ę‹’ē»äŗ†ć€‚å·„å…·é‡Œēš„ `ctx.elicit()` ę˜Æé‡åˆ°å®ƒēš„ē»å…øę–¹å¼ļ¼Œé€šåøøå°±åœØčæ™äøŖå·„å…·ēš„ē¬¬äø€äøŖå†…å­˜ **[测试](get-started/testing.md)** é‡Œļ¼Œå› äøŗ `Client(mcp)` äøē”Øč¦ę±‚å°±ä¼šåå•† `2026-07-28`。传兄 `elicitation_callback=` ä»€ä¹ˆä¹Ÿę”¹å˜äøäŗ†ļ¼Œå› äøŗę ¹ęœ¬ę²”ęœ‰čÆ·ę±‚åˆ°č¾¾å®¢ęˆ·ē«Æč®©å®ƒåŽ»å›žē­”ļ¼š ```python title="server.py" hl_lines="16" --8<-- "docs_src/troubleshooting/tutorial006.py" ``` ```python -async def main() -> None: +async def test_book_table() -> None: async with Client(mcp) as client: await client.call_tool("book_table", {"date": "Friday"}) ``` @@ -347,7 +359,7 @@ mcp.shared.exceptions.MCPError: Cannot send 'elicitation/create': this transport ```python async def main() -> None: - async with Client(mcp) as client: + async with Client("http://localhost:8000/mcp") as client: await client.call_tool("forecast", {"city": "London"}, request_state="round-1-from-worker-a") ``` @@ -400,7 +412,7 @@ mcp = MCPServer("Weather", request_state_security=RequestStateSecurity(keys=[key * ęœåŠ”å™Øę—„åæ—é‡Œēš„ `Tool already exists:` ę˜Æäø¤äøŖåŒåå·„å…·åˆå¹¶ęˆäø€äøŖēš„å”Æäø€čæ¹č±”ć€‚ * 一个 421ļ¼Œäø‰ē§å†™ę³•ļ¼š`Server returned an error response`(python `Client`)、`421 Misdirected Request` / `Invalid Host header`ļ¼ˆå…¶ä»–ę‰€ęœ‰åœ°ę–¹ļ¼‰ć€`Invalid Host header: `ļ¼ˆęœåŠ”å™Øę—„åæ—ļ¼‰ć€‚äæ®å¤ļ¼š`transport_security=TransportSecuritySettings(allowed_hosts=[...])`怂 * `Task group is not initialized` -> č¢«ęŒ‚č½½ēš„åŗ”ē”Øļ¼Œå…¶å®æäø»ē”Ÿå‘½å‘ØęœŸä»ŽęœŖčæ›å…„ `mcp.session_manager.run()`怂 -* `Session not found` -> ęœåŠ”å™Øé‡åÆäŗ†ļ¼›é‡čæžć€‚ +* `Session not found` -> ęœåŠ”å™Øé‡åÆäŗ†ęˆ–ä¼ščÆčæ‡ęœŸäŗ†ļ¼ˆ`session_idle_timeout`ļ¼‰ļ¼›é‡čæžć€‚ * `Cannot send 'elicitation/create': ... no back-channel ...` -> `ctx.elicit()` éœ€č¦äø€ę”ęœåŠ”å™Øåˆ°å®¢ęˆ·ē«Æēš„é€šé“ļ¼š`2026-07-28` čæžęŽ„ę°øčæœę²”ęœ‰ļ¼Œ`stateless_http=True` ę‹æčµ°äŗ†ę—§ē‰ˆēš„é‚£ę”ļ¼Œ`json_response=True` ę‹æčµ°äŗ†čÆ·ę±‚ēŗ§ēš„é‚£ę”ć€‚ē”Øč§£ęžå™Øļ¼ˆę—§ē‰ˆå®¢ęˆ·ē«Æčæ˜éœ€č¦äø€äøŖäæē•™čÆ„é€šé“ēš„ęœåŠ”å™Øļ¼‰ć€‚å®ƒēš„é‚»å±… `Method not found` ę˜ÆčÆ·ę±‚äŗ†åÆ¹ę–¹åč®®äæ®č®¢ē‰ˆé‡Œę²”ęœ‰ēš„ę–¹ę³•ć€‚ * `Client did not declare the form elicitation capability ...` 和 `Elicitation not supported` -> å®¢ęˆ·ē«Æē¼ŗå°‘ `elicitation_callback=`怂 * `Invalid or expired requestState` åœØēŗæč·ÆäøŠä»ŽäøčÆ“ę˜ŽåŽŸå› ć€‚ęœåŠ”å™Øę—„åæ—ä¼ščÆ“ļ¼›`unknown key` ę„å‘³ē€č¦åœØå„ worker 闓共享 `RequestStateSecurity(keys=[...])`怂 diff --git a/i18n/zh/pages/whats-new.md b/i18n/zh/pages/whats-new.md index fa2be14c..d3445d69 100644 --- a/i18n/zh/pages/whats-new.md +++ b/i18n/zh/pages/whats-new.md @@ -1,6 +1,6 @@ --- translation: - sections: [cfe01c0c5863dfa2, 1c58c5cfcc37d455, a7392996acf1ad8f, 875eb2889263424e] + sections: [cfe01c0c5863dfa2, 0dbb68d8b210177b, 80cf193023af3ed4, 875eb2889263424e] tool: 1 --- # v2 ēš„ę–°å˜åŒ– {#whats-new-in-v2} @@ -37,11 +37,11 @@ mcp = MCPServer("Demo") # v1: FastMCP("Demo") v1 äŗ¤ē»™ä½ ēš„ę˜Æäø‰å±‚åµŒå„—ļ¼šäø€äøŖäŗ§å‡ŗåŽŸå§‹ęµēš„ä¼ č¾“äøŠäø‹ę–‡ē®”ē†å™Øļ¼ŒåŒ…åœØå¤–é¢ēš„ `ClientSession`ļ¼Œå†åŠ äøŠę‰‹åŠØč°ƒē”Øēš„ `await session.initialize()`怂v2 åŖęœ‰äø€äøŖåÆ¹č±”ļ¼š -```python title="client.py" hl_lines="14-18" ---8<-- "docs_src/client/tutorial001.py" +```python title="client.py" hl_lines="7-11" +--8<-- "docs_src/client/tutorial001_client.py" ``` -`Client` ęŽ„å—äø€äøŖęœåŠ”å™ØåÆ¹č±”ļ¼ˆå†…å­˜ē›“čæžļ¼Œę²”ęœ‰ä¼ č¾“ļ¼ščæ™å°±ę˜Æęµ‹čÆ•ēš„åšę³•ļ¼‰ć€äø€äøŖ URL(Streamable HTTP)、一个 `StdioServerParameters`(stdio å­čæ›ēØ‹ļ¼‰ļ¼Œęˆ–č€…å…¶ä»–ä»»ę„ä¼ č¾“äøŠäø‹ę–‡ē®”ē†å™Øļ¼ŒęÆ”å¦‚ `sse_client(...)`。进兄 `async with` å°±ä¼šå»ŗē«‹čæžęŽ„å¹¶åå•†åč®®ē‰ˆęœ¬ļ¼Œäøē®”ęœåŠ”å™Øč®²ēš„ę˜Æå“Ŗäø€ä»£åč®®ļ¼›ä¹‹åŽ `client.server_capabilities` 和 `client.protocol_version` ē›“ęŽ„å°±åœØé‚£é‡Œļ¼ŒęœåŠ”å™Øč”Øę˜Žčŗ«ä»½ę—¶ `client.server_info` ä¹Ÿäø€ę ·ļ¼ˆå®ƒēŽ°åœØę˜Æ `Implementation | None`ļ¼Œå› äøŗ 2026 ē‰ˆēš„čŗ«ä»½äæ”ęÆę˜ÆåÆé€‰ēš„ļ¼‰ć€‚ä½ åœØ v1 ę³Øå†Œēš„é‡‡ę ·å’Œå¾čÆ¢å›žč°ƒä»ē„¶čƒ½ē”Øļ¼ˆå®ƒä»¬ēš„å‡½ę•°ä½“ä¼šēœ‹åˆ°å’Œęœ¬é”µå…¶ä»–åœ°ę–¹äø€ę ·ēš„ snake_case å±žę€§é‡å‘½åļ¼‰ļ¼ŒēŽ°åœØčæ˜ä¼šå›žē­” 2026 é£Žę ¼ēš„ć€åµŒåœØē»“ęžœé‡Œēš„čÆ·ę±‚ļ¼ˆč§äø‹ę–‡ļ¼‰ļ¼Œå¹¶äø”ę˜Æå¹¶å‘čæč”Œč€Œäøę˜Æäø€ę¬”äø€äøŖć€‚ęƒ³č¦åŗ•å±‚ęŽ„å£ēš„äŗŗä»ē„¶åÆä»„ē”Øåŗ•äø‹ēš„ `ClientSession`,`client.session` ä¼šęŠŠå®ƒäŗ¤ē»™ä½ ļ¼›å®ƒä¹Ÿå˜äŗ†ļ¼ˆčæč”ŒåœØę–°ēš„č°ƒåŗ¦å™Øå¼•ę“ŽäøŠļ¼Œč‡Ŗčŗ«ēš„äø€äŗ›ē­¾åä¹Ÿę”¹äŗ†ļ¼‰ļ¼Œę‰€ä»„äø‹ęŽ¢ä¹‹å‰å…ˆčÆ» **[čæē§»ęŒ‡å—](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**怂 +`Client` ęŽ„å—äø€äøŖ URL(Streamable HTTP)、一个 `StdioServerParameters`(stdio å­čæ›ēØ‹ļ¼‰ć€å…¶ä»–ä»»ę„ä¼ č¾“äøŠäø‹ę–‡ē®”ē†å™Øļ¼ˆęÆ”å¦‚ `sse_client(...)`ļ¼‰ļ¼Œęˆ–č€…åœØęµ‹čÆ•é‡Œē›“ęŽ„ęŽ„å—ęœåŠ”å™ØåÆ¹č±”ęœ¬čŗ«ļ¼ˆå†…å­˜ē›“čæžļ¼Œę²”ęœ‰ä¼ č¾“ļ¼‰ć€‚čæ›å…„ `async with` å°±ä¼šå»ŗē«‹čæžęŽ„å¹¶åå•†åč®®ē‰ˆęœ¬ļ¼Œäøē®”ęœåŠ”å™Øč®²ēš„ę˜Æå“Ŗäø€ä»£åč®®ļ¼›ä¹‹åŽ `client.server_capabilities` 和 `client.protocol_version` ē›“ęŽ„å°±åœØé‚£é‡Œļ¼ŒęœåŠ”å™Øč”Øę˜Žčŗ«ä»½ę—¶ `client.server_info` ä¹Ÿäø€ę ·ļ¼ˆå®ƒēŽ°åœØę˜Æ `Implementation | None`ļ¼Œå› äøŗ 2026 ē‰ˆēš„čŗ«ä»½äæ”ęÆę˜ÆåÆé€‰ēš„ļ¼‰ć€‚ä½ åœØ v1 ę³Øå†Œēš„é‡‡ę ·å’Œå¾čÆ¢å›žč°ƒä»ē„¶čƒ½ē”Øļ¼ˆå®ƒä»¬ēš„å‡½ę•°ä½“ä¼šēœ‹åˆ°å’Œęœ¬é”µå…¶ä»–åœ°ę–¹äø€ę ·ēš„ snake_case å±žę€§é‡å‘½åļ¼‰ļ¼ŒēŽ°åœØčæ˜ä¼šå›žē­” 2026 é£Žę ¼ēš„ć€åµŒåœØē»“ęžœé‡Œēš„čÆ·ę±‚ļ¼ˆč§äø‹ę–‡ļ¼‰ļ¼Œå¹¶äø”ę˜Æå¹¶å‘čæč”Œč€Œäøę˜Æäø€ę¬”äø€äøŖć€‚ęƒ³č¦åŗ•å±‚ęŽ„å£ēš„äŗŗä»ē„¶åÆä»„ē”Øåŗ•äø‹ēš„ `ClientSession`,`client.session` ä¼šęŠŠå®ƒäŗ¤ē»™ä½ ļ¼›å®ƒä¹Ÿå˜äŗ†ļ¼ˆčæč”ŒåœØę–°ēš„č°ƒåŗ¦å™Øå¼•ę“ŽäøŠļ¼Œč‡Ŗčŗ«ēš„äø€äŗ›ē­¾åä¹Ÿę”¹äŗ†ļ¼‰ļ¼Œę‰€ä»„äø‹ęŽ¢ä¹‹å‰å…ˆčÆ» **[čæē§»ęŒ‡å—](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)**怂 **[Client](client/index.md)** ä»‹ē»å®ƒļ¼Œ**[å®¢ęˆ·ē«Æä¼ č¾“](client/transports.md)** č®²å››ē§čæžęŽ„å½¢å¼ļ¼Œ**[å®¢ęˆ·ē«Æå›žč°ƒ](client/callbacks.md)** č®²å›žč°ƒęœ¬čŗ«ļ¼Œ**[测试](get-started/testing.md)** å±•ē¤ŗå–ä»£ v1 `create_connected_server_and_client_session()` č¾…åŠ©å‡½ę•°ēš„å†…å­˜ęØ”å¼ć€‚ @@ -166,11 +166,15 @@ v2 å®žēŽ°äŗ† 2026-07-28 äæ®č®¢ē‰ˆļ¼Œå¹¶äø”åŒę—¶ęœåŠ” **两个** äæ®č®¢ē‰ˆļ¼š ę›æä»£ę–¹ę”ˆęŠŠč°ƒē”Øåäŗ†čæ‡ę„ć€‚éœ€č¦ä»Žē”Øęˆ·é‚£é‡Œę‹æäøœč„æēš„å·„å…·ęŠŠé—®é¢˜ **čæ”å›ž** å‡ŗåŽ»ļ¼ˆ`InputRequiredResult`ļ¼‰ļ¼Œå®¢ęˆ·ē«Æē”Øå®ƒäø€ē›“éƒ½ęœ‰ēš„é‚£äŗ›å›žč°ƒę„å›žē­”ļ¼Œē„¶åŽč°ƒē”Øä¼šåø¦ē€ē­”ę”ˆé‡čÆ•ć€‚`Client` ę›æä½ é©±åŠØčæ™äøŖå¾ŖēŽÆć€‚åœØęœåŠ”å™ØäøŠä½ å¾ˆå°‘č‡Ŗå·±ęž„å»ŗčæ™äøŖē»“ęžœļ¼Œå› äøŗ **[ä¾čµ–](handlers/dependencies.md)** ä¼šåščæ™ä»¶äŗ‹ļ¼šē”Ø `Resolve(ask_quantity)` ę³Øč§£äø€äøŖå‚ę•°ļ¼Œå…¶äø­ `ask_quantity` ę˜Æä½ å†™ēš„ę™®é€šå‡½ę•°ļ¼ŒSDK å°±ä¼šé€ščæ‡čæžęŽ„ę‰€ę”ÆęŒēš„ęœŗåˆ¶åŽ»é—®ā€”ā€”åœØę—§ē‰ˆä¼ščÆäøŠę˜Æå®žę—¶ēš„å¾čÆ¢čÆ·ę±‚ļ¼ŒåœØ 2026 äøŠę˜Æå¤šč½®å¾€čæ”ć€‚äø€äøŖå·„å…·å‡½ę•°ä½“ļ¼Œäø¤ä»£åč®®ļ¼š -```python title="dual_era.py" hl_lines="24 37-38" +```python title="server.py" hl_lines="21" --8<-- "docs_src/legacy_clients/tutorial001.py" ``` -čæ™äøŖę–‡ä»¶ęŠŠå–ē‚¹é›†äø­åœØäŗ†äø€å¤„ļ¼šäø€äøŖęœåŠ”å™Øļ¼Œäø€äøŖē”± `Resolve` ę”Æę’‘ēš„å·„å…·ļ¼Œäø€äøŖę—§ē‰ˆå®¢ęˆ·ē«ÆåŠ äø€äøŖę–°ē‰ˆå®¢ęˆ·ē«Æéƒ½ę‹æåˆ°äŗ†å„č‡Ŗēš„ē­”ę”ˆļ¼Œå…ØåœØå†…å­˜é‡Œć€‚**[å¤šč½®å¾€čæ”čÆ·ę±‚](handlers/multi-round-trip.md)** č§£é‡Ščæ™äøŖęœŗåˆ¶ļ¼ˆåŒ…ę‹¬ `request_state`,SDK ä¼šę›æä½ åÆ†å°å¹¶éŖŒčÆå®ƒļ¼‰ļ¼›**[征询](handlers/elicitation.md)** č®²ęé—®ēš„éƒØåˆ†ć€‚ +```python title="client.py" hl_lines="14-15" +--8<-- "docs_src/legacy_clients/tutorial001_client.py" +``` + +čæ™äø¤äøŖę–‡ä»¶å°±ę˜Æå…ØéƒØå–ē‚¹ļ¼šäø€äøŖęœåŠ”å™Øļ¼Œäø€äøŖē”± `Resolve` ę”Æę’‘ēš„å·„å…·ļ¼Œäø€äøŖę—§ē‰ˆå®¢ęˆ·ē«ÆåŠ äø€äøŖę–°ē‰ˆå®¢ęˆ·ē«Æéƒ½ä»ŽåŒäø€äøŖę­£åœØčæč”Œēš„ęœåŠ”å™Øę‹æåˆ°äŗ†å„č‡Ŗēš„ē­”ę”ˆļ¼ˆ**[ęœåŠ”ę—§ē‰ˆå®¢ęˆ·ē«Æ](run/legacy-clients.md)** ä¼šé€ę­„č®²č§£å®ƒä»¬ļ¼‰ć€‚**[å¤šč½®å¾€čæ”čÆ·ę±‚](handlers/multi-round-trip.md)** č§£é‡Ščæ™äøŖęœŗåˆ¶ļ¼ˆåŒ…ę‹¬ `request_state`,SDK ä¼šę›æä½ åÆ†å°å¹¶éŖŒčÆå®ƒļ¼‰ļ¼›**[征询](handlers/elicitation.md)** č®²ęé—®ēš„éƒØåˆ†ć€‚ !!! warning "čæ™ę˜Æē§»ę¤čæ‡ę„ēš„ v1 ęœåŠ”å™Øå”Æäø€ä¼šę”¹å˜č”Œäøŗēš„åœ°ę–¹" ä½ č‡Ŗå·±ēš„ęµ‹čÆ•ä¼šęœ€å…ˆē¢°åˆ°å®ƒļ¼š`Client(mcp)` é»˜č®¤ä¼šå’Œä½ ēš„ v2 ęœåŠ”å™Øåå•†å‡ŗ 2026-07-28ļ¼Œę‰€ä»„č°ƒē”Ø `ctx.elicit()` ēš„å·„å…·ä¼šåœØäø€äøŖ v1 äøŠčƒ½é€ščæ‡ēš„ęµ‹čÆ•é‡Œå¤±č“„ć€‚ęŠŠé—®é¢˜ęŒŖčæ›äø€äøŖ `Resolve(...)` å‚ę•°ļ¼ˆäø¤ä»£é€šē”Øļ¼‰ļ¼Œęˆ–č€…å¦‚ęžœä½ ē”®å®žęƒ³č¦ęŽØé€č”Œäøŗļ¼Œå°±ęŠŠęµ‹čÆ•å®¢ęˆ·ē«Æå›ŗå®šäøŗ `mode="legacy"`怂 diff --git a/pyproject.toml b/pyproject.toml index d12cb748..b2f26da5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,6 +77,7 @@ dev = [ "strict-no-cover", "logfire>=3.0.0", "opentelemetry-sdk>=1.39.1", + "blockbuster>=1.5.27", ] docs = [ # Zensical is the Material team's successor to MkDocs; it natively @@ -219,6 +220,7 @@ ignore = ["PERF203"] [tool.ruff.lint.flake8-tidy-imports.banned-api] "pydantic.RootModel".msg = "Use `pydantic.TypeAdapter` instead." +"pydantic.root_model.RootModel".msg = "Use `pydantic.TypeAdapter` instead." [tool.ruff.lint.mccabe] @@ -228,8 +230,8 @@ max-complexity = 24 # Default is 10 "__init__.py" = ["F401"] # The mcp.types package is an alias that mirrors mcp_types namespaces by design. "src/mcp/types/*.py" = ["F403"] -# Generated by scripts/gen_surface_types.py: raw datamodel-codegen output (TID251 lifts the repo-wide RootModel ban for these generated validators). -"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "TID251", "UP007", "UP037"] +# Generated by scripts/gen_surface_types.py. +"src/mcp-types/mcp_types/_v*/__init__.py" = ["D212", "E501", "I001", "UP007", "UP037"] "tests/server/mcpserver/test_func_metadata.py" = ["E501"] # Inline snapshots of the translation tool's output carry long status/prompt lines verbatim. "tests/docs/test_translations.py" = ["E501"] diff --git a/scripts/docs/gen_ref_pages.py b/scripts/docs/gen_ref_pages.py index 42c75616..c368ff89 100644 --- a/scripts/docs/gen_ref_pages.py +++ b/scripts/docs/gen_ref_pages.py @@ -31,11 +31,12 @@ API_DIR = ROOT / "docs" / "api" # it from `src/` would emit the unimportable `mcp-types.mcp_types.*`. PACKAGES = (ROOT / "src" / "mcp", ROOT / "src" / "mcp-types" / "mcp_types") -# Alias packages that mirror another package's namespaces (`mcp.types` mirrors -# `mcp_types`, `mcp.types.version` mirrors `mcp_types.version`): the mirrored -# package's pages are the canonical rendering, so an alias, and every module -# under it, earns no page of its own. -EXCLUDED = frozenset({"mcp.types"}) +# Module paths that get no page, and neither does anything under them: alias +# packages that mirror another package's namespaces (`mcp.types` mirrors +# `mcp_types`), whose canonical rendering is the mirrored package's pages; and +# removed v1 import paths (`mcp.server.fastmcp`) that only raise a pointer to +# the migration guide and carry no API. +EXCLUDED = frozenset({"mcp.types", "mcp.server.fastmcp"}) _KIND_SECTIONS = { griffe.Kind.MODULE: "Modules", diff --git a/scripts/docs/translations.py b/scripts/docs/translations.py index 4dd4cfd0..59d454e1 100644 --- a/scripts/docs/translations.py +++ b/scripts/docs/translations.py @@ -13,12 +13,13 @@ English section hashes it reflects; nothing else tracks state. Usage (from the repository root): python scripts/docs/translations.py status [--lang CODE] - python scripts/docs/translations.py translate --lang CODE [--pages PATH ...] + python scripts/docs/translations.py translate [--lang CODE ...] [--pages PATH ...] [--jobs N] python scripts/docs/translations.py stage [--lang CODE] Only `translate` calls the model (credentials come from the environment, e.g. -`ANTHROPIC_API_KEY`) and needs the `translate` dependency group; -`DOCS_TRANSLATE_MODEL`, if set, replaces the registry's `model` for that run. +`ANTHROPIC_API_KEY`) and needs the `translate` dependency group; it keeps +`--jobs` pages in flight at once, and `DOCS_TRANSLATE_MODEL`, if set, replaces +the registry's `model` for that run. Exit codes: 0 done, 1 some page failed, 2 configuration or credential error. """ @@ -31,9 +32,12 @@ import posixpath import re import shutil import sys -from collections import Counter +import threading +from collections import Counter, deque from collections.abc import Callable, Iterator, Sequence +from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait from dataclasses import dataclass, field +from itertools import chain, zip_longest from pathlib import Path from typing import Any, Literal, Protocol, cast, get_args @@ -54,6 +58,12 @@ TOOL_VERSION = 1 OUTPUT_TOKEN_BUDGET = 64_000 # Repair turns fed back to the model after the first reply before a page fails. MAX_REPAIRS = 2 +# Pages in flight at once unless `--jobs` says otherwise: each page is its own +# conversation, so concurrency changes nothing the model sees, only wall-clock time. +DEFAULT_JOBS = 8 +# Retries (with backoff) the API client makes on rate limits and overloads before +# a request fails its page; generous, since many pages share one rate limit. +API_RETRIES = 6 NOTICES_PAGE = "i18n/notices.md" # The nav page the notices link to for how the translations are made. TRANSLATIONS_DOC = "translations.md" @@ -385,6 +395,8 @@ class Repo: prose_pages: list[str] translatable: list[str] renderer: markdown.Markdown + # python-markdown instances are not thread-safe; pages render one at a time. + _render_lock: threading.Lock = field(default_factory=threading.Lock, repr=False) def language(self, code: str) -> Language: for language in self.registry.languages: @@ -413,10 +425,12 @@ class Repo: Raises: PageError: The renderer sees headings the source scan does not (setext, indented, HTML). """ - self.renderer.reset() - self.renderer.convert(body) - tokens = cast("list[dict[str, Any]]", getattr(self.renderer, "toc_tokens", [])) - ids, found = [str(token["id"]) for token in _flatten(tokens)], parse_headings(body) + with self._render_lock: + self.renderer.reset() + self.renderer.convert(body) + tokens = cast("list[dict[str, Any]]", getattr(self.renderer, "toc_tokens", [])) + ids = [str(token["id"]) for token in _flatten(tokens)] + found = parse_headings(body) if len(ids) != len(found): raise PageError(f"the page renders {len(ids)} headings but {len(found)} are ATX headings at column 0") return ids @@ -579,7 +593,10 @@ class Completion: class Translator(Protocol): - """Anything that answers a conversation (`ConfigError`: credentials rejected; `PageError`: request failed).""" + """Anything that answers a conversation (`ConfigError`: credentials rejected; `PageError`: request failed). + + `complete` is called from several threads at once when pages run in parallel. + """ def complete(self, *, model: str, system: str, messages: Sequence[Message], max_tokens: int) -> Completion: ... @@ -587,14 +604,22 @@ class Translator(Protocol): def anthropic_translator() -> Translator: """The Claude Messages API client, streaming, with the system prompt as one cached block. - `anthropic` lives in the non-default `translate` dependency group, so it is - imported here, by name: offline commands and type checking never need it. + One client serves every thread. The system prompt is everything a language's + pages share (rules, instructions, glossary), so it is the cacheable prefix: + the first request of a language writes it and the rest read it. A page that + starts before that first reply has begun streaming writes it again instead; + `command_translate` orders the work so that is rare, and nothing waits on it. + + `anthropic` (and `httpx`, its transport, whose errors can escape it) lives in + the non-default `translate` dependency group, so both are imported here, by + name: offline commands and type checking never need them. Raises: ConfigError: The `translate` dependency group is not installed, or no credentials are configured. """ try: sdk = importlib.import_module("anthropic") + httpx = importlib.import_module("httpx") except ImportError as exc: raise ConfigError( "the anthropic package is not installed; run with `uv run --frozen --group translate`" @@ -602,7 +627,7 @@ def anthropic_translator() -> Translator: # The SDK resolves every credential source it knows at construction; fail # here, before any page work, rather than on the first request. try: - client = sdk.Anthropic() + client = sdk.Anthropic(max_retries=API_RETRIES) except sdk.AnthropicError as exc: # e.g. a credential profile it was pointed at is unreadable raise ConfigError(f"cannot set up the API client: {exc}") from exc if not (client.api_key or client.auth_token or client.credentials): @@ -619,6 +644,8 @@ def anthropic_translator() -> Translator: raise ConfigError(f"the API rejected the credentials: {exc.message}") from exc except sdk.APIError as exc: raise PageError(f"API request failed: {exc.message}") from exc + except httpx.HTTPError as exc: # the connection failing mid-reply is not wrapped by the SDK + raise PageError(f"API connection failed: {exc!r}") from exc usage = Usage( reply.usage.input_tokens, reply.usage.output_tokens, @@ -926,30 +953,78 @@ def translate_page(repo: Repo, inputs: Inputs, job: Job, translator: Translator, def command_translate(repo: Repo, args: argparse.Namespace, translator: Translator | None) -> int: - language = repo.language(args.lang) - inputs = repo.inputs(language) - jobs = select_jobs([classify(page) for page in repo.pages(language)], args.pages) - if not jobs: - print(f"{language.code}: nothing to translate") + codes = list(dict.fromkeys(args.lang)) # each language once, in the order given + languages = [repo.language(code) for code in codes] if codes else repo.registry.languages + per_language: list[list[tuple[Inputs, Job]]] = [] + for language in languages: + inputs = repo.inputs(language) + jobs = select_jobs([classify(page) for page in repo.pages(language)], args.pages) + if not jobs: + print(f"{language.code}: nothing to translate") + per_language.append([(inputs, job) for job in jobs]) + # Page-major across languages (every language's first page, then every second page, ...), + # so each language's first request is under way, and its cached prefix written, before + # its next page goes out, however many pages are in flight. + work = [item for item in chain.from_iterable(zip_longest(*per_language)) if item is not None] + if not work: return 0 model = os.environ.get("DOCS_TRANSLATE_MODEL") or repo.registry.model # never recorded in the generated files # Only a job with open sections calls the model; a run without one needs no client and no # credentials. Otherwise both are set up here, so bad credentials fail before any page work. - if translator is None and any(job.open for job in jobs): + if translator is None and any(job.open for _, job in work): translator = anthropic_translator() - usage, failed = Usage(), False - for job in jobs: - page = job.state.page + + def produce(inputs: Inputs, job: Job) -> tuple[str | PageError | ConfigError, Usage]: + """The page body, or why it failed or must stop the run, with the tokens it cost; runs on a pool thread.""" + spent = Usage() try: - body = translate_page(repo, inputs, job, translator, model, usage) if translator else reassemble(repo, job) - except PageError as exc: - failed = True - print(f"error: {page.key}: {exc}", file=sys.stderr) - continue - page.target.parent.mkdir(parents=True, exist_ok=True) - page.target.write_text(with_provenance(body, job.state.hashes), encoding="utf-8", newline="\n") - print(f"translated: {page.key} ({len(job.open)} of {len(job.state.hashes)} sections)", flush=True) + body = translate_page(repo, inputs, job, translator, model, spent) if translator else reassemble(repo, job) + except (PageError, ConfigError) as exc: + return exc, spent + return body, spent + + active = sorted({inputs.language.code for inputs, _ in work}, key=[lang.code for lang in languages].index) + print(f"translating {len(work)} pages ({', '.join(active)}), {args.jobs} at a time, with {model}", flush=True) + usage, failed = Usage(), False + queue = deque(work) + running: dict[Future[tuple[str | PageError | ConfigError, Usage]], tuple[Language, Job]] = {} + stopped: ConfigError | None = None # rejected credentials: start nothing more, keep what still lands + pool = ThreadPoolExecutor(max_workers=args.jobs) + try: + # The window is refilled here rather than handing the pool every page up front, so + # that once the credentials are rejected no further page starts; pages already in + # flight are still collected, and written if they made it, before the run stops. + while (queue and stopped is None) or running: + while queue and stopped is None and len(running) < args.jobs: + inputs, job = queue.popleft() + running[pool.submit(produce, inputs, job)] = (inputs.language, job) + for future in wait(running, return_when=FIRST_COMPLETED).done: + language, job = running.pop(future) + result, spent = future.result() + usage.add(spent) + if isinstance(result, ConfigError): + stopped = stopped or result + continue + page = job.state.page + if isinstance(result, PageError): + failed = True + print(f"{language.code}: error: {page.key}: {result}", file=sys.stderr) + continue + page.target.parent.mkdir(parents=True, exist_ok=True) + page.target.write_text(with_provenance(result, job.state.hashes), encoding="utf-8", newline="\n") + done = f"{len(job.open)} of {len(job.state.hashes)} sections" + print(f"{language.code}: translated {page.key} ({done})", flush=True) + except KeyboardInterrupt: + # The pool's threads are not daemons: left to them, the process would sit until every + # page in flight finished (and was thrown away). Leave now instead. + print(f"interrupted: {len(running)} pages in flight abandoned, {len(queue)} not started", file=sys.stderr) + print(f"usage: {usage}", flush=True) + os._exit(130) + finally: + pool.shutdown(wait=False) print(f"usage: {usage}") + if stopped is not None: + raise stopped return 1 if failed else 0 @@ -1072,6 +1147,12 @@ def command_status(repo: Repo, args: argparse.Namespace) -> int: # ---- Command line ---- +def _positive(text: str) -> int: + if (value := int(text)) < 1: + raise argparse.ArgumentTypeError("must be at least 1") + return value + + def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="translations.py", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter @@ -1080,9 +1161,14 @@ def _parser() -> argparse.ArgumentParser: status = commands.add_parser("status", help="what each language is missing") status.add_argument("--lang", metavar="CODE") translate = commands.add_parser("translate", help="translate missing and outdated pages (calls the model)") - translate.add_argument("--lang", metavar="CODE", required=True) translate.add_argument( - "--pages", nargs="+", metavar="PATH", default=[], help="re-translate exactly these pages from scratch" + "--lang", nargs="+", action="extend", metavar="CODE", default=[], help="only these (default: every language)" + ) + translate.add_argument( + "--pages", nargs="+", action="extend", metavar="PATH", default=[], help="re-translate exactly these, afresh" + ) + translate.add_argument( + "--jobs", type=_positive, metavar="N", default=DEFAULT_JOBS, help=f"pages in flight (default {DEFAULT_JOBS})" ) staged = commands.add_parser("stage", help="assemble .build/i18n/CODE/docs for the site build") staged.add_argument("--lang", metavar="CODE", help="stage this language only (default: every language)") diff --git a/scripts/gen_surface_types.py b/scripts/gen_surface_types.py index a9887dd4..9e0279ff 100644 --- a/scripts/gen_surface_types.py +++ b/scripts/gen_surface_types.py @@ -5,13 +5,14 @@ writes the result to `src/mcp-types/mcp_types/_v/__init__.py` (the underscore marks these as internal validators, not public API) with only the fixes the raw output needs: a small JSON pre-patch for the known `number`-as-`integer` schema.json defect, a header, full URLs for the spec's -site-absolute doc links, and per-version epilogue aliases. Run with +site-absolute doc links, plain type aliases, and per-version epilogue aliases. Run with `uv run --frozen --group codegen python scripts/gen_surface_types.py [--check]`. """ from __future__ import annotations import argparse +import ast import difflib import hashlib import json @@ -195,6 +196,7 @@ def run_codegen(schema_path: Path, output_path: Path) -> None: "--use-annotated", "--use-field-description", "--use-schema-description", "--enum-field-as-literal", "all", "--use-union-operator", "--use-double-quotes", + "--use-type-alias", "--skip-root-model", "--extra-fields", "ignore", # JSON Schema `format` is annotation-only; codegen's defaults # (Base64Str, AnyUrl) over-assert and reject valid wire data. @@ -237,6 +239,10 @@ def build(entry: dict[str, str]) -> str: schema = json.loads((SCHEMA_DIR / f"{version}.json").read_text(encoding="utf-8")) patch_schema(schema, SCHEMA_PATCHES.get(version, [])) make_server_info_opaque(schema) + if "JSONValue" in schema["$defs"]: + # A single recursive alias avoids mutually recursive alias evaluation in type checkers. + assert schema["$defs"]["JSONValue"]["anyOf"][0] == {"$ref": "#/$defs/JSONObject"} + schema["$defs"]["JSONValue"]["anyOf"][0] = schema["$defs"]["JSONObject"] with tempfile.TemporaryDirectory() as tmp: patched = Path(tmp) / "schema.json" @@ -246,7 +252,27 @@ def build(entry: dict[str, str]) -> str: source = raw.read_text(encoding="utf-8") source = re.sub(r"\A# generated by datamodel-codegen:\n#[^\n]*\n", "", source) - source = re.sub(r"^class Model\(RootModel\[Any\]\):\n {4}root: Any\n+", "", source, count=1, flags=re.MULTILINE) + # Keep named aliases only for recursive types; other aliases remain ordinary Python types and unions. + for node in reversed(ast.parse(source).body): + if not ( + isinstance(node, ast.Assign) + and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "TypeAliasType" + ): + continue + value = node.value.args[1] + if any( + isinstance(part, ast.Constant) and isinstance(part.value, str) and part.value in schema["$defs"] + for part in ast.walk(value) + ): + continue + original = ast.get_source_segment(source, node.value) + replacement = ast.get_source_segment(source, value) + assert original is not None and replacement is not None + source = source.replace(original, f"({replacement})", 1) + if "= TypeAliasType(" not in source: + source = source.replace("from typing_extensions import TypeAliasType\n", "") # Codegen appends `| None` to forward refs of nullable models, which is a # runtime TypeError on a string ref and redundant since `JSONValue` includes None. source = source.replace('"JSONValue" | None', '"JSONValue"') @@ -256,8 +282,7 @@ def build(entry: dict[str, str]) -> str: source = source.replace("](/", "](https://modelcontextprotocol.io/") source = allow_open_class_extras(source, OPEN_CLASSES[version]) if epilogue := EPILOGUES.get(version, ""): - # Insert before the trailing model_rebuild() block: pyright's evaluation - # order for the recursive RootModel block is sensitive to placement. + # Resolve aliases before rebuilding models with forward references. match = re.search(r"^\w+\.model_rebuild\(\)$", source, flags=re.MULTILINE) cut = match.start() if match else len(source) source = f"{source[:cut]}{epilogue}\n\n{source[cut:]}" diff --git a/src/mcp-types/mcp_types/_v2025_11_25/__init__.py b/src/mcp-types/mcp_types/_v2025_11_25/__init__.py index b5f5b967..5ffd15f0 100644 --- a/src/mcp-types/mcp_types/_v2025_11_25/__init__.py +++ b/src/mcp-types/mcp_types/_v2025_11_25/__init__.py @@ -9,7 +9,7 @@ from __future__ import annotations from typing import Annotated, Any, Literal from mcp_types._wire_base import WireModel -from pydantic import ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field class BaseMetadata(WireModel): @@ -285,11 +285,10 @@ class CompleteResult(WireModel): completion: Completion -class Cursor(RootModel[str]): - root: str - """ - An opaque token used to represent a cursor for pagination. - """ +Cursor = str +""" +An opaque token used to represent a cursor for pagination. +""" class RequestedSchema(WireModel): @@ -556,27 +555,13 @@ class LegacyTitledEnumSchema(WireModel): type: Literal["string"] -class LoggingLevel( - RootModel[ - Literal[ - "alert", - "critical", - "debug", - "emergency", - "error", - "info", - "notice", - "warning", - ] - ] -): - root: Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] - """ - The severity of a log message. +LoggingLevel = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] +""" +The severity of a log message. - These map to syslog message severities, as specified in RFC-5424: - https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - """ +These map to syslog message severities, as specified in RFC-5424: +https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +""" class LoggingMessageNotificationParams(WireModel): @@ -723,11 +708,10 @@ class PaginatedResult(WireModel): """ -class ProgressToken(RootModel[str | int]): - root: str | int - """ - A progress token, used to associate progress notifications with the original request. - """ +ProgressToken = str | int +""" +A progress token, used to associate progress notifications with the original request. +""" class PromptArgument(WireModel): @@ -853,11 +837,10 @@ class Request(WireModel): params: dict[str, Any] | None = None -class RequestId(RootModel[str | int]): - root: str | int - """ - A uniquely identifying ID for a request in JSON-RPC. - """ +RequestId = str | int +""" +A uniquely identifying ID for a request in JSON-RPC. +""" class RequestParams(WireModel): @@ -970,11 +953,10 @@ class Result(WireModel): """ -class Role(RootModel[Literal["assistant", "user"]]): - root: Literal["assistant", "user"] - """ - The sender or recipient of messages and data in a conversation. - """ +Role = Literal["assistant", "user"] +""" +The sender or recipient of messages and data in a conversation. +""" class Root(WireModel): @@ -1216,11 +1198,10 @@ class TaskMetadata(WireModel): """ -class TaskStatus(RootModel[Literal["cancelled", "completed", "failed", "input_required", "working"]]): - root: Literal["cancelled", "completed", "failed", "input_required", "working"] - """ - The status of a task. - """ +TaskStatus = Literal["cancelled", "completed", "failed", "input_required", "working"] +""" +The status of a task. +""" class TextResourceContents(WireModel): @@ -1867,26 +1848,16 @@ class EmbeddedResource(WireModel): type: Literal["resource"] -class EmptyResult(RootModel[Result]): - root: Result +EmptyResult = Result -class EnumSchema( - RootModel[ - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) +EnumSchema = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) class GetPromptRequestParams(WireModel): @@ -2115,8 +2086,7 @@ class LoggingMessageNotification(WireModel): params: LoggingMessageNotificationParams -class MultiSelectEnumSchema(RootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]): - root: UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema +MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema class PaginatedRequestParams(WireModel): @@ -2152,32 +2122,20 @@ class PingRequest(WireModel): params: RequestParams | None = None -class PrimitiveSchemaDefinition( - RootModel[ - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) - """ - Restricted schema definitions that only allow primitive types - without nested objects or arrays. - """ +PrimitiveSchemaDefinition = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) +""" +Restricted schema definitions that only allow primitive types +without nested objects or arrays. +""" class ProgressNotificationParams(WireModel): @@ -2499,8 +2457,7 @@ class SetLevelRequest(WireModel): params: SetLevelRequestParams -class SingleSelectEnumSchema(RootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]): - root: UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema +SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema class SubscribeRequest(WireModel): @@ -2793,8 +2750,7 @@ class CompleteRequest(WireModel): params: CompleteRequestParams -class ContentBlock(RootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]): - root: TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource +ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource class CreateTaskResult(WireModel): @@ -2812,11 +2768,10 @@ class CreateTaskResult(WireModel): task: Task -class ElicitRequestParams(RootModel[ElicitRequestURLParams | ElicitRequestFormParams]): - root: ElicitRequestURLParams | ElicitRequestFormParams - """ - The parameters for a request to elicit additional information from the user via the client. - """ +ElicitRequestParams = ElicitRequestURLParams | ElicitRequestFormParams +""" +The parameters for a request to elicit additional information from the user via the client. +""" class GetPromptRequest(WireModel): @@ -2857,18 +2812,16 @@ class InitializeRequest(WireModel): params: InitializeRequestParams -class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse - """ - Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - """ +JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse +""" +Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. +""" -class JSONRPCResponse(RootModel[JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCResultResponse | JSONRPCErrorResponse - """ - A response to a request, containing either the result or error. - """ +JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse +""" +A response to a request, containing either the result or error. +""" class ListPromptsRequest(WireModel): @@ -3173,64 +3126,34 @@ class CallToolResult(WireModel): """ -class ClientNotification( - RootModel[ - CancelledNotification - | InitializedNotification - | ProgressNotification - | TaskStatusNotification - | RootsListChangedNotification - ] -): - root: ( - CancelledNotification - | InitializedNotification - | ProgressNotification - | TaskStatusNotification - | RootsListChangedNotification - ) +ClientNotification = ( + CancelledNotification + | InitializedNotification + | ProgressNotification + | TaskStatusNotification + | RootsListChangedNotification +) -class ClientRequest( - RootModel[ - InitializeRequest - | PingRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | SetLevelRequest - | CompleteRequest - ] -): - root: ( - InitializeRequest - | PingRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscribeRequest - | UnsubscribeRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | SetLevelRequest - | CompleteRequest - ) +ClientRequest = ( + InitializeRequest + | PingRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscribeRequest + | UnsubscribeRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | SetLevelRequest + | CompleteRequest +) class ElicitRequest(WireModel): @@ -3266,72 +3189,38 @@ class GetPromptResult(WireModel): messages: list[PromptMessage] -class SamplingMessageContentBlock( - RootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent] -): - root: TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent +SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent -class ServerNotification( - RootModel[ - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | TaskStatusNotification - | LoggingMessageNotification - | ElicitationCompleteNotification - ] -): - root: ( - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | TaskStatusNotification - | LoggingMessageNotification - | ElicitationCompleteNotification - ) +ServerNotification = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | TaskStatusNotification + | LoggingMessageNotification + | ElicitationCompleteNotification +) -class ServerResult( - RootModel[ - Result - | InitializeResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CompleteResult - ] -): - root: ( - Result - | InitializeResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CompleteResult - ) +ServerResult = ( + Result + | InitializeResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CompleteResult +) class CreateMessageResult(WireModel): @@ -3398,28 +3287,16 @@ class SamplingMessage(WireModel): role: Role -class ClientResult( - RootModel[ - Result - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CreateMessageResult - | ListRootsResult - | ElicitResult - ] -): - root: ( - Result - | GetTaskResult - | GetTaskPayloadResult - | CancelTaskResult - | ListTasksResult - | CreateMessageResult - | ListRootsResult - | ElicitResult - ) +ClientResult = ( + Result + | GetTaskResult + | GetTaskPayloadResult + | CancelTaskResult + | ListTasksResult + | CreateMessageResult + | ListRootsResult + | ElicitResult +) class CreateMessageRequestParams(WireModel): @@ -3502,25 +3379,13 @@ class CreateMessageRequest(WireModel): params: CreateMessageRequestParams -class ServerRequest( - RootModel[ - PingRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | CreateMessageRequest - | ListRootsRequest - | ElicitRequest - ] -): - root: ( - PingRequest - | GetTaskRequest - | GetTaskPayloadRequest - | CancelTaskRequest - | ListTasksRequest - | CreateMessageRequest - | ListRootsRequest - | ElicitRequest - ) +ServerRequest = ( + PingRequest + | GetTaskRequest + | GetTaskPayloadRequest + | CancelTaskRequest + | ListTasksRequest + | CreateMessageRequest + | ListRootsRequest + | ElicitRequest +) diff --git a/src/mcp-types/mcp_types/_v2026_07_28/__init__.py b/src/mcp-types/mcp_types/_v2026_07_28/__init__.py index fb168b30..869700f2 100644 --- a/src/mcp-types/mcp_types/_v2026_07_28/__init__.py +++ b/src/mcp-types/mcp_types/_v2026_07_28/__init__.py @@ -9,7 +9,8 @@ from __future__ import annotations from typing import Annotated, Any, Literal, Union from mcp_types._wire_base import WireModel -from pydantic import ConfigDict, Field, RootModel +from pydantic import ConfigDict, Field +from typing_extensions import TypeAliasType class BaseMetadata(WireModel): @@ -95,11 +96,10 @@ class Completion(WireModel): """ -class Cursor(RootModel[str]): - root: str - """ - An opaque token used to represent a cursor for pagination. - """ +Cursor = str +""" +An opaque token used to represent a cursor for pagination. +""" class RequestedSchema(WireModel): @@ -415,6 +415,12 @@ class JSONRPCNotification(WireModel): params: dict[str, Any] | None = None +JSONValue = TypeAliasType( + "JSONValue", + Union[dict[str, "JSONValue"], list["JSONValue"], str | int | float | bool | None], +) + + class LegacyTitledEnumSchema(WireModel): """ Use {@link TitledSingleSelectEnumSchema} instead. @@ -436,27 +442,13 @@ class LegacyTitledEnumSchema(WireModel): type: Literal["string"] -class LoggingLevel( - RootModel[ - Literal[ - "alert", - "critical", - "debug", - "emergency", - "error", - "info", - "notice", - "warning", - ] - ] -): - root: Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] - """ - The severity of a log message. +LoggingLevel = Literal["alert", "critical", "debug", "emergency", "error", "info", "notice", "warning"] +""" +The severity of a log message. - These map to syslog message severities, as specified in RFC-5424: - https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 - """ +These map to syslog message severities, as specified in RFC-5424: +https://datatracker.ietf.org/doc/html/rfc5424#section-6.2.1 +""" class MetaObject(WireModel): @@ -624,11 +616,10 @@ class ParseError(WireModel): """ -class ProgressToken(RootModel[str | int]): - root: str | int - """ - A progress token, used to associate progress notifications with the original request. - """ +ProgressToken = str | int +""" +A progress token, used to associate progress notifications with the original request. +""" class PromptArgument(WireModel): @@ -694,11 +685,10 @@ class Request(WireModel): params: dict[str, Any] | None = None -class RequestId(RootModel[str | int]): - root: str | int - """ - A uniquely identifying ID for a request in JSON-RPC. - """ +RequestId = str | int +""" +A uniquely identifying ID for a request in JSON-RPC. +""" class ResourceContents(WireModel): @@ -759,22 +749,20 @@ class ResultMetaObject(WireModel): """ -class ResultType(RootModel[str]): - root: str - """ - Indicates the type of a {@link Result} object, allowing the client to - determine how to parse the response. +ResultType = str +""" +Indicates the type of a {@link Result} object, allowing the client to +determine how to parse the response. - complete - the request completed successfully and the result contains the final content. - input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. - """ +complete - the request completed successfully and the result contains the final content. +input_required - the request requires additional input and the result contains an {@link InputRequiredResult} object with instructions for the client to provide additional input before retrying the original request. +""" -class Role(RootModel[Literal["assistant", "user"]]): - root: Literal["assistant", "user"] - """ - The sender or recipient of messages and data in a conversation. - """ +Role = Literal["assistant", "user"] +""" +The sender or recipient of messages and data in a conversation. +""" class Root(WireModel): @@ -1468,11 +1456,10 @@ class CompleteResultResponse(WireModel): result: CompleteResult -class ElicitRequestParams(RootModel[ElicitRequestFormParams | ElicitRequestURLParams]): - root: ElicitRequestFormParams | ElicitRequestURLParams - """ - The parameters for a request to elicit additional information from the user via the client. - """ +ElicitRequestParams = ElicitRequestFormParams | ElicitRequestURLParams +""" +The parameters for a request to elicit additional information from the user via the client. +""" class EmbeddedResource(WireModel): @@ -1495,22 +1482,13 @@ class EmbeddedResource(WireModel): type: Literal["resource"] -class EnumSchema( - RootModel[ - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) +EnumSchema = ( + UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) class HeaderMismatchError(WireModel): @@ -1553,6 +1531,12 @@ class ImageContent(WireModel): type: Literal["image"] +JSONArray = list[JSONValue] + + +JSONObject = dict[str, JSONValue] + + class JSONRPCErrorResponse(WireModel): """ A response to a request that indicates an error occurred. @@ -1618,8 +1602,7 @@ class ListRootsResult(WireModel): roots: list[Root] -class MultiSelectEnumSchema(RootModel[UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema]): - root: UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema +MultiSelectEnumSchema = UntitledMultiSelectEnumSchema | TitledMultiSelectEnumSchema class NotificationMetaObject(WireModel): @@ -1680,32 +1663,20 @@ class PaginatedResult(WireModel): """ -class PrimitiveSchemaDefinition( - RootModel[ - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ] -): - root: ( - StringSchema - | NumberSchema - | BooleanSchema - | UntitledSingleSelectEnumSchema - | TitledSingleSelectEnumSchema - | UntitledMultiSelectEnumSchema - | TitledMultiSelectEnumSchema - | LegacyTitledEnumSchema - ) - """ - Restricted schema definitions that only allow primitive types - without nested objects or arrays. - """ +PrimitiveSchemaDefinition = ( + StringSchema + | NumberSchema + | BooleanSchema + | UntitledSingleSelectEnumSchema + | TitledSingleSelectEnumSchema + | UntitledMultiSelectEnumSchema + | TitledMultiSelectEnumSchema + | LegacyTitledEnumSchema +) +""" +Restricted schema definitions that only allow primitive types +without nested objects or arrays. +""" class ProgressNotificationParams(WireModel): @@ -2064,8 +2035,50 @@ class Result(WireModel): """ -class SingleSelectEnumSchema(RootModel[UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema]): - root: UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema +class ServerCapabilities(WireModel): + """ + Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. + """ + + model_config = ConfigDict( + extra="ignore", + ) + completions: JSONObject | None = None + """ + Present if the server supports argument autocompletion suggestions. + """ + experimental: dict[str, JSONObject] | None = None + """ + Experimental, non-standard capabilities that the server supports. + """ + extensions: dict[str, JSONObject] | None = None + """ + Optional MCP extensions that the server supports. Keys are extension identifiers + (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings + objects. An empty object indicates support with no settings. + + Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a + mandatory prefix. + """ + logging: JSONObject | None = None + """ + Present if the server supports sending log messages to the client. + """ + prompts: Prompts | None = None + """ + Present if the server offers any prompt templates. + """ + resources: Resources | None = None + """ + Present if the server offers any resources to read. + """ + tools: Tools | None = None + """ + Present if the server offers any tools to call. + """ + + +SingleSelectEnumSchema = UntitledSingleSelectEnumSchema | TitledSingleSelectEnumSchema class SubscriptionsAcknowledgedNotificationParams(WireModel): @@ -2237,6 +2250,72 @@ class CancelledNotificationParams(WireModel): """ +class Elicitation(WireModel): + """ + Present if the client supports elicitation from the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + form: JSONObject | None = None + url: JSONObject | None = None + + +class Sampling(WireModel): + """ + Present if the client supports sampling from an LLM. + """ + + model_config = ConfigDict( + extra="ignore", + ) + context: JSONObject | None = None + """ + Whether the client supports context inclusion via `includeContext` parameter. + If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). + """ + tools: JSONObject | None = None + """ + Whether the client supports tool use via `tools` and `toolChoice` parameters. + """ + + +class ClientCapabilities(WireModel): + """ + Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. + """ + + model_config = ConfigDict( + extra="ignore", + ) + elicitation: Elicitation | None = None + """ + Present if the client supports elicitation from the server. + """ + experimental: dict[str, JSONObject] | None = None + """ + Experimental, non-standard capabilities that the client supports. + """ + extensions: dict[str, JSONObject] | None = None + """ + Optional MCP extensions that the client supports. Keys are extension identifiers + (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are + per-extension settings objects. An empty object indicates support with no settings. + + Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a + mandatory prefix. + """ + roots: dict[str, Any] | None = None + """ + Present if the client supports listing roots. + """ + sampling: Sampling | None = None + """ + Present if the client supports sampling from an LLM. + """ + + class ClientNotification(WireModel): """ This notification is sent by the client to indicate that it is cancelling a request it previously issued. @@ -2256,15 +2335,89 @@ class ClientNotification(WireModel): params: CancelledNotificationParams -class ClientResult(RootModel[Result]): - root: Result +ClientResult = Result +""" +Common result fields. +""" + + +ContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource + + +class DiscoverResult(WireModel): """ - Common result fields. + The result returned by the server for a {@link DiscoverRequestserver/discover} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None + cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] + """ + Indicates the intended scope of the cached response, analogous to HTTP + `Cache-Control: public` vs `Cache-Control: private`. + + - `"public"`: The response does not contain user-specific data. Any + client or intermediary (e.g., shared gateway, caching proxy) MAY cache + the response and serve it across authorization contexts. + - `"private"`: The response MAY be cached and reused only within the + same authorization context. Caches MUST NOT be shared across + authorization contexts (e.g., a different access token requires a + different cache). + """ + capabilities: ServerCapabilities + """ + The capabilities of the server. + """ + instructions: str | None = None + """ + Natural-language guidance describing the server and its features. + + This can be used by clients to improve an LLM's understanding of + available tools (e.g., by including it in a system prompt). It should + focus on information that helps the model use the server effectively + and should not duplicate information already in tool descriptions. + """ + result_type: Annotated[str, Field(alias="resultType")] + """ + Indicates the type of the result, which allows the client to determine + how to parse the result object. + + Servers implementing this protocol version MUST include this field. + For backward compatibility, when a client receives a result from a + server implementing an earlier protocol version (which does not include + `resultType`), the client MUST treat the absent field as `"complete"`. + """ + supported_versions: Annotated[list[str], Field(alias="supportedVersions")] + """ + MCP Protocol Versions this server supports. The client should choose a + version from this list for use in subsequent requests. + """ + ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] + """ + A hint from the server indicating how long (in milliseconds) the + client MAY cache this response before re-fetching. Semantics are + analogous to HTTP Cache-Control max-age. + + - If 0, The response SHOULD be considered immediately stale, + The client MAY re-fetch every time the result is needed. + - If positive, the client SHOULD consider the result fresh for this many + milliseconds after receiving the response. """ -class ContentBlock(RootModel[TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource]): - root: TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource +class DiscoverResultResponse(WireModel): + """ + A successful response from the server for a {@link DiscoverRequestserver/discover} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + result: DiscoverResult class ElicitRequest(WireModel): @@ -2279,11 +2432,10 @@ class ElicitRequest(WireModel): params: ElicitRequestParams -class EmptyResult(RootModel[Result]): - root: Result - """ - Common result fields. - """ +EmptyResult = Result +""" +Common result fields. +""" class JSONRPCResultResponse(WireModel): @@ -2578,6 +2730,49 @@ class LoggingMessageNotificationParams(WireModel): """ +class Data(WireModel): + """ + Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + """ + + model_config = ConfigDict( + extra="ignore", + ) + required_capabilities: Annotated[ClientCapabilities, Field(alias="requiredCapabilities")] + """ + The capabilities the server requires from the client to process this request. + """ + + +class Error2(Error): + model_config = ConfigDict( + extra="ignore", + ) + code: Literal[-32021] + """ + The error type that occurred. + """ + data: Data + """ + Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). + """ + + +class MissingRequiredClientCapabilityError(WireModel): + """ + Returned when processing a request requires a capability the client did not + declare in `clientCapabilities`. For HTTP, the response status code MUST be + `400 Bad Request`. + """ + + model_config = ConfigDict( + extra="ignore", + ) + error: Error2 + id: RequestId | None = None + jsonrpc: Literal["2.0"] + + class ProgressNotification(WireModel): """ An out-of-band notification used to inform the receiver of a progress update for a long-running request. @@ -2606,6 +2801,91 @@ class PromptMessage(WireModel): role: Role +class RequestMetaObject(WireModel): + """ + Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. + """ + + model_config = ConfigDict( + extra="allow", + ) + io_modelcontextprotocol_client_capabilities: Annotated[ + ClientCapabilities, Field(alias="io.modelcontextprotocol/clientCapabilities") + ] + """ + The client's capabilities for this specific request. Required. + + Capabilities are declared per-request rather than once at initialization; + an empty object means the client supports no optional capabilities. + Servers MUST NOT infer capabilities from prior requests. + """ + io_modelcontextprotocol_client_info: Annotated[ + Implementation | None, Field(alias="io.modelcontextprotocol/clientInfo") + ] = None + """ + Identifies the client software making the request. Clients SHOULD + include this field on every request unless specifically configured not + to do so. + + The {@link Implementation} schema requires `name` and `version`; other + fields are optional. + + The value is self-reported by the client and is not verified by the + protocol. It is intended for display, logging, and debugging. Servers + SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for + security decisions. + """ + io_modelcontextprotocol_log_level: Annotated[ + LoggingLevel | None, Field(alias="io.modelcontextprotocol/logLevel") + ] = None + """ + The desired log level for this request. Optional. + + If absent, the server MUST NOT send any {@link LoggingMessageNotificationnotifications/message} + notifications for this request. The client opts in to log messages by + explicitly setting a level. Replaces the former `logging/setLevel` RPC. + """ + io_modelcontextprotocol_protocol_version: Annotated[str, Field(alias="io.modelcontextprotocol/protocolVersion")] + """ + The MCP Protocol Version being used for this request. Required. + + For the HTTP transport, this value MUST match the `MCP-Protocol-Version` + header; otherwise the server MUST return a `400 Bad Request`. If the + server does not support the requested version, it MUST return an + {@link UnsupportedProtocolVersionError}. + """ + progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None + """ + If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotificationnotifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. + """ + + +class RequestParams(WireModel): + """ + Common params for any request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + + +class ResourceRequestParams(WireModel): + """ + Common params for resource-related requests. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + uri: str + """ + The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. + """ + + class ResourceUpdatedNotification(WireModel): """ A notification from the server to the client, informing it that a resource has changed and may need to be read again. This is only sent for resources the client opted in to via the `resourceSubscriptions` field of a {@link SubscriptionsListenRequestsubscriptions/listen} request. @@ -2641,6 +2921,23 @@ class SubscriptionsAcknowledgedNotification(WireModel): params: SubscriptionsAcknowledgedNotificationParams +class SubscriptionsListenRequestParams(WireModel): + """ + Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + notifications: SubscriptionFilter + """ + The notifications the client opts in to on this stream. The server + **MUST NOT** send notification types the client has not explicitly + requested. + """ + + class ToolResultContent(WireModel): """ The result of a tool use, provided by the user back to the assistant. @@ -2750,6 +3047,43 @@ class CancelledNotification(WireModel): params: CancelledNotificationParams +class CompleteRequestParams(WireModel): + """ + Parameters for a `completion/complete` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + argument: Argument + """ + The argument's information + """ + context: Context | None = None + """ + Additional, optional context for completions + """ + ref: PromptReference | ResourceTemplateReference + + +class DiscoverRequest(WireModel): + """ + A request from the client asking the server to advertise its supported + protocol versions, capabilities, and other metadata. Servers **MUST** + implement `server/discover`. Clients **MAY** call it but are not required + to — version negotiation can also happen inline via per-request `_meta`. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["server/discover"] + params: RequestParams + + class GetPromptResult(WireModel): """ The result returned by the server for a {@link GetPromptRequestprompts/get} request. @@ -2776,18 +3110,16 @@ class GetPromptResult(WireModel): """ -class JSONRPCMessage(RootModel[JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse - """ - Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. - """ +JSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResultResponse | JSONRPCErrorResponse +""" +Refers to any valid JSON-RPC object that can be decoded off the wire, or encoded to be sent. +""" -class JSONRPCResponse(RootModel[JSONRPCResultResponse | JSONRPCErrorResponse]): - root: JSONRPCResultResponse | JSONRPCErrorResponse - """ - A response to a request, containing either the result or error. - """ +JSONRPCResponse = JSONRPCResultResponse | JSONRPCErrorResponse +""" +A response to a request, containing either the result or error. +""" class LoggingMessageNotification(WireModel): @@ -2803,34 +3135,65 @@ class LoggingMessageNotification(WireModel): params: LoggingMessageNotificationParams -class SamplingMessageContentBlock( - RootModel[TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent] -): - root: TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent +class PaginatedRequestParams(WireModel): + """ + Common params for paginated requests. + """ - -class ServerNotification( - RootModel[ - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification - ] -): - root: ( - CancelledNotification - | ProgressNotification - | ResourceListChangedNotification - | SubscriptionsAcknowledgedNotification - | ResourceUpdatedNotification - | PromptListChangedNotification - | ToolListChangedNotification - | LoggingMessageNotification + model_config = ConfigDict( + extra="ignore", ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + cursor: str | None = None + """ + An opaque token representing the current pagination position. + If provided, the server should return results starting after this cursor. + """ + + +SamplingMessageContentBlock = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent + + +ServerNotification = ( + CancelledNotification + | ProgressNotification + | ResourceListChangedNotification + | SubscriptionsAcknowledgedNotification + | ResourceUpdatedNotification + | PromptListChangedNotification + | ToolListChangedNotification + | LoggingMessageNotification +) + + +class SubscriptionsListenRequest(WireModel): + """ + Sent from the client to open a long-lived channel for receiving notifications + outside the context of a specific request. Replaces the previous HTTP GET + endpoint and ensures consistent behavior between HTTP and STDIO. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["subscriptions/listen"] + params: SubscriptionsListenRequestParams + + +class CompleteRequest(WireModel): + """ + A request from the client to the server, to ask for completion options. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["completion/complete"] + params: CompleteRequestParams class CreateMessageResult(WireModel): @@ -2871,428 +3234,15 @@ class CreateMessageResult(WireModel): """ -class InputResponse(RootModel[CreateMessageResult | ListRootsResult | ElicitResult]): - root: CreateMessageResult | ListRootsResult | ElicitResult +InputResponse = CreateMessageResult | ListRootsResult | ElicitResult -class InputResponses(RootModel[dict[str, InputResponse]]): - """ - A map of client responses to server-initiated requests. - Keys correspond to the keys in the {@link InputRequests} map; - values are the client's result for each request. - """ - - root: dict[str, InputResponse] - - -class SamplingMessage(WireModel): - """ - Describes a message issued to or received from an LLM API. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[MetaObject | None, Field(alias="_meta")] = None - content: ( - TextContent - | ImageContent - | AudioContent - | ToolUseContent - | ToolResultContent - | list[SamplingMessageContentBlock] - ) - role: Role - - -class CallToolRequest(WireModel): - """ - Used by the client to invoke a tool provided by the server. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["tools/call"] - params: CallToolRequestParams - - -class CallToolRequestParams(WireModel): - """ - Parameters for a `tools/call` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - arguments: dict[str, Any] | None = None - """ - Arguments to use for the tool call. - """ - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - name: str - """ - The name of the tool. - """ - request_state: Annotated[str | None, Field(alias="requestState")] = None - - -class CallToolResultResponse(WireModel): - """ - A successful response from the server for a {@link CallToolRequesttools/call} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - result: InputRequiredResult | CallToolResult - - -class Elicitation(WireModel): - """ - Present if the client supports elicitation from the server. - """ - - model_config = ConfigDict( - extra="ignore", - ) - form: JSONObject | None = None - url: JSONObject | None = None - - -class Sampling(WireModel): - """ - Present if the client supports sampling from an LLM. - """ - - model_config = ConfigDict( - extra="ignore", - ) - context: JSONObject | None = None - """ - Whether the client supports context inclusion via `includeContext` parameter. - If not declared, servers SHOULD only use `includeContext: "none"` (or omit it). - """ - tools: JSONObject | None = None - """ - Whether the client supports tool use via `tools` and `toolChoice` parameters. - """ - - -class ClientCapabilities(WireModel): - """ - Capabilities a client may support. Known capabilities are defined here, in this schema, but this is not a closed set: any client can define its own, additional capabilities. - """ - - model_config = ConfigDict( - extra="ignore", - ) - elicitation: Elicitation | None = None - """ - Present if the client supports elicitation from the server. - """ - experimental: dict[str, JSONObject] | None = None - """ - Experimental, non-standard capabilities that the client supports. - """ - extensions: dict[str, JSONObject] | None = None - """ - Optional MCP extensions that the client supports. Keys are extension identifiers - (e.g., "io.modelcontextprotocol/oauth-client-credentials"), and values are - per-extension settings objects. An empty object indicates support with no settings. - - Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a - mandatory prefix. - """ - roots: dict[str, Any] | None = None - """ - Present if the client supports listing roots. - """ - sampling: Sampling | None = None - """ - Present if the client supports sampling from an LLM. - """ - - -class CompleteRequest(WireModel): - """ - A request from the client to the server, to ask for completion options. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["completion/complete"] - params: CompleteRequestParams - - -class CompleteRequestParams(WireModel): - """ - Parameters for a `completion/complete` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - argument: Argument - """ - The argument's information - """ - context: Context | None = None - """ - Additional, optional context for completions - """ - ref: PromptReference | ResourceTemplateReference - - -class CreateMessageRequest(WireModel): - """ - A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. - """ - - model_config = ConfigDict( - extra="ignore", - ) - method: Literal["sampling/createMessage"] - params: CreateMessageRequestParams - - -class CreateMessageRequestParams(WireModel): - """ - Parameters for a `sampling/createMessage` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - include_context: Annotated[ - Literal["allServers", "none", "thisServer"] | None, - Field(alias="includeContext"), - ] = None - """ - A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. - The client MAY ignore this request. - - Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD - omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares - {@link ClientCapabilities.sampling.context}. - """ - max_tokens: Annotated[int, Field(alias="maxTokens")] - """ - The requested maximum number of tokens to sample (to prevent runaway completions). - - The client MAY choose to sample fewer tokens than the requested maximum. - """ - messages: list[SamplingMessage] - metadata: JSONObject | None = None - """ - Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. - """ - model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None - """ - The server's preferences for which model to select. The client MAY ignore these preferences. - """ - stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None - system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None - """ - An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. - """ - temperature: float | None = None - tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None - """ - Controls how the model uses tools. - The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - Default is `{ mode: "auto" }`. - """ - tools: list[Tool] | None = None - """ - Tools that the model may use during generation. - The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. - """ - - -class DiscoverRequest(WireModel): - """ - A request from the client asking the server to advertise its supported - protocol versions, capabilities, and other metadata. Servers **MUST** - implement `server/discover`. Clients **MAY** call it but are not required - to — version negotiation can also happen inline via per-request `_meta`. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["server/discover"] - params: RequestParams - - -class DiscoverResult(WireModel): - """ - The result returned by the server for a {@link DiscoverRequestserver/discover} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None - cache_scope: Annotated[Literal["private", "public"], Field(alias="cacheScope")] - """ - Indicates the intended scope of the cached response, analogous to HTTP - `Cache-Control: public` vs `Cache-Control: private`. - - - `"public"`: The response does not contain user-specific data. Any - client or intermediary (e.g., shared gateway, caching proxy) MAY cache - the response and serve it across authorization contexts. - - `"private"`: The response MAY be cached and reused only within the - same authorization context. Caches MUST NOT be shared across - authorization contexts (e.g., a different access token requires a - different cache). - """ - capabilities: ServerCapabilities - """ - The capabilities of the server. - """ - instructions: str | None = None - """ - Natural-language guidance describing the server and its features. - - This can be used by clients to improve an LLM's understanding of - available tools (e.g., by including it in a system prompt). It should - focus on information that helps the model use the server effectively - and should not duplicate information already in tool descriptions. - """ - result_type: Annotated[str, Field(alias="resultType")] - """ - Indicates the type of the result, which allows the client to determine - how to parse the result object. - - Servers implementing this protocol version MUST include this field. - For backward compatibility, when a client receives a result from a - server implementing an earlier protocol version (which does not include - `resultType`), the client MUST treat the absent field as `"complete"`. - """ - supported_versions: Annotated[list[str], Field(alias="supportedVersions")] - """ - MCP Protocol Versions this server supports. The client should choose a - version from this list for use in subsequent requests. - """ - ttl_ms: Annotated[int, Field(alias="ttlMs", ge=0)] - """ - A hint from the server indicating how long (in milliseconds) the - client MAY cache this response before re-fetching. Semantics are - analogous to HTTP Cache-Control max-age. - - - If 0, The response SHOULD be considered immediately stale, - The client MAY re-fetch every time the result is needed. - - If positive, the client SHOULD consider the result fresh for this many - milliseconds after receiving the response. - """ - - -class DiscoverResultResponse(WireModel): - """ - A successful response from the server for a {@link DiscoverRequestserver/discover} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - result: DiscoverResult - - -class GetPromptRequest(WireModel): - """ - Used by the client to get a prompt provided by the server. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["prompts/get"] - params: GetPromptRequestParams - - -class GetPromptRequestParams(WireModel): - """ - Parameters for a `prompts/get` request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - arguments: dict[str, str] | None = None - """ - Arguments to use for templating the prompt. - """ - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - name: str - """ - The name of the prompt or prompt template. - """ - request_state: Annotated[str | None, Field(alias="requestState")] = None - - -class GetPromptResultResponse(WireModel): - """ - A successful response from the server for a {@link GetPromptRequestprompts/get} request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - result: InputRequiredResult | GetPromptResult - - -class InputRequiredResult(WireModel): - """ - An InputRequiredResult sent by the server to indicate that additional input is needed - before the request can be completed. - - At least one of `inputRequests` or `requestState` MUST be present. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None - input_requests: Annotated[InputRequests | None, Field(alias="inputRequests")] = None - request_state: Annotated[str | None, Field(alias="requestState")] = None - result_type: Annotated[str, Field(alias="resultType")] - """ - Indicates the type of the result, which allows the client to determine - how to parse the result object. - - Servers implementing this protocol version MUST include this field. - For backward compatibility, when a client receives a result from a - server implementing an earlier protocol version (which does not include - `resultType`), the client MUST treat the absent field as `"complete"`. - """ - - -class InputResponseRequestParams(WireModel): - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None - request_state: Annotated[str | None, Field(alias="requestState")] = None +InputResponses = dict[str, InputResponse] +""" +A map of client responses to server-initiated requests. +Keys correspond to the keys in the {@link InputRequests} map; +values are the client's result for each request. +""" class ListPromptsRequest(WireModel): @@ -3351,49 +3301,6 @@ class ListToolsRequest(WireModel): params: PaginatedRequestParams -class Data(WireModel): - """ - Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - """ - - model_config = ConfigDict( - extra="ignore", - ) - required_capabilities: Annotated[ClientCapabilities, Field(alias="requiredCapabilities")] - """ - The capabilities the server requires from the client to process this request. - """ - - -class Error2(Error): - model_config = ConfigDict( - extra="ignore", - ) - code: Literal[-32021] - """ - The error type that occurred. - """ - data: Data - """ - Additional information about the error. The value of this member is defined by the sender (e.g. detailed error information, nested errors etc.). - """ - - -class MissingRequiredClientCapabilityError(WireModel): - """ - Returned when processing a request requires a capability the client did not - declare in `clientCapabilities`. For HTTP, the response status code MUST be - `400 Bad Request`. - """ - - model_config = ConfigDict( - extra="ignore", - ) - error: Error2 - id: RequestId | None = None - jsonrpc: Literal["2.0"] - - class PaginatedRequest(WireModel): model_config = ConfigDict( extra="ignore", @@ -3404,36 +3311,6 @@ class PaginatedRequest(WireModel): params: PaginatedRequestParams -class PaginatedRequestParams(WireModel): - """ - Common params for paginated requests. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - cursor: str | None = None - """ - An opaque token representing the current pagination position. - If provided, the server should return results starting after this cursor. - """ - - -class ReadResourceRequest(WireModel): - """ - Sent from the client to the server, to read a specific resource URI. - """ - - model_config = ConfigDict( - extra="ignore", - ) - id: RequestId - jsonrpc: Literal["2.0"] - method: Literal["resources/read"] - params: ReadResourceRequestParams - - class ReadResourceRequestParams(WireModel): """ Parameters for a `resources/read` request. @@ -3451,6 +3328,221 @@ class ReadResourceRequestParams(WireModel): """ +class SamplingMessage(WireModel): + """ + Describes a message issued to or received from an LLM API. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[MetaObject | None, Field(alias="_meta")] = None + content: ( + TextContent + | ImageContent + | AudioContent + | ToolUseContent + | ToolResultContent + | list[SamplingMessageContentBlock] + ) + role: Role + + +class CallToolRequestParams(WireModel): + """ + Parameters for a `tools/call` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + arguments: dict[str, Any] | None = None + """ + Arguments to use for the tool call. + """ + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str + """ + The name of the tool. + """ + request_state: Annotated[str | None, Field(alias="requestState")] = None + + +class CreateMessageRequestParams(WireModel): + """ + Parameters for a `sampling/createMessage` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + include_context: Annotated[ + Literal["allServers", "none", "thisServer"] | None, + Field(alias="includeContext"), + ] = None + """ + A request to include context from one or more MCP servers (including the caller), to be attached to the prompt. + The client MAY ignore this request. + + Default is `"none"`. The values `"thisServer"` and `"allServers"` are deprecated (SEP-2596): servers SHOULD + omit this field or use `"none"`, and SHOULD only use the deprecated values if the client declares + {@link ClientCapabilities.sampling.context}. + """ + max_tokens: Annotated[int, Field(alias="maxTokens")] + """ + The requested maximum number of tokens to sample (to prevent runaway completions). + + The client MAY choose to sample fewer tokens than the requested maximum. + """ + messages: list[SamplingMessage] + metadata: JSONObject | None = None + """ + Optional metadata to pass through to the LLM provider. The format of this metadata is provider-specific. + """ + model_preferences: Annotated[ModelPreferences | None, Field(alias="modelPreferences")] = None + """ + The server's preferences for which model to select. The client MAY ignore these preferences. + """ + stop_sequences: Annotated[list[str] | None, Field(alias="stopSequences")] = None + system_prompt: Annotated[str | None, Field(alias="systemPrompt")] = None + """ + An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt. + """ + temperature: float | None = None + tool_choice: Annotated[ToolChoice | None, Field(alias="toolChoice")] = None + """ + Controls how the model uses tools. + The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + Default is `{ mode: "auto" }`. + """ + tools: list[Tool] | None = None + """ + Tools that the model may use during generation. + The client MUST return an error if this field is provided but {@link ClientCapabilities.sampling.tools} is not declared. + """ + + +class GetPromptRequestParams(WireModel): + """ + Parameters for a `prompts/get` request. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + arguments: dict[str, str] | None = None + """ + Arguments to use for templating the prompt. + """ + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + name: str + """ + The name of the prompt or prompt template. + """ + request_state: Annotated[str | None, Field(alias="requestState")] = None + + +class InputResponseRequestParams(WireModel): + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[RequestMetaObject, Field(alias="_meta")] + input_responses: Annotated[InputResponses | None, Field(alias="inputResponses")] = None + request_state: Annotated[str | None, Field(alias="requestState")] = None + + +class ReadResourceRequest(WireModel): + """ + Sent from the client to the server, to read a specific resource URI. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["resources/read"] + params: ReadResourceRequestParams + + +class CallToolRequest(WireModel): + """ + Used by the client to invoke a tool provided by the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["tools/call"] + params: CallToolRequestParams + + +class CreateMessageRequest(WireModel): + """ + A request from the server to sample an LLM via the client. The client has full discretion over which model to select. The client should also inform the user before beginning sampling, to allow them to inspect the request (human in the loop) and decide whether to approve it. + """ + + model_config = ConfigDict( + extra="ignore", + ) + method: Literal["sampling/createMessage"] + params: CreateMessageRequestParams + + +class GetPromptRequest(WireModel): + """ + Used by the client to get a prompt provided by the server. + """ + + model_config = ConfigDict( + extra="ignore", + ) + id: RequestId + jsonrpc: Literal["2.0"] + method: Literal["prompts/get"] + params: GetPromptRequestParams + + +InputRequest = CreateMessageRequest | ListRootsRequest | ElicitRequest + + +InputRequests = dict[str, InputRequest] +""" +A map of server-initiated requests that the client must fulfill. +Keys are server-assigned identifiers; values are the request objects. +""" + + +class InputRequiredResult(WireModel): + """ + An InputRequiredResult sent by the server to indicate that additional input is needed + before the request can be completed. + + At least one of `inputRequests` or `requestState` MUST be present. + """ + + model_config = ConfigDict( + extra="ignore", + ) + meta: Annotated[ResultMetaObject | None, Field(alias="_meta")] = None + input_requests: Annotated[InputRequests | None, Field(alias="inputRequests")] = None + request_state: Annotated[str | None, Field(alias="requestState")] = None + result_type: Annotated[str, Field(alias="resultType")] + """ + Indicates the type of the result, which allows the client to determine + how to parse the result object. + + Servers implementing this protocol version MUST include this field. + For backward compatibility, when a client receives a result from a + server implementing an earlier protocol version (which does not include + `resultType`), the client MUST treat the absent field as `"complete"`. + """ + + class ReadResourceResultResponse(WireModel): """ A successful response from the server for a {@link ReadResourceRequestresources/read} request. @@ -3464,139 +3556,25 @@ class ReadResourceResultResponse(WireModel): result: InputRequiredResult | ReadResourceResult -class RequestMetaObject(WireModel): - """ - Extends {@link MetaObject} with additional request-specific fields. All key naming rules from `MetaObject` apply. - """ - - model_config = ConfigDict( - extra="allow", - ) - io_modelcontextprotocol_client_capabilities: Annotated[ - ClientCapabilities, Field(alias="io.modelcontextprotocol/clientCapabilities") - ] - """ - The client's capabilities for this specific request. Required. - - Capabilities are declared per-request rather than once at initialization; - an empty object means the client supports no optional capabilities. - Servers MUST NOT infer capabilities from prior requests. - """ - io_modelcontextprotocol_client_info: Annotated[ - Implementation | None, Field(alias="io.modelcontextprotocol/clientInfo") - ] = None - """ - Identifies the client software making the request. Clients SHOULD - include this field on every request unless specifically configured not - to do so. - - The {@link Implementation} schema requires `name` and `version`; other - fields are optional. - - The value is self-reported by the client and is not verified by the - protocol. It is intended for display, logging, and debugging. Servers - SHOULD NOT use it to change their behavior, and SHOULD NOT rely on it for - security decisions. - """ - io_modelcontextprotocol_log_level: Annotated[ - LoggingLevel | None, Field(alias="io.modelcontextprotocol/logLevel") - ] = None - """ - The desired log level for this request. Optional. - - If absent, the server MUST NOT send any {@link LoggingMessageNotificationnotifications/message} - notifications for this request. The client opts in to log messages by - explicitly setting a level. Replaces the former `logging/setLevel` RPC. - """ - io_modelcontextprotocol_protocol_version: Annotated[str, Field(alias="io.modelcontextprotocol/protocolVersion")] - """ - The MCP Protocol Version being used for this request. Required. - - For the HTTP transport, this value MUST match the `MCP-Protocol-Version` - header; otherwise the server MUST return a `400 Bad Request`. If the - server does not support the requested version, it MUST return an - {@link UnsupportedProtocolVersionError}. - """ - progress_token: Annotated[ProgressToken | None, Field(alias="progressToken")] = None - """ - If specified, the caller is requesting out-of-band progress notifications for this request (as represented by {@link ProgressNotificationnotifications/progress}). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications. - """ +ServerResult = ( + Result + | InputRequiredResult + | DiscoverResult + | ListResourcesResult + | ListResourceTemplatesResult + | ReadResourceResult + | SubscriptionsListenResult + | ListPromptsResult + | GetPromptResult + | ListToolsResult + | CallToolResult + | CompleteResult +) -class RequestParams(WireModel): +class CallToolResultResponse(WireModel): """ - Common params for any request. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - - -class ResourceRequestParams(WireModel): - """ - Common params for resource-related requests. - """ - - model_config = ConfigDict( - extra="ignore", - ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - uri: str - """ - The URI of the resource. The URI can use any protocol; it is up to the server how to interpret it. - """ - - -class ServerCapabilities(WireModel): - """ - Capabilities that a server may support. Known capabilities are defined here, in this schema, but this is not a closed set: any server can define its own, additional capabilities. - """ - - model_config = ConfigDict( - extra="ignore", - ) - completions: JSONObject | None = None - """ - Present if the server supports argument autocompletion suggestions. - """ - experimental: dict[str, JSONObject] | None = None - """ - Experimental, non-standard capabilities that the server supports. - """ - extensions: dict[str, JSONObject] | None = None - """ - Optional MCP extensions that the server supports. Keys are extension identifiers - (e.g., "io.modelcontextprotocol/tasks"), and values are per-extension settings - objects. An empty object indicates support with no settings. - - Keys MUST follow the {@link MetaObject`_meta` key naming rules}, with a - mandatory prefix. - """ - logging: JSONObject | None = None - """ - Present if the server supports sending log messages to the client. - """ - prompts: Prompts | None = None - """ - Present if the server offers any prompt templates. - """ - resources: Resources | None = None - """ - Present if the server offers any resources to read. - """ - tools: Tools | None = None - """ - Present if the server offers any tools to call. - """ - - -class SubscriptionsListenRequest(WireModel): - """ - Sent from the client to open a long-lived channel for receiving notifications - outside the context of a specific request. Replaces the previous HTTP GET - endpoint and ensures consistent behavior between HTTP and STDIO. + A successful response from the server for a {@link CallToolRequesttools/call} request. """ model_config = ConfigDict( @@ -3604,143 +3582,36 @@ class SubscriptionsListenRequest(WireModel): ) id: RequestId jsonrpc: Literal["2.0"] - method: Literal["subscriptions/listen"] - params: SubscriptionsListenRequestParams + result: InputRequiredResult | CallToolResult -class SubscriptionsListenRequestParams(WireModel): +ClientRequest = ( + DiscoverRequest + | ListResourcesRequest + | ListResourceTemplatesRequest + | ReadResourceRequest + | SubscriptionsListenRequest + | ListPromptsRequest + | GetPromptRequest + | ListToolsRequest + | CallToolRequest + | CompleteRequest +) + + +class GetPromptResultResponse(WireModel): """ - Parameters for a {@link SubscriptionsListenRequestsubscriptions/listen} request. + A successful response from the server for a {@link GetPromptRequestprompts/get} request. """ model_config = ConfigDict( extra="ignore", ) - meta: Annotated[RequestMetaObject, Field(alias="_meta")] - notifications: SubscriptionFilter - """ - The notifications the client opts in to on this stream. The server - **MUST NOT** send notification types the client has not explicitly - requested. - """ - - -class InputRequest(RootModel[CreateMessageRequest | ListRootsRequest | ElicitRequest]): - root: CreateMessageRequest | ListRootsRequest | ElicitRequest - - -class ServerResult( - RootModel[ - Result - | InputRequiredResult - | DiscoverResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | SubscriptionsListenResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | CompleteResult - ] -): - root: ( - Result - | InputRequiredResult - | DiscoverResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | SubscriptionsListenResult - | ListPromptsResult - | GetPromptResult - | ListToolsResult - | CallToolResult - | CompleteResult - ) - - -class ClientRequest( - RootModel[ - DiscoverRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscriptionsListenRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | CompleteRequest - ] -): - root: ( - DiscoverRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | SubscriptionsListenRequest - | ListPromptsRequest - | GetPromptRequest - | ListToolsRequest - | CallToolRequest - | CompleteRequest - ) - - -class InputRequests(RootModel[dict[str, InputRequest]]): - """ - A map of server-initiated requests that the client must fulfill. - Keys are server-assigned identifiers; values are the request objects. - """ - - root: dict[str, InputRequest] - - -class JSONArray(RootModel[list["JSONValue"]]): - root: list["JSONValue"] - - -class JSONObject(RootModel[dict[str, "JSONValue"]]): - root: dict[str, "JSONValue"] - - -class JSONValue(RootModel[Union[JSONObject, list["JSONValue"], str | int | float | bool | None]]): - root: Union[JSONObject, list["JSONValue"], str | int | float | bool | None] + id: RequestId + jsonrpc: Literal["2.0"] + result: InputRequiredResult | GetPromptResult AnyCallToolResult = CallToolResult | InputRequiredResult AnyGetPromptResult = GetPromptResult | InputRequiredResult AnyReadResourceResult = ReadResourceResult | InputRequiredResult - - -CallToolRequest.model_rebuild() -CallToolRequestParams.model_rebuild() -CallToolResultResponse.model_rebuild() -Elicitation.model_rebuild() -Sampling.model_rebuild() -ClientCapabilities.model_rebuild() -CompleteRequest.model_rebuild() -CompleteRequestParams.model_rebuild() -CreateMessageRequest.model_rebuild() -CreateMessageRequestParams.model_rebuild() -DiscoverRequest.model_rebuild() -DiscoverResult.model_rebuild() -GetPromptRequest.model_rebuild() -GetPromptRequestParams.model_rebuild() -GetPromptResultResponse.model_rebuild() -InputRequiredResult.model_rebuild() -InputResponseRequestParams.model_rebuild() -ListPromptsRequest.model_rebuild() -ListResourceTemplatesRequest.model_rebuild() -ListResourcesRequest.model_rebuild() -ListToolsRequest.model_rebuild() -PaginatedRequest.model_rebuild() -PaginatedRequestParams.model_rebuild() -ReadResourceRequest.model_rebuild() -ReadResourceRequestParams.model_rebuild() -ServerCapabilities.model_rebuild() -SubscriptionsListenRequest.model_rebuild() -JSONArray.model_rebuild() -JSONObject.model_rebuild() diff --git a/src/mcp/client/auth/extensions/client_credentials.py b/src/mcp/client/auth/extensions/client_credentials.py index 29197bb5..091f9e39 100644 --- a/src/mcp/client/auth/extensions/client_credentials.py +++ b/src/mcp/client/auth/extensions/client_credentials.py @@ -7,8 +7,10 @@ Provides OAuth providers for machine-to-machine authentication flows: """ import time +import warnings from collections.abc import Awaitable, Callable from typing import Any, Literal +from urllib.parse import urlparse from uuid import uuid4 import httpx2 @@ -16,7 +18,50 @@ import jwt from pydantic import BaseModel, Field from mcp.client.auth import OAuthClientProvider, OAuthFlowError, TokenStorage +from mcp.client.auth.oauth2 import OAuthContext +from mcp.client.auth.utils import issuers_match from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata +from mcp.shared.exceptions import MCPDeprecationWarning + + +def _checked_issuer(issuer: str | None) -> str | None: + if issuer is None: + warnings.warn( + "Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server " + "decides which authorization server receives this client's credentials; pass " + "issuer= so they are only ever sent there.", + MCPDeprecationWarning, + stacklevel=3, + ) + return None + if urlparse(issuer).scheme not in ("http", "https"): + raise ValueError(f"issuer must be the authorization server's http(s) issuer URL, got {issuer!r}") + return issuer + + +def _preferred_authorization_server(advertised: list[str], issuer: str | None) -> str: + """The advertised server matching the configured issuer if there is one, else the first.""" + return next( + (server for server in advertised if issuer is not None and issuers_match(server, issuer)), advertised[0] + ) + + +def _require_metadata_for_configured_issuer(context: OAuthContext, issuer: str | None) -> None: + """With an issuer configured, a token request is only built from metadata discovered for that issuer. + + Anything else held is dropped along with the tokens, so the next request starts discovery afresh + rather than refreshing against it. + """ + if issuer is None: + return + metadata = context.oauth_metadata + if metadata is not None and issuers_match(str(metadata.issuer), issuer): + return + context.oauth_metadata = None + context.clear_tokens() + if metadata is None: + raise OAuthFlowError(f"No authorization server metadata discovered for configured issuer {issuer}") + raise OAuthFlowError(f"Authorization server metadata issuer mismatch: {metadata.issuer} != {issuer}") class ClientCredentialsOAuthProvider(OAuthClientProvider): @@ -24,6 +69,9 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): This provider sets client_info directly, bypassing dynamic client registration. Use this when you already have client credentials (client_id and client_secret). + Pass `issuer` to name the authorization server those credentials belong to: token + requests are then only built from authorization server metadata for that issuer, and + the flow stops if the MCP server leads anywhere else. Example: ```python @@ -32,6 +80,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): storage=my_token_storage, client_id="my-client-id", client_secret="my-client-secret", + issuer="https://auth.example.com", ) ``` """ @@ -44,6 +93,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): client_secret: str, token_endpoint_auth_method: Literal["client_secret_basic", "client_secret_post"] = "client_secret_basic", scope: str | None = None, + issuer: str | None = None, ) -> None: """Initialize client_credentials OAuth provider. @@ -55,6 +105,12 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): token_endpoint_auth_method: Authentication method for token endpoint. Either "client_secret_basic" (default) or "client_secret_post". scope: Optional space-separated list of scopes to request. + issuer: The issuer identifier of the authorization server that issued + `client_id` and `client_secret`. When set, token requests are only built from + discovered authorization server metadata whose `issuer` is exactly this string; + otherwise the flow stops with `OAuthFlowError`. Omitting it is deprecated + (`MCPDeprecationWarning`) and it will be required in 3.0; until then, whichever + authorization server discovery yields is used. """ # Build minimal client_metadata for the base class client_metadata = OAuthClientMetadata( @@ -64,6 +120,7 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): scope=scope, ) super().__init__(server_url, client_metadata, storage, None, None) + self._issuer = _checked_issuer(issuer) # Store client_info to be set during _initialize - no dynamic registration needed self._fixed_client_info = OAuthClientInformationFull( redirect_uris=None, @@ -80,12 +137,17 @@ class ClientCredentialsOAuthProvider(OAuthClientProvider): self.context.client_info = self._fixed_client_info self._initialized = True + def _select_authorization_server(self, advertised: list[str]) -> str: + return _preferred_authorization_server(advertised, self._issuer) + async def _perform_authorization(self) -> httpx2.Request: """Perform client_credentials authorization.""" return await self._exchange_token_client_credentials() async def _exchange_token_client_credentials(self) -> httpx2.Request: """Build token exchange request for client_credentials grant.""" + _require_metadata_for_configured_issuer(self.context, self._issuer) + token_data: dict[str, Any] = { "grant_type": "client_credentials", } @@ -118,6 +180,7 @@ def static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]: storage=my_token_storage, client_id="my-client-id", assertion_provider=static_assertion_provider(my_prebuilt_jwt), + issuer="https://auth.example.com", ) ``` @@ -152,6 +215,7 @@ class SignedJWTParameters(BaseModel): storage=my_token_storage, client_id="my-client-id", assertion_provider=jwt_params.create_assertion_provider(), + issuer="https://auth.example.com", ) ``` """ @@ -196,7 +260,10 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): The JWT assertion's audience MUST be the authorization server's issuer identifier (per RFC 7523bis security updates). The `assertion_provider` callback receives - this audience value and must return a JWT with that audience. + this audience value and must return a JWT with that audience. Pass `issuer` to name + the authorization server this client is registered with: an assertion is then only + minted once metadata for that issuer has been discovered, and token requests are only + built from that metadata. **Option 1: Pre-built JWT via Workload Identity Federation** @@ -214,6 +281,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): storage=my_token_storage, client_id="my-client-id", assertion_provider=get_workload_identity_token, + issuer="https://auth.example.com", ) ``` @@ -227,6 +295,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): storage=my_token_storage, client_id="my-client-id", assertion_provider=static_assertion_provider(my_prebuilt_jwt), + issuer="https://auth.example.com", ) ``` @@ -245,6 +314,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): storage=my_token_storage, client_id="my-client-id", assertion_provider=jwt_params.create_assertion_provider(), + issuer="https://auth.example.com", ) ``` """ @@ -256,6 +326,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): client_id: str, assertion_provider: Callable[[str], Awaitable[str]], scope: str | None = None, + issuer: str | None = None, ) -> None: """Initialize private_key_jwt OAuth provider. @@ -269,6 +340,12 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): `static_assertion_provider()` for pre-built JWTs, or provide your own callback for workload identity federation. scope: Optional space-separated list of scopes to request. + issuer: The issuer identifier of the authorization server `client_id` is + registered with. When set, an assertion is only minted, and token requests + are only built, once authorization server metadata whose `issuer` is exactly this + string has been discovered; otherwise the flow stops with `OAuthFlowError`. + Omitting it is deprecated (`MCPDeprecationWarning`) and it will be required in + 3.0; until then, whichever authorization server discovery yields is used. """ # Build minimal client_metadata for the base class client_metadata = OAuthClientMetadata( @@ -279,6 +356,7 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): ) super().__init__(server_url, client_metadata, storage, None, None) self._assertion_provider = assertion_provider + self._issuer = _checked_issuer(issuer) # Store client_info to be set during _initialize - no dynamic registration needed self._fixed_client_info = OAuthClientInformationFull( redirect_uris=None, @@ -294,6 +372,9 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): self.context.client_info = self._fixed_client_info self._initialized = True + def _select_authorization_server(self, advertised: list[str]) -> str: + return _preferred_authorization_server(advertised, self._issuer) + async def _perform_authorization(self) -> httpx2.Request: """Perform client_credentials authorization with private_key_jwt.""" return await self._exchange_token_client_credentials() @@ -314,6 +395,8 @@ class PrivateKeyJWTOAuthProvider(OAuthClientProvider): async def _exchange_token_client_credentials(self) -> httpx2.Request: """Build token exchange request for client_credentials grant with private_key_jwt.""" + _require_metadata_for_configured_issuer(self.context, self._issuer) + token_data: dict[str, Any] = { "grant_type": "client_credentials", } diff --git a/src/mcp/client/auth/extensions/identity_assertion.py b/src/mcp/client/auth/extensions/identity_assertion.py index 17738a70..35e48c03 100644 --- a/src/mcp/client/auth/extensions/identity_assertion.py +++ b/src/mcp/client/auth/extensions/identity_assertion.py @@ -39,6 +39,7 @@ from mcp.client.auth.utils import ( union_scopes, validate_metadata_issuer, ) +from mcp.shared._httpx_utils import RedirectAwareAuth, redirect_note from mcp.shared.auth import JWT_BEARER_GRANT_TYPE, OAuthClientInformationFull, OAuthToken from mcp.shared.auth_utils import calculate_token_expiry, resource_url_from_server_url @@ -56,7 +57,7 @@ def _origin(url: str) -> tuple[str, str, int | None]: return (parsed.scheme, parsed.hostname or "", port) -class IdentityAssertionOAuthProvider(httpx2.Auth): +class IdentityAssertionOAuthProvider(RedirectAwareAuth): """`httpx2.Auth` for the SEP-990 ID-JAG flow (RFC 7523 jwt-bearer grant) against a configured AS. The authorization server `issuer` is fixed at construction; metadata is fetched from its @@ -159,7 +160,7 @@ class IdentityAssertionOAuthProvider(httpx2.Auth): data["client_secret"] = self._client.client_secret return httpx2.Request("POST", self._token_endpoint, data=data, headers=headers) - async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: + async def _auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: async with self._lock: if not self._initialized: self._tokens = await self._storage.get_tokens() @@ -201,7 +202,9 @@ class IdentityAssertionOAuthProvider(httpx2.Auth): token_response = yield self._build_token_request(scope_to_request, assertion) if token_response.status_code != 200: body = (await token_response.aread()).decode(errors="replace") - raise OAuthTokenError(f"Token exchange failed ({token_response.status_code}): {body}") + raise OAuthTokenError( + f"Token exchange failed ({token_response.status_code}){redirect_note(token_response)}: {body}" + ) tokens = await handle_token_response_scopes(token_response) if tokens.scope is None: tokens.scope = scope_to_request diff --git a/src/mcp/client/auth/oauth2.py b/src/mcp/client/auth/oauth2.py index 7dc62b52..85882089 100644 --- a/src/mcp/client/auth/oauth2.py +++ b/src/mcp/client/auth/oauth2.py @@ -17,7 +17,7 @@ from urllib.parse import quote, urlencode, urljoin, urlparse import anyio import httpx2 from mcp_types.version import is_version_at_least -from pydantic import BaseModel, Field, ValidationError +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, TypeAdapter, ValidationError from mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError from mcp.client.auth.utils import ( @@ -36,11 +36,13 @@ from mcp.client.auth.utils import ( handle_registration_response, handle_token_response_scopes, is_valid_client_metadata_url, + issuers_match, should_use_client_metadata_url, union_scopes, validate_authorization_response_iss, validate_metadata_issuer, ) +from mcp.shared._httpx_utils import RedirectAwareAuth, redirect_note from mcp.shared.auth import ( AuthorizationCodeResult, OAuthClientInformationFull, @@ -276,7 +278,17 @@ class OAuthContext: return data, headers -class OAuthClientProvider(httpx2.Auth): +_ORIGIN_URL = TypeAdapter(AnyHttpUrl, config=ConfigDict(url_preserve_empty_path=True)) + + +def _origin_issuer(server_url: str) -> str: + """The resource server's origin as an issuer identifier: `scheme://authority`, rendered the way + `OAuthMetadata.issuer` renders URLs (host case, default ports) so the two compare as strings.""" + parsed = urlparse(server_url) + return str(_ORIGIN_URL.validate_python(f"{parsed.scheme}://{parsed.netloc}")) + + +class OAuthClientProvider(RedirectAwareAuth): """OAuth2 authentication for httpx2. Handles OAuth flow with automatic client registration and token storage. @@ -469,7 +481,9 @@ class OAuthClientProvider(httpx2.Auth): if response.status_code not in {200, 201}: body = await response.aread() body_text = body.decode("utf-8") - raise OAuthTokenError(f"Token exchange failed ({response.status_code}): {body_text}") + raise OAuthTokenError( + f"Token exchange failed ({response.status_code}){redirect_note(response)}: {body_text}" + ) # Parse and validate response with scope validation token_response = await handle_token_response_scopes(response) @@ -519,7 +533,7 @@ class OAuthClientProvider(httpx2.Auth): async def _handle_refresh_response(self, response: httpx2.Response) -> bool: """Handle token refresh response. Returns True if successful.""" if response.status_code != 200: - logger.warning(f"Token refresh failed: {response.status_code}") + logger.warning(f"Token refresh failed: {response.status_code}{redirect_note(response)}") self.context.clear_tokens() return False @@ -577,8 +591,18 @@ class OAuthClientProvider(httpx2.Auth): if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource): raise OAuthFlowError(f"Protected resource {prm_resource} does not match expected {default_resource}") - async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: - """httpx2 auth flow integration.""" + def _select_authorization_server(self, advertised: list[str]) -> str: + """Which of the servers listed in protected resource metadata to use: the first (the list is never empty).""" + return advertised[0] + + def _expected_issuer(self) -> str: + """The issuer that authorization server metadata and client credentials must belong to: the + PRM-advertised server, or on the legacy no-PRM path the resource server's origin, which is what + the 2025-03-26 well-known URL is built from (RFC 8414 §3.3).""" + return self.context.auth_server_url or _origin_issuer(self.context.server_url) + + async def _auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: + """The OAuth flow proper; `async_auth_flow` drives it (see `RedirectAwareAuth`).""" async with self.context.lock: if not self._initialized: await self._initialize() @@ -600,107 +624,122 @@ class OAuthClientProvider(httpx2.Auth): response = yield request - if response.status_code == 401: + step_up = ( + response.status_code == 403 and extract_field_from_www_auth(response, "error") == "insufficient_scope" + ) + + if response.status_code == 401 or step_up: # Perform full OAuth flow try: - # OAuth flow must be inline due to generator constraints - www_auth_resource_metadata_url = extract_resource_metadata_from_www_auth(response) + # Read before discovery, which may clear the tokens: on a restart the stored + # token's scope is the only record of what was granted (see Step 3). + granted_scope = self.context.current_tokens.scope if self.context.current_tokens else None - # Step 1: Discover protected resource metadata (SEP-985 with fallback support) - prm_discovery_urls = build_protected_resource_metadata_discovery_urls( - www_auth_resource_metadata_url, self.context.server_url - ) + # OAuth flow must be inline due to generator constraints. + # Steps 1-2 run on every 401. A scope step-up reuses the metadata discovered earlier + # in this process, and discovers it first when none is held yet (for example when + # tokens were loaded from storage), so re-authorization targets the right server. + if response.status_code == 401 or self.context.oauth_metadata is None: + www_auth_resource_metadata_url = extract_resource_metadata_from_www_auth(response) - for url in prm_discovery_urls: # pragma: no branch - discovery_request = create_oauth_metadata_request(url) - - discovery_response = yield discovery_request # sending request - - prm = await handle_protected_resource_response(discovery_response) - if prm: - # Validate PRM resource matches server URL (RFC 8707) - await self._validate_resource_match(prm) - self.context.protected_resource_metadata = prm - - # todo: try all authorization_servers to find the OASM - assert ( - len(prm.authorization_servers) > 0 - ) # this is always true as authorization_servers has a min length of 1 - - self.context.auth_server_url = str(prm.authorization_servers[0]) - break - else: - logger.debug(f"Protected resource metadata discovery failed: {url}") - - # SEP-2352: stored credentials are bound to the issuer that registered them. - # If the authorization server changed, drop them (and the old tokens) so the - # flow re-registers instead of presenting another server's credentials. - if ( - self.context.client_info is not None - and self.context.auth_server_url is not None - and not credentials_match_issuer( - self.context.client_info, self.context.auth_server_url, self.context.client_metadata_url + # Step 1: Discover protected resource metadata (SEP-985 with fallback support) + prm_discovery_urls = build_protected_resource_metadata_discovery_urls( + www_auth_resource_metadata_url, self.context.server_url ) - ): - logger.debug("Authorization server changed; discarding bound credentials and re-registering") - self.context.client_info = None - self.context.clear_tokens() - # Any cached AS metadata is for the old server; drop it so a failed - # rediscovery cannot leak the old registration/token endpoints into Step 4. - self.context.oauth_metadata = None - asm_discovery_urls = build_oauth_authorization_server_metadata_discovery_urls( - self.context.auth_server_url, self.context.server_url - ) + prm_request_failed: int | None = None + for url in prm_discovery_urls: + discovery_request = create_oauth_metadata_request(url) - # Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers) - for url in asm_discovery_urls: # pragma: no branch - oauth_metadata_request = create_oauth_metadata_request(url) - oauth_metadata_response = yield oauth_metadata_request + discovery_response = yield discovery_request # sending request - ok, asm = await handle_auth_metadata_response(oauth_metadata_response) - if not ok: - break - if ok and asm: - # SEP-2468: metadata issuer must match the discovery issuer - if self.context.auth_server_url is not None: - validate_metadata_issuer(asm, self.context.auth_server_url) - self.context.oauth_metadata = asm - break + if discovery_response.status_code >= 500 or discovery_response.status_code == 429: + prm_request_failed = discovery_response.status_code + prm = await handle_protected_resource_response(discovery_response) + if prm: + # Validate PRM resource matches server URL (RFC 8707) + await self._validate_resource_match(prm) + self.context.protected_resource_metadata = prm + + self.context.auth_server_url = self._select_authorization_server( + [str(url) for url in prm.authorization_servers] + ) + break + else: + logger.debug(f"Protected resource metadata discovery failed: {url}") else: - logger.debug(f"OAuth metadata discovery failed: {url}") + if prm_request_failed is not None: + # A server error says nothing about whether the resource publishes + # metadata, so it must not send the flow down the legacy path. + raise OAuthFlowError( + f"Protected resource metadata request failed: HTTP {prm_request_failed}" + ) - # SEP-2352: on the legacy no-PRM path the issuer is only known after ASM - # discovery, so re-evaluate the binding here using the discovered metadata - # issuer (mirroring the bound_issuer fallback in Step 4). - if ( - self.context.client_info is not None - and self.context.auth_server_url is None - and self.context.oauth_metadata is not None - and not credentials_match_issuer( - self.context.client_info, - str(self.context.oauth_metadata.issuer), - self.context.client_metadata_url, + expected_issuer = self._expected_issuer() + + # SEP-2352: stored credentials are bound to the issuer that registered them. + # Decided before any metadata is fetched: if the expected issuer is a different + # server, drop them (and the old tokens) so the flow re-registers instead of + # presenting another server's credentials. + if self.context.client_info is not None and not credentials_match_issuer( + self.context.client_info, expected_issuer, self.context.client_metadata_url + ): + logger.debug( + "Authorization server changed; discarding bound credentials and re-registering" + ) + self.context.client_info = None + self.context.clear_tokens() + # Any cached AS metadata is for the old server; drop it so a failed + # rediscovery cannot leak the old registration/token endpoints into Step 4. + self.context.oauth_metadata = None + + asm_discovery_urls = build_oauth_authorization_server_metadata_discovery_urls( + self.context.auth_server_url, self.context.server_url ) - ): - logger.debug("Authorization server changed; discarding bound credentials and re-registering") - self.context.client_info = None - self.context.clear_tokens() + + # Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers) + for url in asm_discovery_urls: # pragma: no branch + oauth_metadata_request = create_oauth_metadata_request(url) + oauth_metadata_response = yield oauth_metadata_request + + ok, asm = await handle_auth_metadata_response(oauth_metadata_response) + if not ok: + break + if ok and asm: + # SEP-2468 / RFC 8414 §3.3: the metadata must name the expected issuer. + # On the legacy path a root issuer rendered with its trailing slash + # names the same origin. + if self.context.auth_server_url is None and issuers_match( + str(asm.issuer), expected_issuer + ): + expected_issuer = str(asm.issuer) + validate_metadata_issuer(asm, expected_issuer) + self.context.oauth_metadata = asm + break + else: + logger.debug(f"OAuth metadata discovery failed: {url}") # Step 3: Apply scope selection strategy - self.context.client_metadata.scope = get_client_metadata_scopes( + challenged_scope = get_client_metadata_scopes( extract_scope_from_www_auth(response), self.context.protected_resource_metadata, self.context.oauth_metadata, self.context.client_metadata.grant_types, ) + if step_up: + # SEP-2350: union previously requested scopes with the newly challenged ones so + # escalating one operation keeps the others' grants, folding in the granted + # scope read above since client_metadata.scope is not reloaded on a restart. + prior_scope = union_scopes(self.context.client_metadata.scope, granted_scope) + self.context.client_metadata.scope = union_scopes(prior_scope, challenged_scope) + else: + self.context.client_metadata.scope = challenged_scope # Step 4: Register client or use URL-based client ID (CIMD) if not self.context.client_info: - # SEP-2352: the issuer to bind these credentials to, when known. - discovered_issuer: str | None = None - if self.context.oauth_metadata is not None: - discovered_issuer = self.context.auth_server_url or str(self.context.oauth_metadata.issuer) + # SEP-2352: the issuer to bind these credentials to, once metadata for it + # was actually found. + discovered_issuer = self._expected_issuer() if self.context.oauth_metadata is not None else None if should_use_client_metadata_url( self.context.oauth_metadata, self.context.client_metadata_url @@ -752,34 +791,3 @@ class OAuthClientProvider(httpx2.Auth): # Retry with new tokens self._add_auth_header(request) yield request - elif response.status_code == 403: - # Step 1: Extract error field from WWW-Authenticate header - error = extract_field_from_www_auth(response, "error") - - # Step 2: Check if we need to step-up authorization - if error == "insufficient_scope": # pragma: no branch - try: - # Step 2a: Union previously requested scopes with the newly challenged - # scopes (SEP-2350) so escalating one operation keeps the others' grants. - # Fold in the stored token's scope too: on a restart the token is reloaded - # but client_metadata.scope is not, so it would otherwise be the only basis. - challenged_scope = get_client_metadata_scopes( - extract_scope_from_www_auth(response), - self.context.protected_resource_metadata, - self.context.oauth_metadata, - self.context.client_metadata.grant_types, - ) - granted_scope = self.context.current_tokens.scope if self.context.current_tokens else None - prior_scope = union_scopes(self.context.client_metadata.scope, granted_scope) - self.context.client_metadata.scope = union_scopes(prior_scope, challenged_scope) - - # Step 2b: Perform (re-)authorization and token exchange - token_response = yield await self._perform_authorization() - await self._handle_token_response(token_response) - except Exception: # pragma: no cover - logger.exception("OAuth flow error") - raise - - # Retry with new tokens - self._add_auth_header(request) - yield request diff --git a/src/mcp/client/auth/utils.py b/src/mcp/client/auth/utils.py index 31e2e5ca..0a2ba80a 100644 --- a/src/mcp/client/auth/utils.py +++ b/src/mcp/client/auth/utils.py @@ -8,6 +8,7 @@ from pydantic import AnyUrl, ValidationError from pydantic_core import from_json from mcp.client.auth import OAuthFlowError, OAuthRegistrationError, OAuthTokenError +from mcp.shared._httpx_utils import redirect_note from mcp.shared.auth import ( OAuthClientInformationFull, OAuthClientMetadata, @@ -54,7 +55,7 @@ def extract_resource_metadata_from_www_auth(response: Response) -> str | None: Returns: Resource metadata URL if found in WWW-Authenticate header, None otherwise """ - if not response or response.status_code != 401: + if not response or response.status_code not in (401, 403): return None # pragma: no cover return extract_field_from_www_auth(response, "resource_metadata") @@ -230,9 +231,9 @@ async def handle_auth_metadata_response(response: Response) -> tuple[bool, OAuth return True, asm except ValidationError: # pragma: no cover return True, None - elif response.status_code < 400 or response.status_code >= 500: - return False, None # Non-4XX error, stop trying - return True, None + elif 300 <= response.status_code < 500: + return True, None # Not served at this URL (redirects are not followed) - try the next candidate + return False, None # Server error or unexpected status, stop trying def validate_authorization_response_iss(iss: str | None, oauth_metadata: OAuthMetadata | None) -> None: @@ -297,7 +298,9 @@ async def handle_registration_response(response: Response) -> OAuthClientInforma """Handle registration response.""" if response.status_code not in (200, 201): await response.aread() - raise OAuthRegistrationError(f"Registration failed: {response.status_code} {response.text}") + raise OAuthRegistrationError( + f"Registration failed: {response.status_code}{redirect_note(response)} {response.text}" + ) try: content = await response.aread() @@ -343,7 +346,8 @@ def credentials_match_issuer( document is resolved by whichever server is in use — so it always matches; CIMD is identified by the client ID being the configured `client_metadata_url`, not by URL shape (a registration server may also issue URL-shaped IDs that are bound to it). Credentials with a recorded issuer - match only when it equals `issuer` (simple string comparison). Credentials with no recorded + match only when it equals `issuer` (simple string comparison; a root issuer with and without + its trailing slash count as equal). Credentials with no recorded issuer (pre-registered, or stored before issuer binding existed) carry no binding to enforce and are left as-is. """ @@ -351,7 +355,18 @@ def credentials_match_issuer( return True if client_info.issuer is None: return True - return client_info.issuer == issuer + return issuers_match(client_info.issuer, issuer) + + +def issuers_match(a: str, b: str) -> bool: + """Simple string comparison of two issuer identifiers (RFC 8414 section 3.3), except that a root + issuer with and without its trailing slash (`scheme://authority` and `scheme://authority/`) name + the same server.""" + if a == b: + return True + shorter, longer = sorted((a, b), key=len) + parsed = urlparse(shorter) + return longer == f"{shorter}/" and shorter == f"{parsed.scheme}://{parsed.netloc}" def should_use_client_metadata_url( diff --git a/src/mcp/client/client.py b/src/mcp/client/client.py index 2f467c26..f921c7e3 100644 --- a/src/mcp/client/client.py +++ b/src/mcp/client/client.py @@ -268,17 +268,12 @@ class Client: Example: ```python - from mcp.client import Client - from mcp.server.mcpserver import MCPServer + import asyncio - server = MCPServer("test") - - @server.tool() - def add(a: int, b: int) -> int: - return a + b + from mcp import Client async def main(): - async with Client(server) as client: + async with Client("http://localhost:8000/mcp") as client: result = await client.call_tool("add", {"a": 1, "b": 2}) asyncio.run(main()) diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index f18cc0ef..a6181121 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -80,19 +80,12 @@ def _clamp_inbound_ttl(raw: dict[str, Any]) -> None: @cache def _wire_fields(target: type[BaseModel] | UnionType) -> frozenset[str]: - """Top-level wire keys `target` declares (its members', for a union). - - A `RootModel` row (e.g. an empty result carried as `RootModel[Result]`) - reports its wrapped type's keys, not the pydantic-internal `root`. - """ + """Top-level wire keys `target` declares (its members', for a union).""" members: tuple[Any, ...] = get_args(target) if isinstance(target, UnionType) else (target,) models = [m for m in members if isinstance(m, type) and issubclass(m, BaseModel)] fields: set[str] = set() for model in models: - if getattr(model, "__pydantic_root_model__", False): # a RootModel wrapper row - fields |= _wire_fields(model.model_fields["root"].annotation) - else: - fields.update(field.alias or name for name, field in model.model_fields.items()) + fields.update(field.alias or name for name, field in model.model_fields.items()) return frozenset(fields) @@ -1126,7 +1119,8 @@ class ClientSession: """Revalidate a `CallToolResult` against the tool's declared output schema. Raises: - RuntimeError: Structured content is missing or does not conform to the schema. + RuntimeError: Structured content is missing or does not conform to the schema, or the + schema is invalid or has a `$ref` that does not resolve within the schema document. """ if name not in self._tool_output_schemas: # refresh output schema cache @@ -1140,6 +1134,7 @@ class ClientSession: if output_schema is not None: from jsonschema import exceptions as jsonschema_exceptions + from referencing.exceptions import Unresolvable if result.structured_content is None: raise RuntimeError(f"Tool {name} has an output schema but did not return structured content") @@ -1147,10 +1142,14 @@ class ClientSession: # `best_match` picks the same error the previous `jsonschema.validate()` call raised, # so the message a caller sees is unchanged. It is untyped upstream. errors = validator.iter_errors(result.structured_content) - error = cast( - "Exception | None", - jsonschema_exceptions.best_match(errors), # pyright: ignore[reportUnknownMemberType] - ) + try: + error = cast( + "Exception | None", + jsonschema_exceptions.best_match(errors), # pyright: ignore[reportUnknownMemberType] + ) + except Unresolvable as e: + # A `$ref` did not resolve within the schema document. + raise RuntimeError(f"Invalid schema for tool {name}: {e}") from e if error is not None: raise RuntimeError(f"Invalid structured content returned by tool {name}: {error}") from error @@ -1168,6 +1167,7 @@ class ClientSession: """ from jsonschema import SchemaError from jsonschema.validators import validator_for + from referencing import Registry if (validator := self._tool_output_validators.get(name)) is not None: return validator @@ -1177,9 +1177,8 @@ class ClientSession: validator_cls.check_schema(output_schema) except SchemaError as e: raise RuntimeError(f"Invalid schema for tool {name}: {e}") - # jsonschema ships no `py.typed`, so pyright reads typeshed's stub, which declares - # `registry` as required (concrete validators default it); cast to a schema-only ctor. - validator = cast("Callable[[dict[str, Any]], Validator]", validator_cls)(output_schema) + # An explicit empty registry: `$ref`s resolve within the schema document and the bundled metaschemas. + validator = validator_cls(output_schema, registry=Registry()) self._tool_output_validators[name] = validator return validator diff --git a/src/mcp/client/sse.py b/src/mcp/client/sse.py index 31d0f353..f72ff273 100644 --- a/src/mcp/client/sse.py +++ b/src/mcp/client/sse.py @@ -12,7 +12,12 @@ from httpx2 import SSEError from mcp.shared._compat import resync_tracer from mcp.shared._context_streams import create_context_streams -from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client +from mcp.shared._httpx_utils import ( + McpHttpClientFactory, + create_mcp_http_client, + request_within_origin, + sse_within_origin, +) from mcp.shared.message import SessionMessage logger = logging.getLogger(__name__) @@ -47,7 +52,14 @@ async def sse_client( headers: Optional headers to include in requests. timeout: HTTP timeout for regular operations (in seconds). sse_read_timeout: Timeout for SSE read operations (in seconds). - httpx_client_factory: Factory function for creating the httpx2 client. + httpx_client_factory: Factory function for creating the httpx2 client. Whichever client it + returns, MCP requests follow a redirect only when it stays on the endpoint's origin + (same scheme, host and port, or http to https on the same host with default ports) and + keeps the request method (any status for the SSE GET, 307/308 for a message POST); any + other redirect is not followed, so connecting fails with + `httpx2.HTTPStatusError` for the redirect response. The client's `follow_redirects` + setting is not consulted; the SDK's OAuth providers apply the same rule to the requests + they make. auth: Optional httpx2 authentication handler. on_session_created: Optional callback invoked with the session ID when received. """ @@ -55,7 +67,7 @@ async def sse_client( async with httpx_client_factory( headers=headers, auth=auth, timeout=httpx2.Timeout(timeout, read=sse_read_timeout) ) as client: - async with client.sse(url) as event_source: + async with sse_within_origin(client, url) as event_source: event_source.response.raise_for_status() logger.debug("SSE connection established") @@ -121,13 +133,11 @@ async def sse_client( async def _send_message(session_message: SessionMessage) -> None: logger.debug(f"Sending client message: {session_message}") - response = await client.post( + response = await request_within_origin( + client, + "POST", endpoint_url, - json=session_message.message.model_dump( - by_alias=True, - mode="json", - exclude_unset=True, - ), + json=session_message.message.model_dump(by_alias=True, mode="json", exclude_unset=True), ) response.raise_for_status() logger.debug(f"Client message sent successfully: {response.status_code}") diff --git a/src/mcp/client/stdio.py b/src/mcp/client/stdio.py index 3e03eef9..6a3ad121 100644 --- a/src/mcp/client/stdio.py +++ b/src/mcp/client/stdio.py @@ -18,6 +18,7 @@ from typing import Literal, TextIO import anyio import anyio.lowlevel +import anyio.to_thread import mcp_types as types from anyio.abc import AsyncResource, Process from anyio.streams.text import TextReceiveStream @@ -120,7 +121,7 @@ async def stdio_client( OSError: If the server process cannot be spawned. ValueError: If the spawn parameters are invalid (embedded NUL bytes). """ - command = _get_executable_command(server.command) + command = await _get_executable_command(server.command) process = await _create_platform_compatible_process( command=command, @@ -317,10 +318,10 @@ def _close_subprocess_transport(process: ServerProcess) -> None: close() -def _get_executable_command(command: str) -> str: +async def _get_executable_command(command: str) -> str: """Normalizes the command for the current platform.""" - if sys.platform == "win32": # pragma: no cover - return get_windows_executable_command(command) + if sys.platform == "win32": + return await anyio.to_thread.run_sync(get_windows_executable_command, command, abandon_on_cancel=True) else: # pragma: lax no cover return command diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 226b0fec..82de50fd 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -33,7 +33,13 @@ from pydantic import ValidationError from mcp.client._transport import TransportStreams from mcp.shared._compat import resync_tracer from mcp.shared._context_streams import ContextReceiveStream, ContextSendStream, create_context_streams -from mcp.shared._httpx_utils import create_mcp_http_client +from mcp.shared._httpx_utils import ( + create_mcp_http_client, + redirect_location, + request_within_origin, + sse_within_origin, + stream_within_origin, +) from mcp.shared.inbound import MCP_PROTOCOL_VERSION_HEADER from mcp.shared.jsonrpc_dispatcher import cancelled_request_id_from_params from mcp.shared.message import ClientMessageMetadata, SessionMessage @@ -62,6 +68,21 @@ class ResumptionError(StreamableHTTPError): """Raised when resumption request is invalid.""" +def _unfollowed_redirect(response: httpx2.Response) -> str | None: + """Describe a redirect `stream_within_origin` left unfollowed, or None if `response` is not one.""" + location = redirect_location(response) + if location is None: + return None + if response.request.url.scheme == "https" and location.scheme == "http": + return ( + f"Redirect to {location} not followed: it would downgrade this HTTPS endpoint to plain HTTP.\n" + "The server is likely behind a TLS-terminating proxy whose forwarded headers it does not trust,\n" + f"often combined with a trailing-slash difference. Try {location.copy_with(scheme='https')} instead, " + "or fix the proxy settings." + ) + return f"Redirect to {location} not followed; use that URL as the endpoint if it is the intended server" + + @dataclass class RequestContext: """Context for a request operation.""" @@ -210,7 +231,11 @@ class StreamableHTTPTransport: if last_event_id: headers[LAST_EVENT_ID] = last_event_id - async with client.sse(self.url, headers=headers) as event_source: + async with sse_within_origin(client, self.url, headers=headers) as event_source: + if (redirect := _unfollowed_redirect(event_source.response)) is not None: + # The same GET would be redirected again, so retrying cannot help. + logger.warning(f"GET stream not opened: {redirect}") + return event_source.response.raise_for_status() logger.debug("GET SSE connection established") @@ -253,7 +278,14 @@ class StreamableHTTPTransport: if isinstance(ctx.session_message.message, JSONRPCRequest): # pragma: no branch original_request_id = ctx.session_message.message.id - async with ctx.client.sse(self.url, headers=headers) as event_source: + async with sse_within_origin(ctx.client, self.url, headers=headers) as event_source: + if (redirect := _unfollowed_redirect(event_source.response)) is not None: + logger.warning(redirect) + assert original_request_id is not None + await self._resolve_abandoned_request( + ctx.read_stream_writer, original_request_id, redirect, code=INVALID_REQUEST + ) + return event_source.response.raise_for_status() logger.debug("Resumption GET SSE connection established") @@ -320,7 +352,8 @@ class StreamableHTTPTransport: if ctx.metadata is not None and ctx.metadata.headers is not None: headers.update(ctx.metadata.headers) - async with ctx.client.stream( + async with stream_within_origin( + ctx.client, "POST", self.url, json=message.model_dump(by_alias=True, mode="json", exclude_unset=True), @@ -339,6 +372,14 @@ class StreamableHTTPTransport: ) return + if (redirect := _unfollowed_redirect(response)) is not None: + logger.warning(redirect) + if isinstance(message, JSONRPCRequest): + await self._resolve_abandoned_request( + ctx.read_stream_writer, message.id, redirect, code=INVALID_REQUEST + ) + return + if response.status_code >= 400: if isinstance(message, JSONRPCRequest): # A spec-correct server may return the JSON-RPC error in the @@ -501,7 +542,7 @@ class StreamableHTTPTransport: headers[LAST_EVENT_ID] = last_event_id try: - async with ctx.client.sse(self.url, headers=headers) as event_source: + async with sse_within_origin(ctx.client, self.url, headers=headers) as event_source: event_source.response.raise_for_status() logger.info("Reconnected to SSE stream") @@ -626,7 +667,7 @@ class StreamableHTTPTransport: try: headers = self._prepare_headers() - response = await client.delete(self.url, headers=headers) + response = await request_within_origin(client, "DELETE", self.url, headers=headers) if response.status_code == 405: logger.debug("Server does not allow session termination") @@ -650,6 +691,13 @@ async def streamable_http_client( http_client: Optional pre-configured httpx2.AsyncClient. If None, a default client with recommended MCP timeouts will be created. To configure headers, authentication, or other HTTP settings, create an httpx2.AsyncClient and pass it here. + Whichever client is used, MCP requests follow a redirect only when it stays on the + endpoint's origin (same scheme, host and port, or http to https on the same host with + default ports) and keeps the request method (307/308 for a POST; any status for the GET + stream); any other redirect is not followed and the message it answered fails with an + error naming the location. The + client's `follow_redirects` setting is not consulted; the SDK's OAuth providers apply the + same rule to the requests they make. terminate_on_close: If True, send a DELETE request to terminate the session when the context exits. Yields: diff --git a/src/mcp/server/auth/handlers/authorize.py b/src/mcp/server/auth/handlers/authorize.py index 5cf93cf8..91955eb9 100644 --- a/src/mcp/server/auth/handlers/authorize.py +++ b/src/mcp/server/auth/handlers/authorize.py @@ -2,8 +2,7 @@ import logging from dataclasses import dataclass from typing import Any, Literal -# TODO(Marcelo): We should drop the `RootModel`. -from pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError # noqa: TID251 +from pydantic import AnyUrl, BaseModel, Field, TypeAdapter, ValidationError from starlette.datastructures import FormData, QueryParams from starlette.requests import Request from starlette.responses import RedirectResponse, Response @@ -20,6 +19,7 @@ from mcp.server.auth.provider import ( from mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError logger = logging.getLogger(__name__) +_ANY_URL_ADAPTER = TypeAdapter(AnyUrl) class AuthorizationRequest(BaseModel): @@ -59,10 +59,6 @@ def best_effort_extract_string(key: str, params: None | FormData | QueryParams) return None -class AnyUrlModel(RootModel[AnyUrl]): - root: AnyUrl - - @dataclass class AuthorizationHandler: provider: OAuthAuthorizationServerProvider[Any, Any, Any] @@ -107,9 +103,9 @@ class AuthorizationHandler: if params is not None and "redirect_uri" not in params: raw_redirect_uri = None else: - raw_redirect_uri = AnyUrlModel.model_validate( + raw_redirect_uri = _ANY_URL_ADAPTER.validate_python( best_effort_extract_string("redirect_uri", params) - ).root + ) redirect_uri = client.validate_redirect_uri(raw_redirect_uri) except (ValidationError, InvalidRedirectUriError): # if the redirect URI is invalid, ignore it & just return the diff --git a/src/mcp/server/auth/middleware/bearer_auth.py b/src/mcp/server/auth/middleware/bearer_auth.py index 29413abf..1a070cd2 100644 --- a/src/mcp/server/auth/middleware/bearer_auth.py +++ b/src/mcp/server/auth/middleware/bearer_auth.py @@ -1,14 +1,17 @@ import json +import logging import time from typing import Any, TypedDict -from pydantic import AnyHttpUrl +from pydantic import AnyHttpUrl, ValidationError from starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser from starlette.requests import HTTPConnection from starlette.types import Receive, Scope, Send from mcp.server.auth.provider import AccessToken, TokenVerifier, principal_components +logger = logging.getLogger(__name__) + class AuthenticatedUser(SimpleUser): """User with authentication info.""" @@ -39,10 +42,15 @@ def authorization_context(user: AuthenticatedUser) -> AuthorizationContext: class BearerAuthBackend(AuthenticationBackend): - """Authentication backend that validates Bearer tokens using a TokenVerifier.""" + """Authentication backend that validates Bearer tokens using a TokenVerifier. - def __init__(self, token_verifier: TokenVerifier): + When `resource_server_url` is given, only a token whose `AccessToken.resource` + (its RFC 8707 resource indicator / audience) is that URL is accepted. + """ + + def __init__(self, token_verifier: TokenVerifier, *, resource_server_url: AnyHttpUrl | None = None): self.token_verifier = token_verifier + self.resource_server_url = resource_server_url async def authenticate(self, conn: HTTPConnection): auth_header = next( @@ -63,8 +71,22 @@ class BearerAuthBackend(AuthenticationBackend): if auth_info.expires_at and auth_info.expires_at < int(time.time()): return None + if self.resource_server_url and not self._issued_for_this_resource(auth_info.resource): + logger.warning( + "Bearer token resource %r is not resource_server_url %s", auth_info.resource, self.resource_server_url + ) + return None + return AuthCredentials(auth_info.scopes), AuthenticatedUser(auth_info) + def _issued_for_this_resource(self, resource: str | None) -> bool: + """Compare as URLs (so case and default-port spelling do not matter), a trailing slash aside.""" + try: + token_resource = str(AnyHttpUrl(resource or "")) + except ValidationError: + return False + return token_resource.removesuffix("/") == str(self.resource_server_url).removesuffix("/") + class RequireAuthMiddleware: """Middleware that requires a valid Bearer token in the Authorization header. diff --git a/src/mcp/server/auth/provider.py b/src/mcp/server/auth/provider.py index 644868f3..bc8ba3e5 100644 --- a/src/mcp/server/auth/provider.py +++ b/src/mcp/server/auth/provider.py @@ -46,6 +46,7 @@ class RefreshToken(BaseModel): client_id: str scopes: list[str] expires_at: int | None = None + resource: str | None = None # RFC 8707 resource indicator; propagate to refreshed AccessTokens subject: str | None = None # resource owner; propagate to refreshed AccessTokens @@ -124,7 +125,15 @@ class TokenVerifier(Protocol): """Protocol for verifying bearer tokens.""" async def verify_token(self, token: str) -> AccessToken | None: - """Verify a bearer token and return access info if valid.""" + """Verify a bearer token and return access info if valid. + + Set `AccessToken.resource` to the resource the token was issued for (its RFC 8707 + resource indicator / `aud`; for a list, the entry equal to the server's + `AuthSettings.resource_server_url`). With `AuthSettings.validate_token_resource` the + bearer middleware then refuses any token whose resource is not `resource_server_url`; + without it, confirming the token was issued for this server (for example by passing the + expected audience to your JWT library) is up to the verifier. + """ # NOTE: MCPServer doesn't render any of these types in the user response, so it's diff --git a/src/mcp/server/auth/settings.py b/src/mcp/server/auth/settings.py index ae2083a3..4d0f7f13 100644 --- a/src/mcp/server/auth/settings.py +++ b/src/mcp/server/auth/settings.py @@ -1,4 +1,9 @@ -from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field +import warnings + +from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, model_validator +from typing_extensions import Self + +from mcp.shared.exceptions import MCPDeprecationWarning class ClientRegistrationOptions(BaseModel): @@ -40,3 +45,26 @@ class AuthSettings(BaseModel): description="The URL of the MCP server to be used as the resource identifier " "and base route to look up OAuth Protected Resource Metadata.", ) + validate_token_resource: bool | None = Field( + default=None, + description="Only accept tokens the token verifier reports as issued for `resource_server_url` " + "(`AccessToken.resource`, the RFC 8707 resource indicator). Enable it when your authorization " + "server binds tokens to the `resource` the client requested; set it to False when your token " + "verifier checks the token's audience itself. With `resource_server_url` set, leaving it unset warns " + "and behaves as False; 3.0 makes True the default there.", + ) + + @model_validator(mode="after") + def _check_validate_token_resource(self) -> Self: + if self.validate_token_resource and self.resource_server_url is None: + raise ValueError("validate_token_resource requires resource_server_url") + if self.validate_token_resource is None and self.resource_server_url is not None: + warnings.warn( + "`AuthSettings.validate_token_resource` is not set, so bearer tokens are not checked " + "against `resource_server_url`; it will default to True in 3.0 when `resource_server_url` is " + "set. Set it to True to have the server refuse tokens issued for another resource, or to " + "False if your TokenVerifier validates the token's audience itself.", + MCPDeprecationWarning, + stacklevel=3, + ) + return self diff --git a/src/mcp/server/elicitation.py b/src/mcp/server/elicitation.py index 26425c13..65037888 100644 --- a/src/mcp/server/elicitation.py +++ b/src/mcp/server/elicitation.py @@ -8,7 +8,7 @@ from mcp_types import RequestId # Internal surface package; imported as the gate's source of truth for spec-valid property schemas. from mcp_types._v2025_11_25 import PrimitiveSchemaDefinition -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, TypeAdapter, ValidationError from pydantic.json_schema import GenerateJsonSchema, JsonSchemaValue from pydantic_core import core_schema from typing_extensions import TypeAliasType @@ -16,6 +16,7 @@ from typing_extensions import TypeAliasType from mcp.server.session import ServerSession ElicitSchemaModelT = TypeVar("ElicitSchemaModelT", bound=BaseModel) +_PRIMITIVE_SCHEMA_ADAPTER = TypeAdapter[PrimitiveSchemaDefinition](PrimitiveSchemaDefinition) class AcceptedElicitation(BaseModel, Generic[ElicitSchemaModelT]): @@ -79,7 +80,7 @@ def _validate_rendered_properties(json_schema: dict[str, Any]) -> None: """ for field_name, prop in json_schema.get("properties", {}).items(): try: - PrimitiveSchemaDefinition.model_validate(prop) + _PRIMITIVE_SCHEMA_ADAPTER.validate_python(prop) except ValidationError: raise TypeError( f"Elicitation schema field {field_name!r} rendered as {prop!r}, " diff --git a/src/mcp/server/fastmcp.py b/src/mcp/server/fastmcp.py new file mode 100644 index 00000000..7dd4646d --- /dev/null +++ b/src/mcp/server/fastmcp.py @@ -0,0 +1,16 @@ +"""Removed in mcp 2: `FastMCP` is now `mcp.server.mcpserver.MCPServer`. + +This module has no API. Importing it, or anything below it, raises +`ModuleNotFoundError` with a message that points at the migration guide. It +exists only because the bare "No module named 'mcp.server.fastmcp'" gave v1 +code no hint that the installed SDK is a different major version. +""" + +_MESSAGE = ( + "No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer " + "(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at " + "https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver " + "or pin 'mcp<2' to keep running v1 code." +) + +raise ModuleNotFoundError(_MESSAGE, name=__name__) diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 4c327f4e..8a886dcc 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -65,7 +65,12 @@ from mcp.server.context import HandlerResult, ServerMiddleware, ServerRequestCon from mcp.server.models import InitializationOptions from mcp.server.runner import serve_dual_era_loop from mcp.server.streamable_http import EventStore -from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager +from mcp.server.streamable_http_manager import ( + DEFAULT_MAX_SESSIONS, + DEFAULT_SESSION_IDLE_TIMEOUT, + StreamableHTTPASGIApp, + StreamableHTTPSessionManager, +) from mcp.server.transport_security import DEFAULT_MAX_REQUEST_BODY_SIZE, TransportSecuritySettings from mcp.shared._stream_protocols import ReadStream, WriteStream from mcp.shared.exceptions import MCPDeprecationWarning @@ -722,6 +727,8 @@ class Server(Generic[LifespanResultT]): event_store: EventStore | None = None, retry_interval: int | None = None, max_request_body_size: int = DEFAULT_MAX_REQUEST_BODY_SIZE, + session_idle_timeout: float | None = DEFAULT_SESSION_IDLE_TIMEOUT, + max_sessions: int | None = DEFAULT_MAX_SESSIONS, transport_security: TransportSecuritySettings | None = None, host: str = "127.0.0.1", auth: AuthSettings | None = None, @@ -747,6 +754,8 @@ class Server(Generic[LifespanResultT]): stateless=stateless_http, security_settings=transport_security, max_request_body_size=max_request_body_size, + session_idle_timeout=session_idle_timeout, + max_sessions=max_sessions, ) self._session_manager = session_manager @@ -767,7 +776,10 @@ class Server(Generic[LifespanResultT]): middleware = [ Middleware( AuthenticationMiddleware, - backend=BearerAuthBackend(token_verifier), + backend=BearerAuthBackend( + token_verifier, + resource_server_url=auth.resource_server_url if auth.validate_token_resource else None, + ), ), Middleware(AuthContextMiddleware), ] diff --git a/src/mcp/server/mcpserver/server.py b/src/mcp/server/mcpserver/server.py index 93bef165..fbd2c26d 100644 --- a/src/mcp/server/mcpserver/server.py +++ b/src/mcp/server/mcpserver/server.py @@ -93,7 +93,11 @@ from mcp.server.request_state import RequestStateBoundary, RequestStateSecurity from mcp.server.sse import SseServerTransport from mcp.server.stdio import stdio_server from mcp.server.streamable_http import EventStore -from mcp.server.streamable_http_manager import StreamableHTTPSessionManager +from mcp.server.streamable_http_manager import ( + DEFAULT_MAX_SESSIONS, + DEFAULT_SESSION_IDLE_TIMEOUT, + StreamableHTTPSessionManager, +) from mcp.server.subscriptions import InMemorySubscriptionBus, ListenHandler, SubscriptionBus from mcp.server.transport_security import DEFAULT_MAX_REQUEST_BODY_SIZE, TransportSecuritySettings from mcp.shared.exceptions import MCPError @@ -388,6 +392,8 @@ class MCPServer(Generic[LifespanResultT]): event_store: EventStore | None = ..., retry_interval: int | None = ..., max_request_body_size: int = ..., + session_idle_timeout: float | None = ..., + max_sessions: int | None = ..., transport_security: TransportSecuritySettings | None = ..., ) -> None: ... @@ -1106,6 +1112,8 @@ class MCPServer(Generic[LifespanResultT]): event_store: EventStore | None = None, retry_interval: int | None = None, max_request_body_size: int = DEFAULT_MAX_REQUEST_BODY_SIZE, + session_idle_timeout: float | None = DEFAULT_SESSION_IDLE_TIMEOUT, + max_sessions: int | None = DEFAULT_MAX_SESSIONS, transport_security: TransportSecuritySettings | None = None, ) -> None: """Run the server using StreamableHTTP transport.""" @@ -1118,6 +1126,8 @@ class MCPServer(Generic[LifespanResultT]): event_store=event_store, retry_interval=retry_interval, max_request_body_size=max_request_body_size, + session_idle_timeout=session_idle_timeout, + max_sessions=max_sessions, transport_security=transport_security, host=host, ) @@ -1177,7 +1187,12 @@ class MCPServer(Generic[LifespanResultT]): # extract auth info from request (but do not require it) Middleware( AuthenticationMiddleware, - backend=BearerAuthBackend(self._token_verifier), + backend=BearerAuthBackend( + self._token_verifier, + resource_server_url=self.settings.auth.resource_server_url + if self.settings.auth.validate_token_resource + else None, + ), ), # Add the auth context middleware to store # authenticated user in a contextvar @@ -1270,6 +1285,8 @@ class MCPServer(Generic[LifespanResultT]): event_store: EventStore | None = None, retry_interval: int | None = None, max_request_body_size: int = DEFAULT_MAX_REQUEST_BODY_SIZE, + session_idle_timeout: float | None = DEFAULT_SESSION_IDLE_TIMEOUT, + max_sessions: int | None = DEFAULT_MAX_SESSIONS, transport_security: TransportSecuritySettings | None = None, host: str = "127.0.0.1", ) -> Starlette: @@ -1281,6 +1298,8 @@ class MCPServer(Generic[LifespanResultT]): event_store=event_store, retry_interval=retry_interval, max_request_body_size=max_request_body_size, + session_idle_timeout=session_idle_timeout, + max_sessions=max_sessions, transport_security=transport_security, host=host, auth=self.settings.auth, diff --git a/src/mcp/server/mcpserver/utilities/func_metadata.py b/src/mcp/server/mcpserver/utilities/func_metadata.py index cc324335..0ffac07c 100644 --- a/src/mcp/server/mcpserver/utilities/func_metadata.py +++ b/src/mcp/server/mcpserver/utilities/func_metadata.py @@ -114,15 +114,15 @@ class ArgModelBase(BaseModel): class FuncMetadata(BaseModel): """A tool function's argument model plus, for structured output, the published `output_schema` and the - `output_model` results are validated against. Constructing one with an `output_model` and no schema derives - the schema (and raises if pydantic can't); the fields are read live, so clearing or reassigning them later + `output_model` type annotation results are validated against. Constructing one with an `output_model` and no + schema derives the schema (and raises if pydantic can't); the fields are read live, so reassigning them later takes effect on the next call.""" arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)] output_schema: dict[str, Any] | None = None - output_model: Annotated[type[Any], WithJsonSchema(None)] | None = None + output_model: Annotated[Any, WithJsonSchema(None)] = None wrap_output: bool = False - _adapter: tuple[type[Any], TypeAdapter[Any]] | None = PrivateAttr(default=None) + _adapter: tuple[Any, TypeAdapter[Any]] | None = PrivateAttr(default=None) def model_post_init(self, context: Any, /) -> None: if self.output_model is not None and self.output_schema is None: @@ -130,7 +130,7 @@ class FuncMetadata(BaseModel): schema = self._output_adapter(self.output_model).json_schema(schema_generator=StrictJsonSchema) self.output_schema = _inline_root_ref(schema) - def _output_adapter(self, output_model: type[Any]) -> TypeAdapter[Any]: + def _output_adapter(self, output_model: Any) -> TypeAdapter[Any]: """The validator/serializer for `output_model`, built once and rebuilt only if the field is reassigned.""" if self._adapter is None or self._adapter[0] is not output_model: self._adapter = (output_model, TypeAdapter(_pydantic_readable_typeddict(output_model))) @@ -477,7 +477,7 @@ def func_metadata( return FuncMetadata(arg_model=arguments_model) -def _create_output_model(original_annotation: Any, type_expr: Any, func_name: str) -> tuple[type[Any] | None, bool]: +def _create_output_model(original_annotation: Any, type_expr: Any, func_name: str) -> tuple[Any, bool]: """Pick the type structured output is validated against for the given return annotation. Args: @@ -491,7 +491,7 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st Model is None if the type cannot carry structured output. wrap_output is True if the result needs to be wrapped in {"result": ...} """ - model: type[Any] | None = None + model: Any = None wrap_output = False # First handle special case: None @@ -503,13 +503,12 @@ def _create_output_model(original_annotation: Any, type_expr: Any, func_name: st elif isinstance(type_expr, GenericAlias): origin = get_origin(type_expr) - # Special case: dict with string keys can use RootModel if origin is dict: args = get_args(type_expr) if len(args) == 2 and args[0] is str: # TODO: should we use the original annotation? We are losing any potential `Annotated` # metadata for Pydantic here: - model = _create_dict_model(func_name, type_expr) + model = Annotated[type_expr, Field(title=f"{func_name}DictOutput")] else: # dict with non-str keys needs wrapping model = _create_wrapped_model(func_name, original_annotation) @@ -622,21 +621,6 @@ def _create_wrapped_model(func_name: str, annotation: Any) -> type[BaseModel]: return create_model(model_name, result=annotation) -def _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]: - """Create a RootModel for dict[str, T] types.""" - # TODO(Marcelo): We should not rely on RootModel for this. - from pydantic import RootModel # noqa: TID251 - - class DictModel(RootModel[dict_annotation]): - pass - - # Give it a meaningful name - DictModel.__name__ = f"{func_name}DictOutput" - DictModel.__qualname__ = f"{func_name}DictOutput" - - return DictModel - - def _convert_to_content(result: Any) -> list[ContentBlock]: """Convert a result to a sequence of content objects. diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index 1a4e9939..416dd9e2 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -7,6 +7,7 @@ responses, with streaming support for long-running operations. """ import logging +import math import re from abc import ABC, abstractmethod from collections.abc import AsyncGenerator, Awaitable, Callable @@ -167,6 +168,7 @@ class StreamableHTTPServerTransport: event_store: EventStore | None = None, security_settings: TransportSecuritySettings | None = None, retry_interval: int | None = None, + idle_timeout: float | None = None, ) -> None: """Initialize a new StreamableHTTP server transport. @@ -187,12 +189,22 @@ class StreamableHTTPServerTransport: retry field. When set, the server will send a retry field in SSE priming events to control client reconnection timing for polling behavior. Only used when event_store is provided. + idle_timeout: Seconds the session may go without any request in flight before + `idle_scope` is cancelled. A request being served or an open GET + stream holds the session open; the countdown starts each time the + last in-flight request completes. The host enters `idle_scope` + (available once `connect()` has been entered) around the session's + message loop to end the session when it fires. Default is None: no + `idle_scope`, the session never expires. Raises: - ValueError: If the session ID contains invalid characters. + ValueError: If the session ID contains invalid characters, or if `idle_timeout` + is not a positive, finite number. """ if mcp_session_id is not None and not SESSION_ID_PATTERN.fullmatch(mcp_session_id): raise ValueError("Session ID must only contain visible ASCII characters (0x21-0x7E)") + if idle_timeout is not None and not (math.isfinite(idle_timeout) and idle_timeout > 0): + raise ValueError("idle_timeout must be a positive, finite number of seconds") self.mcp_session_id = mcp_session_id self.is_json_response_enabled = is_json_response_enabled @@ -208,8 +220,11 @@ class StreamableHTTPServerTransport: ] = {} self._sse_stream_writers: dict[RequestId, MemoryObjectSendStream[SSEEvent]] = {} self._terminated = False - # Idle timeout cancel scope; managed by the session manager. + self._idle_timeout = idle_timeout + self._requests_in_flight = 0 self.idle_scope: anyio.CancelScope | None = None + """Created when `connect()` is entered if `idle_timeout` is set; cancelled once no request has been in + flight for `idle_timeout` seconds.""" @property def is_terminated(self) -> bool: @@ -458,6 +473,32 @@ class StreamableHTTPServerTransport: async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None: """Application entry point that handles all HTTP requests.""" + if self.idle_scope is None or self._idle_timeout is None: + await self._handle_request(scope, receive, send) + return + + if self.idle_scope.cancel_called: + # The idle period already ran out and the host is ending this + # session: answer as terminated rather than dispatch into a + # message loop that is going away. + if not self._terminated: + await self.terminate() + await self._handle_request(scope, receive, send) + return + + # A request in flight (an open GET stream included) holds the session: + # the idle countdown is suspended while any is being served and + # restarts when the last one completes. + self._requests_in_flight += 1 + self.idle_scope.deadline = math.inf + try: + await self._handle_request(scope, receive, send) + finally: + self._requests_in_flight -= 1 + if not self._requests_in_flight: + self.idle_scope.deadline = anyio.current_time() + self._idle_timeout + + async def _handle_request(self, scope: Scope, receive: Receive, send: Send) -> None: request = Request(scope, receive) # Validate request headers for DNS rebinding protection @@ -793,7 +834,7 @@ class StreamableHTTPServerTransport: await response(request.scope, request.receive, send) return - if not await self._validate_request_headers(request, send): # pragma: no cover + if not await self._validate_request_headers(request, send): return await self.terminate() @@ -995,6 +1036,8 @@ class StreamableHTTPServerTransport: Yields: Tuple of (read_stream, write_stream) for bidirectional communication """ + if self._idle_timeout is not None: + self.idle_scope = anyio.CancelScope() # Create the memory streams for this connection diff --git a/src/mcp/server/streamable_http_manager.py b/src/mcp/server/streamable_http_manager.py index e9a7d962..a7efac3f 100644 --- a/src/mcp/server/streamable_http_manager.py +++ b/src/mcp/server/streamable_http_manager.py @@ -4,17 +4,18 @@ from __future__ import annotations import contextlib import logging +import math from collections.abc import AsyncIterator -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Final from uuid import uuid4 import anyio from anyio.abc import TaskStatus -from mcp_types import DEFAULT_NEGOTIATED_VERSION, INVALID_REQUEST, ErrorData, JSONRPCError +from mcp_types import DEFAULT_NEGOTIATED_VERSION, INTERNAL_ERROR, INVALID_REQUEST, ErrorData, JSONRPCError from mcp_types.version import HANDSHAKE_PROTOCOL_VERSIONS from starlette.requests import Request from starlette.responses import Response -from starlette.types import Receive, Scope, Send +from starlette.types import ASGIApp, Message, Receive, Scope, Send from mcp.server._streamable_http_modern import handle_modern_request from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser, AuthorizationContext, authorization_context @@ -34,6 +35,12 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +DEFAULT_SESSION_IDLE_TIMEOUT: Final = 30 * 60 +"""Default idle period in seconds after which a stateful Streamable HTTP session is closed (30 minutes).""" + +DEFAULT_MAX_SESSIONS: Final = 10_000 +"""Default maximum number of concurrent stateful Streamable HTTP sessions per session manager.""" + class StreamableHTTPSessionManager: """Manages StreamableHTTP sessions with optional resumability via event store. @@ -45,7 +52,7 @@ class StreamableHTTPSessionManager: 2. Resumability via an optional event store 3. Connection management and lifecycle 4. Request handling and transport setup - 5. Idle session cleanup via optional timeout + 5. Idle session cleanup Important: Only one StreamableHTTPSessionManager instance should be created per application. The instance cannot be reused after its run() context has @@ -62,13 +69,18 @@ class StreamableHTTPSessionManager: security_settings: Optional transport security settings. retry_interval: Retry interval in milliseconds to suggest to clients in SSE retry field. Used for SSE polling behavior. - session_idle_timeout: Optional idle timeout in seconds for stateful sessions. If set, sessions that - receive no HTTP requests for this duration will be automatically terminated and removed. When - retry_interval is also configured, ensure the idle timeout comfortably exceeds the retry interval to - avoid reaping sessions during normal SSE polling gaps. Default is None (no timeout). A value of 1800 - (30 minutes) is recommended for most deployments. + session_idle_timeout: Idle timeout in seconds for stateful sessions. A session that has had no HTTP + request in flight for this long (no request being served, no open GET stream) is terminated and + removed; its ID then answers 404 and the client has to initialize a new session. When retry_interval + is also configured, ensure the idle timeout comfortably exceeds the retry interval to avoid reaping + sessions during normal SSE polling gaps. Defaults to 1800 (30 minutes); None disables the timeout so + sessions live until the client deletes them or the manager shuts down. Unused in stateless mode. max_request_body_size: Maximum size in bytes for Streamable HTTP request bodies. Requests that exceed this limit receive a 413 response before parsing or session creation. Defaults to 4 MiB. + max_sessions: Maximum number of concurrent stateful sessions. While that many sessions are open, a + request that would open another one receives a 503 response; existing sessions are unaffected and + room frees up as they end or expire. Defaults to 10 000; None removes the limit. Unused in stateless + mode. """ def __init__( @@ -79,15 +91,16 @@ class StreamableHTTPSessionManager: stateless: bool = False, security_settings: TransportSecuritySettings | None = None, retry_interval: int | None = None, - session_idle_timeout: float | None = None, + session_idle_timeout: float | None = DEFAULT_SESSION_IDLE_TIMEOUT, max_request_body_size: int = DEFAULT_MAX_REQUEST_BODY_SIZE, + max_sessions: int | None = DEFAULT_MAX_SESSIONS, ): - if session_idle_timeout is not None and session_idle_timeout <= 0: - raise ValueError("session_idle_timeout must be a positive number of seconds") - if stateless and session_idle_timeout is not None: - raise RuntimeError("session_idle_timeout is not supported in stateless mode") + if session_idle_timeout is not None and not (math.isfinite(session_idle_timeout) and session_idle_timeout > 0): + raise ValueError("session_idle_timeout must be a positive, finite number of seconds") if max_request_body_size <= 0: raise ValueError("max_request_body_size must be a positive number of bytes") + if max_sessions is not None and max_sessions <= 0: + raise ValueError("max_sessions must be a positive number of sessions or None") self.app = app self.event_store = event_store @@ -97,6 +110,7 @@ class StreamableHTTPSessionManager: self.retry_interval = retry_interval self.session_idle_timeout = session_idle_timeout self.max_request_body_size = max_request_body_size + self.max_sessions = max_sessions self.asgi_app = RequestBodyLimitMiddleware(self._handle_request, max_request_body_size) # Session tracking (only used if not stateless) @@ -234,16 +248,15 @@ class StreamableHTTPSessionManager: except Exception: # pragma: lax no cover logger.exception("Stateless session crashed") - # Assert task group is not None for type checking + # The per-request server task only ends once the transport is + # terminated, so terminate it even if the request was cancelled. assert self._task_group is not None - # Start the server task - await self._task_group.start(run_stateless_server) - - # Handle the HTTP request and return the response - await http_transport.handle_request(scope, receive, send) - - # Terminate the transport after the request is handled - await http_transport.terminate() + try: + await self._task_group.start(run_stateless_server) + await http_transport.handle_request(scope, receive, send) + finally: + with anyio.CancelScope(shield=True): + await http_transport.terminate() async def _handle_stateful_request(self, scope: Scope, receive: Receive, send: Send) -> None: """Process request in stateful mode - maintaining session state between requests.""" @@ -263,109 +276,145 @@ class StreamableHTTPSessionManager: "Rejecting request for session %s: credential does not match the one that created the session", request_mcp_session_id[:64], ) - body = JSONRPCError( - jsonrpc="2.0", id=None, error=ErrorData(code=INVALID_REQUEST, message="Session not found") - ) - response = Response( - body.model_dump_json(by_alias=True, exclude_unset=True), - status_code=404, - media_type="application/json", - ) - await response(scope, receive, send) + await _error_response("Session not found", 404)(scope, receive, send) return logger.debug("Session already exists, handling request directly") - # Push back idle deadline on activity - if transport.idle_scope is not None and self.session_idle_timeout is not None: - transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout # pragma: no cover await transport.handle_request(scope, receive, send) + if transport.is_terminated: + # The client ended the session (DELETE): forget it now rather + # than when its server task winds down. + await self._discard_session(request_mcp_session_id, transport) return if request_mcp_session_id is None: - # New session case - logger.debug("Creating new transport") + # New session case. Admission (the session limit and registration) + # is decided under the lock; the request itself is served outside + # it, so one client that is slow to send its opening request does + # not hold up the others. async with self._session_creation_lock: - new_session_id = uuid4().hex - http_transport = StreamableHTTPServerTransport( - mcp_session_id=new_session_id, - is_json_response_enabled=self.json_response, - event_store=self.event_store, # May be None (no resumability) - security_settings=self.security_settings, - retry_interval=self.retry_interval, - ) - - assert http_transport.mcp_session_id is not None - if requestor is not None: - self._session_owners[http_transport.mcp_session_id] = requestor - self._server_instances[http_transport.mcp_session_id] = http_transport - logger.info(f"Created new transport with session ID: {new_session_id}") - - # Define the server runner - async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None: - async with http_transport.connect() as streams: - read_stream, write_stream = streams - task_status.started() - try: - # Use a cancel scope for idle timeout — when the - # deadline passes the scope cancels the loop and - # execution continues after the ``with`` block. - # Incoming requests push the deadline forward. - idle_scope = anyio.CancelScope() - if self.session_idle_timeout is not None: - idle_scope.deadline = anyio.current_time() + self.session_idle_timeout - http_transport.idle_scope = idle_scope - - with idle_scope: - # Drive via `serve_loop` (not `Server.run()`) so the - # manager's already-entered lifespan is reused - # rather than re-entered per session. - await serve_loop( - self.app, - read_stream, - write_stream, - lifespan_state=self._lifespan_state, - session_id=http_transport.mcp_session_id, - ) - - if idle_scope.cancelled_caught: - assert http_transport.mcp_session_id is not None - logger.info(f"Session {http_transport.mcp_session_id} idle timeout") - self._server_instances.pop(http_transport.mcp_session_id, None) - self._session_owners.pop(http_transport.mcp_session_id, None) - await http_transport.terminate() - except Exception: - logger.exception(f"Session {http_transport.mcp_session_id} crashed") - finally: - if ( # pragma: no branch - http_transport.mcp_session_id - and http_transport.mcp_session_id in self._server_instances - and not http_transport.is_terminated - ): - logger.info( - "Cleaning up crashed session " - f"{http_transport.mcp_session_id} from active instances." - ) - del self._server_instances[http_transport.mcp_session_id] - self._session_owners.pop(http_transport.mcp_session_id, None) - - # Assert task group is not None for type checking - assert self._task_group is not None - # Start the server task - await self._task_group.start(run_server) - - # Handle the HTTP request and return the response - await http_transport.handle_request(scope, receive, send) + http_transport = self._admit_session(requestor) + if http_transport is None: + logger.warning("Refusing to open a new session: %d sessions are already open", self.max_sessions) + await _error_response("Too many open sessions", 503, INTERNAL_ERROR)(scope, receive, send) + return + await self._serve_opening_request(http_transport, scope, receive, send) else: # Unknown or expired session ID - return 404 per MCP spec # TODO(L62): Align error code once spec clarifies # See: https://github.com/modelcontextprotocol/python-sdk/issues/1821 logger.info(f"Rejected request with unknown or expired session ID: {request_mcp_session_id[:64]}") - body = JSONRPCError( - jsonrpc="2.0", id=None, error=ErrorData(code=INVALID_REQUEST, message="Session not found") - ) - response = Response( - body.model_dump_json(by_alias=True, exclude_unset=True), status_code=404, media_type="application/json" - ) - await response(scope, receive, send) + await _error_response("Session not found", 404)(scope, receive, send) + + def _admit_session(self, requestor: AuthorizationContext | None) -> StreamableHTTPServerTransport | None: + """Register a new session for `requestor` and return its transport, or None at the session limit.""" + if self.max_sessions is not None and len(self._server_instances) >= self.max_sessions: + return None + http_transport = StreamableHTTPServerTransport( + mcp_session_id=uuid4().hex, + is_json_response_enabled=self.json_response, + event_store=self.event_store, # May be None (no resumability) + security_settings=self.security_settings, + retry_interval=self.retry_interval, + idle_timeout=self.session_idle_timeout, + ) + session_id = http_transport.mcp_session_id + assert session_id is not None + if requestor is not None: + self._session_owners[session_id] = requestor + self._server_instances[session_id] = http_transport + logger.info(f"Created new transport with session ID: {session_id}") + return http_transport + + async def _serve_opening_request( + self, http_transport: StreamableHTTPServerTransport, scope: Scope, receive: Receive, send: Send + ) -> None: + """Start the session's server task and let its transport answer the request that opens it. + + Without a session ID only an initialize request can succeed, so if this + one is refused, fails or is cancelled (or the session's server task + cannot even be started) nothing was established: the session is + discarded again rather than kept (with its server task) around. + """ + session_id = http_transport.mcp_session_id + assert session_id is not None + + async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None: + async with http_transport.connect() as streams: + read_stream, write_stream = streams + task_status.started() + try: + # The transport cancels its idle scope once no request + # has been in flight for `session_idle_timeout`; that + # ends the loop and execution continues after the + # `with` block. Without a timeout there is nothing to fire. + idle_scope = http_transport.idle_scope + if idle_scope is None: + idle_scope = anyio.CancelScope() + with idle_scope: + # Drive via `serve_loop` (not `Server.run()`) so the + # manager's already-entered lifespan is reused + # rather than re-entered per session. + await serve_loop( + self.app, + read_stream, + write_stream, + lifespan_state=self._lifespan_state, + session_id=session_id, + ) + + if idle_scope.cancelled_caught: + logger.info(f"Session {session_id} idle timeout") + except Exception: + logger.exception(f"Session {session_id} crashed") + finally: + # However the session ended (client DELETE, idle + # timeout, crash), discard it. + await self._discard_session(session_id, http_transport) + + established = False + try: + assert self._task_group is not None + await self._task_group.start(run_server) + status = await _send_and_report_status(http_transport.handle_request, scope, receive, send) + established = status is not None and status < 400 + finally: + if not established: # pragma: no branch + await self._discard_session(session_id, http_transport) + + async def _discard_session(self, session_id: str, transport: StreamableHTTPServerTransport) -> None: + """Stop tracking the session and make sure its transport refuses anything that still reaches it. + + The session is forgotten first, before any await, so its ID answers 404 + from the moment this is called; terminating the transport is shielded so + it completes even while the caller is being cancelled. + """ + self._server_instances.pop(session_id, None) + self._session_owners.pop(session_id, None) + if not transport.is_terminated: + with anyio.CancelScope(shield=True): + await transport.terminate() + + +def _error_response(message: str, status_code: int, code: int = INVALID_REQUEST) -> Response: + """A JSON-RPC error body (no request id) with the given HTTP status.""" + body = JSONRPCError(jsonrpc="2.0", id=None, error=ErrorData(code=code, message=message)) + return Response( + body.model_dump_json(by_alias=True, exclude_unset=True), status_code=status_code, media_type="application/json" + ) + + +async def _send_and_report_status(app: ASGIApp, scope: Scope, receive: Receive, send: Send) -> int | None: + """Run `app` for one request and return the HTTP status it answered with (None if it sent no response).""" + status: int | None = None + + async def watch_status(message: Message) -> None: + nonlocal status + if message["type"] == "http.response.start": + status = message["status"] + await send(message) + + await app(scope, receive, watch_status) + return status class StreamableHTTPASGIApp: diff --git a/src/mcp/shared/_httpx_utils.py b/src/mcp/shared/_httpx_utils.py index 6bb63888..940b9f08 100644 --- a/src/mcp/shared/_httpx_utils.py +++ b/src/mcp/shared/_httpx_utils.py @@ -1,5 +1,8 @@ -"""Utilities for creating standardized httpx2 AsyncClient instances.""" +"""Utilities for creating and using httpx2 AsyncClient instances in the MCP transports.""" +from abc import ABC, abstractmethod +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager from typing import Any, Protocol import httpx2 @@ -10,6 +13,12 @@ __all__ = ["create_mcp_http_client", "MCP_DEFAULT_TIMEOUT", "MCP_DEFAULT_SSE_REA MCP_DEFAULT_TIMEOUT = 30.0 # General operations (seconds) MCP_DEFAULT_SSE_READ_TIMEOUT = 300.0 # SSE streams - 5 minutes (seconds) +# The headers httpx2.AsyncClient.sse() adds to an event-stream request. +_SSE_HEADERS = {"Accept": "text/event-stream", "Cache-Control": "no-store"} + +# How many redirects one auth-flow request may follow within its origin (see RedirectAwareAuth). +_AUTH_REDIRECT_LIMIT = 5 + class McpHttpClientFactory(Protocol): # pragma: no branch def __call__( # pragma: no branch @@ -25,9 +34,12 @@ def create_mcp_http_client( timeout: httpx2.Timeout | None = None, auth: httpx2.Auth | None = None, ) -> httpx2.AsyncClient: - """Create a standardized httpx2 AsyncClient with MCP defaults. + """Create an httpx2 AsyncClient with the MCP transports' default timeouts. - Always enables follow_redirects and applies an SSE-friendly default timeout. + The client uses a 30-second timeout for connect/write/pool and a 300-second + read timeout, because a server may hold a response stream open. Redirect + following is left at the httpx2 default (off): the MCP transports follow + redirects within the endpoint's origin themselves, see `stream_within_origin`. Args: headers: Optional headers to include with all requests. @@ -36,60 +48,171 @@ def create_mcp_http_client( auth: Optional authentication handler. Returns: - Configured httpx2.AsyncClient instance with MCP defaults. + Configured httpx2.AsyncClient instance. Note: The returned AsyncClient must be used as a context manager to ensure proper cleanup of connections. - - Example: - Basic usage with MCP defaults: - - ```python - async with create_mcp_http_client() as client: - response = await client.get("https://api.example.com") - ``` - - With custom headers: - - ```python - headers = {"Authorization": "Bearer token"} - async with create_mcp_http_client(headers) as client: - response = await client.get("/endpoint") - ``` - - With both custom headers and timeout: - - ```python - timeout = httpx2.Timeout(60.0, read=300.0) - async with create_mcp_http_client(headers, timeout) as client: - response = await client.get("/long-request") - ``` - - With authentication: - - ```python - from httpx2 import BasicAuth - auth = BasicAuth(username="user", password="pass") - async with create_mcp_http_client(headers, timeout, auth) as client: - response = await client.get("/protected-endpoint") - ``` """ - # Set MCP defaults - kwargs: dict[str, Any] = {"follow_redirects": True} - - # Handle timeout if timeout is None: - kwargs["timeout"] = httpx2.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT) - else: - kwargs["timeout"] = timeout - - # Handle headers + timeout = httpx2.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT) + kwargs: dict[str, Any] = {"timeout": timeout} if headers is not None: kwargs["headers"] = headers - - # Handle authentication if auth is not None: # pragma: no cover kwargs["auth"] = auth - return httpx2.AsyncClient(**kwargs) + + +def _within_origin(url: httpx2.URL, location: httpx2.URL) -> bool: + """Whether `location` is on `url`'s origin, or is its https upgrade on the default ports. + + httpx2 normalises a scheme's default port to None and lower-cases hosts, so + plain tuple comparison is exact. The upgrade rule is the one httpx2 itself + uses to decide a redirect has not left the origin (`_is_https_redirect`). + """ + if (url.scheme, url.host, url.port) == (location.scheme, location.host, location.port): + return True + return ( + url.host == location.host + and url.scheme == "http" + and url.port is None + and location.scheme == "https" + and location.port is None + ) + + +def next_request_within_origin(response: httpx2.Response) -> httpx2.Request | None: + """The request that follows `response`'s redirect, if it is one the MCP transports follow. + + That is when httpx2 built a next request for it (a redirect status with a + Location), the next request keeps the method (307/308, or any redirect of a + GET: httpx2 turns a POST into a body-less GET for 301/302/303, which would + drop the message), its URL stays within the origin of the request just sent + (same scheme, host and port, or http to https on the same host with default + ports), and the Location does not bring userinfo of its own (which httpx2 + would otherwise send as Basic auth; userinfo the configured URL already had + is kept by a relative Location and is fine). None for anything else, + including a non-redirect. + """ + next_request = response.next_request + if next_request is None: + return None + sent = response.request + if ( + next_request.method != sent.method + or (next_request.url.userinfo and next_request.url.userinfo != sent.url.userinfo) + or not _within_origin(sent.url, next_request.url) + ): + return None + return next_request + + +@asynccontextmanager +async def stream_within_origin( + client: httpx2.AsyncClient, method: str, url: httpx2.URL | str, **kwargs: Any +) -> AsyncGenerator[httpx2.Response]: + """`client.stream(...)`, following redirects only while they stay within the request's origin. + + An MCP transport talks to one configured endpoint, and everything on a request + (headers, auth, body) was configured for that endpoint. A redirect that + `next_request_within_origin` accepts, such as a 307/308 trailing-slash + normalisation, is followed, at most `client.max_redirects` times. Any other + redirect (or one past that budget) is not followed: the redirect response + itself is yielded, the way httpx2 hands one back when `follow_redirects` is + off, and the caller treats it as the non-success it is. The client's own + `follow_redirects` setting is not consulted. Requests an `httpx2.Auth` flow + makes during the call are sent without following either; the SDK's OAuth + providers apply the same rule to their own requests. + """ + request = client.build_request(method, url, **kwargs) + followed = 0 + while True: + response = await client.send(request, stream=True, follow_redirects=False) + next_request = next_request_within_origin(response) + if next_request is None or followed == client.max_redirects: + break + try: + # Drain the redirect body so the connection returns to the pool, as httpx2 does when it follows. + await response.aread() + finally: + await response.aclose() + request = next_request + followed += 1 + try: + yield response + finally: + await response.aclose() + + +async def request_within_origin( + client: httpx2.AsyncClient, method: str, url: httpx2.URL | str, **kwargs: Any +) -> httpx2.Response: + """`client.request(...)` with the redirect handling of `stream_within_origin`.""" + async with stream_within_origin(client, method, url, **kwargs) as response: + await response.aread() + return response + + +@asynccontextmanager +async def sse_within_origin( + client: httpx2.AsyncClient, url: httpx2.URL | str, *, headers: dict[str, str] | None = None +) -> AsyncGenerator[httpx2.EventSource]: + """`client.sse(url)` with the redirect handling of `stream_within_origin`.""" + merged = httpx2.Headers(_SSE_HEADERS) + merged.update(headers or {}) + async with stream_within_origin(client, "GET", url, headers=merged) as response: + yield httpx2.EventSource(response) + + +def redirect_location(response: httpx2.Response) -> httpx2.URL | None: + """Where `response` redirects to, for use in a message: without userinfo, query or fragment, + which can carry state that does not belong in an error or a log line. None if not a redirect.""" + if response.next_request is None: + return None + return response.next_request.url.copy_with(userinfo=b"", query=None, fragment=None) + + +def redirect_note(response: httpx2.Response) -> str: + """A suffix naming the location of a redirect response that was not followed, else empty.""" + location = redirect_location(response) + if location is None: + return "" + return f" (redirected to {location}; not followed)" + + +class RedirectAwareAuth(ABC, httpx2.Auth): + """An `httpx2.Auth` whose own requests follow redirects the way MCP transport requests do. + + The transports send every request with redirect following off and follow a + redirect themselves only within the endpoint's origin (`stream_within_origin`). + httpx2 applies that per-request setting to the requests an auth flow makes + too (metadata discovery, registration, token), so on their own those would + follow nothing. Subclasses write their flow as `_auth_flow`; this class + drives it and, for each request the flow makes other than the one being + authenticated, follows a redirect that `next_request_within_origin` accepts, + up to `_AUTH_REDIRECT_LIMIT` times. Any other redirect response is handed + to the flow as it is. + """ + + @abstractmethod + def _auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: + """The subclass's flow, written as `httpx2.Auth.async_auth_flow` otherwise would be.""" + + async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: + flow = self._auth_flow(request) + try: + outgoing = await flow.__anext__() + while True: + response = yield outgoing + if outgoing is not request: + for _ in range(_AUTH_REDIRECT_LIMIT): + follow = next_request_within_origin(response) + if follow is None: + break + response = yield follow + outgoing = await flow.asend(response) + except StopAsyncIteration: + return + finally: + await flow.aclose() diff --git a/tests/client/auth/extensions/test_client_credentials.py b/tests/client/auth/extensions/test_client_credentials.py index 16336f80..3b320c59 100644 --- a/tests/client/auth/extensions/test_client_credentials.py +++ b/tests/client/auth/extensions/test_client_credentials.py @@ -1,9 +1,14 @@ import urllib.parse +from collections.abc import AsyncGenerator +import httpx2 import jwt import pytest +from inline_snapshot import snapshot from pydantic import AnyHttpUrl +from mcp import MCPDeprecationWarning +from mcp.client.auth import OAuthClientProvider, OAuthFlowError from mcp.client.auth.extensions.client_credentials import ( ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, @@ -27,7 +32,7 @@ class MockTokenStorage: async def get_tokens(self) -> OAuthToken | None: return self._tokens - async def set_tokens(self, tokens: OAuthToken) -> None: # pragma: no cover + async def set_tokens(self, tokens: OAuthToken) -> None: self._tokens = tokens async def get_client_info(self) -> OAuthClientInformationFull | None: # pragma: no cover @@ -53,6 +58,7 @@ class TestClientCredentialsOAuthProvider: storage=mock_storage, client_id="test-client-id", client_secret="test-client-secret", + issuer="https://api.example.com", ) # client_info is set during _initialize @@ -73,6 +79,7 @@ class TestClientCredentialsOAuthProvider: client_id="test-client-id", client_secret="test-client-secret", scope="read write", + issuer="https://api.example.com", ) await provider._initialize() @@ -88,6 +95,7 @@ class TestClientCredentialsOAuthProvider: client_id="test-client-id", client_secret="test-client-secret", token_endpoint_auth_method="client_secret_post", + issuer="https://api.example.com", ) await provider._initialize() @@ -103,6 +111,7 @@ class TestClientCredentialsOAuthProvider: client_id="test-client-id", client_secret="test-client-secret", scope="read write", + issuer="https://api.example.com", ) provider.context.oauth_metadata = OAuthMetadata( issuer=AnyHttpUrl("https://api.example.com"), @@ -131,6 +140,7 @@ class TestClientCredentialsOAuthProvider: client_secret="test-client-secret", token_endpoint_auth_method="client_secret_post", scope="read write", + issuer="https://api.example.com", ) await provider._initialize() provider.context.oauth_metadata = OAuthMetadata( @@ -157,6 +167,7 @@ class TestClientCredentialsOAuthProvider: storage=mock_storage, client_id="test-client-id", client_secret="test-client-secret", + issuer="https://api.example.com", ) provider.context.oauth_metadata = OAuthMetadata( issuer=AnyHttpUrl("https://api.example.com"), @@ -188,6 +199,7 @@ class TestPrivateKeyJWTOAuthProvider: storage=mock_storage, client_id="test-client-id", assertion_provider=mock_assertion_provider, + issuer="https://api.example.com", ) # client_info is set during _initialize @@ -211,6 +223,7 @@ class TestPrivateKeyJWTOAuthProvider: client_id="test-client-id", assertion_provider=mock_assertion_provider, scope="read write", + issuer="https://auth.example.com", ) provider.context.oauth_metadata = OAuthMetadata( issuer=AnyHttpUrl("https://auth.example.com"), @@ -242,6 +255,7 @@ class TestPrivateKeyJWTOAuthProvider: storage=mock_storage, client_id="test-client-id", assertion_provider=mock_assertion_provider, + issuer="https://auth.example.com", ) provider.context.oauth_metadata = OAuthMetadata( issuer=AnyHttpUrl("https://auth.example.com"), @@ -325,3 +339,265 @@ class TestStaticAssertionProvider: assert result1 == token assert result2 == token + + +_SERVER_URL = "https://api.example.com/v1/mcp" +_CONFIGURED_ISSUER = "https://auth.example.com" + + +def _metadata_for(issuer: str) -> dict[str, str]: + return {"issuer": issuer, "authorization_endpoint": f"{issuer}/authorize", "token_endpoint": f"{issuer}/token"} + + +def _provider_with_issuer(kind: str, storage: MockTokenStorage, audiences: list[str]) -> OAuthClientProvider: + """A ClientCredentials ("secret") or PrivateKeyJWT ("jwt") provider configured for _CONFIGURED_ISSUER; + `audiences` records every audience an assertion is minted for.""" + if kind == "secret": + return ClientCredentialsOAuthProvider( + server_url=_SERVER_URL, storage=storage, client_id="cid", client_secret="csecret", issuer=_CONFIGURED_ISSUER + ) + + async def assertion_provider(audience: str) -> str: + audiences.append(audience) + return "signed-assertion" + + return PrivateKeyJWTOAuthProvider( + server_url=_SERVER_URL, + storage=storage, + client_id="cid", + assertion_provider=assertion_provider, + issuer=_CONFIGURED_ISSUER, + ) + + +async def _answer_discovery( + flow: AsyncGenerator[httpx2.Request, httpx2.Response], + *, + authorization_server: str | list[str] | None, + metadata: dict[str, str] | None, +) -> httpx2.Request: + """Answer the provider's first request with a 401 and its discovery requests as described; + return the request it builds once discovery is over. + + `authorization_server` is what protected-resource metadata advertises (None: no PRM is + served); `metadata` is the authorization server metadata document (None: every well-known + 404s). + """ + request = await flow.__anext__() + request = await flow.asend(httpx2.Response(401, request=request)) + while "/.well-known/oauth-protected-resource" in str(request.url): + if authorization_server is None: + response = httpx2.Response(404, request=request) + else: + advertised = authorization_server if isinstance(authorization_server, list) else [authorization_server] + prm = {"resource": _SERVER_URL, "authorization_servers": advertised} + response = httpx2.Response(200, json=prm, request=request) + request = await flow.asend(response) + while "/.well-known/" in str(request.url): + if metadata is None: + response = httpx2.Response(404, request=request) + else: + response = httpx2.Response(200, json=metadata, request=request) + request = await flow.asend(response) + return request + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "served_issuer", [_CONFIGURED_ISSUER, f"{_CONFIGURED_ISSUER}/"], ids=["as-configured", "root-slash"] +) +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_provider_with_configured_issuer_exchanges_at_that_issuer( + mock_storage: MockTokenStorage, kind: str, served_issuer: str +): + """SDK-defined: with `issuer=` set and metadata discovered for that issuer (a root issuer served with + its trailing slash is the same server), the token request goes to its token endpoint (positive + control for the refusals below).""" + audiences: list[str] = [] + provider = _provider_with_issuer(kind, mock_storage, audiences) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + metadata = {**_metadata_for(_CONFIGURED_ISSUER), "issuer": served_issuer} + + token_request = await _answer_discovery(flow, authorization_server=served_issuer, metadata=metadata) + + assert (token_request.method, str(token_request.url)) == ("POST", "https://auth.example.com/token") + assert audiences == ([] if kind == "secret" else [served_issuer]) + await flow.aclose() + + +@pytest.mark.anyio +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_provider_picks_its_configured_issuer_among_several_advertised_servers( + mock_storage: MockTokenStorage, kind: str +): + """SDK-defined: when the resource lists several authorization servers, the one matching `issuer=` is + discovered and used even if it is not listed first.""" + provider = _provider_with_issuer(kind, mock_storage, []) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + + token_request = await _answer_discovery( + flow, + authorization_server=["https://other-as.example.com", _CONFIGURED_ISSUER], + metadata=_metadata_for(_CONFIGURED_ISSUER), + ) + + assert provider.context.auth_server_url == _CONFIGURED_ISSUER + assert str(token_request.url) == "https://auth.example.com/token" + await flow.aclose() + + +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +def test_constructing_without_issuer_is_deprecated(mock_storage: MockTokenStorage, kind: str) -> None: + """SDK-defined: leaving `issuer` out is allowed, and the provider says at construction that + token requests will follow whichever authorization server the MCP server advertises.""" + + async def assertion_provider(audience: str) -> str: + raise NotImplementedError + + with pytest.warns(MCPDeprecationWarning) as recorded: + if kind == "secret": + ClientCredentialsOAuthProvider( + server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s" + ) + else: + PrivateKeyJWTOAuthProvider( + server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider + ) + + [warning] = recorded + assert warning.filename == __file__ + assert str(warning.message) == ( + "Omitting `issuer` is deprecated and it will be required in 3.0. Without it, the MCP server " + "decides which authorization server receives this client's credentials; pass " + "issuer= so they are only ever sent there." + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_without_issuer_the_exchange_follows_whichever_server_was_discovered( + mock_storage: MockTokenStorage, kind: str +) -> None: + """SDK-defined: with no `issuer` configured the token request is built from whatever metadata + discovery produced, as before.""" + + async def assertion_provider(audience: str) -> str: + return "jwt" + + with pytest.warns(MCPDeprecationWarning, match="Omitting `issuer` is deprecated"): + if kind == "secret": + provider: OAuthClientProvider = ClientCredentialsOAuthProvider( + server_url=_SERVER_URL, storage=mock_storage, client_id="c", client_secret="s" + ) + else: + provider = PrivateKeyJWTOAuthProvider( + server_url=_SERVER_URL, storage=mock_storage, client_id="c", assertion_provider=assertion_provider + ) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + + token_request = await _answer_discovery( + flow, + authorization_server="https://elsewhere.example.com", + metadata=_metadata_for("https://elsewhere.example.com"), + ) + + assert (token_request.method, str(token_request.url)) == ("POST", "https://elsewhere.example.com/token") + await flow.aclose() + + +def test_an_issuer_that_is_not_an_http_url_is_rejected_at_construction(mock_storage: MockTokenStorage) -> None: + """SDK-defined: `issuer=` is the authorization server's issuer URL; anything else is a configuration + error on both machine-to-machine providers.""" + with pytest.raises(ValueError) as cc_error: + ClientCredentialsOAuthProvider( + server_url=_SERVER_URL, storage=mock_storage, client_id="cid", client_secret="s", issuer="auth.example.com" + ) + with pytest.raises(ValueError) as jwt_error: + PrivateKeyJWTOAuthProvider( + server_url=_SERVER_URL, + storage=mock_storage, + client_id="cid", + assertion_provider=static_assertion_provider("jwt"), + issuer="auth.example.com", + ) + assert ( + str(cc_error.value) + == str(jwt_error.value) + == snapshot("issuer must be the authorization server's http(s) issuer URL, got 'auth.example.com'") + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_provider_refuses_metadata_for_a_different_issuer(mock_storage: MockTokenStorage, kind: str): + """SDK-defined: when discovery ends at an authorization server other than the configured `issuer`, + no token request is built and no assertion is minted.""" + audiences: list[str] = [] + provider = _provider_with_issuer(kind, mock_storage, audiences) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + + with pytest.raises(OAuthFlowError) as exc_info: + await _answer_discovery( + flow, + authorization_server="https://other-as.example.com", + metadata=_metadata_for("https://other-as.example.com"), + ) + + assert str(exc_info.value) == snapshot( + "Authorization server metadata issuer mismatch: https://other-as.example.com != https://auth.example.com" + ) + assert audiences == [] + + +@pytest.mark.anyio +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_provider_refuses_to_exchange_without_metadata_when_issuer_configured( + mock_storage: MockTokenStorage, kind: str +): + """SDK-defined: with `issuer=` set, the 2025-03-26 default `/token` on the resource origin is not + used when no authorization server metadata could be discovered.""" + audiences: list[str] = [] + provider = _provider_with_issuer(kind, mock_storage, audiences) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + + with pytest.raises(OAuthFlowError) as exc_info: + await _answer_discovery(flow, authorization_server=None, metadata=None) + + assert str(exc_info.value) == snapshot( + "No authorization server metadata discovered for configured issuer https://auth.example.com" + ) + assert audiences == [] + + +@pytest.mark.anyio +@pytest.mark.parametrize("kind", ["secret", "jwt"]) +async def test_a_refused_authorization_server_is_forgotten_so_the_next_request_rediscovers( + mock_storage: MockTokenStorage, kind: str +): + """SDK-defined: when the exchange is refused because discovery ended somewhere other than the + configured issuer, the refused metadata and any token held are dropped; the next request goes out + unauthenticated and discovery starts again, rather than a refresh being built from what was refused.""" + provider = _provider_with_issuer(kind, mock_storage, []) + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + token_request = await _answer_discovery( + flow, authorization_server=_CONFIGURED_ISSUER, metadata=_metadata_for(_CONFIGURED_ISSUER) + ) + token = {"access_token": "first", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rt"} + retried = await flow.asend(httpx2.Response(200, json=token, request=token_request)) + with pytest.raises(StopAsyncIteration): + await flow.asend(httpx2.Response(200, request=retried)) + + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + with pytest.raises(OAuthFlowError): + await _answer_discovery( + flow, + authorization_server="https://other-as.example.com", + metadata=_metadata_for("https://other-as.example.com"), + ) + assert provider.context.oauth_metadata is None + assert provider.context.current_tokens is None + + flow = provider.async_auth_flow(httpx2.Request("POST", _SERVER_URL)) + request = await flow.__anext__() + assert (str(request.url), request.headers.get("Authorization")) == (_SERVER_URL, None) + await flow.aclose() diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py index be96cc8e..18a15667 100644 --- a/tests/client/test_auth.py +++ b/tests/client/test_auth.py @@ -3,6 +3,7 @@ import base64 import json import time +from collections.abc import AsyncGenerator from unittest import mock from urllib.parse import parse_qs, quote, unquote, urlparse @@ -24,6 +25,7 @@ from mcp.client.auth.utils import ( extract_resource_metadata_from_www_auth, extract_scope_from_www_auth, get_client_metadata_scopes, + handle_auth_metadata_response, handle_registration_response, is_valid_client_metadata_url, should_use_client_metadata_url, @@ -824,6 +826,97 @@ class TestProtectedResourceMetadata: assert "resource=" in content +async def _start_discovery( + provider: OAuthClientProvider, +) -> tuple[AsyncGenerator[httpx2.Request, httpx2.Response], httpx2.Request]: + """Drive `provider`'s auth flow to the point where it has sent the MCP request, seen a 401 and + issued its first protected-resource-metadata request; returns (flow, that request).""" + provider.context.current_tokens = None + provider.context.token_expiry_time = None + provider._initialized = True + mcp_request = httpx2.Request("POST", "https://api.example.com/v1/mcp") + flow = provider.async_auth_flow(mcp_request) + sent = await flow.__anext__() + assert sent is mcp_request + # No resource_metadata hint, so discovery tries the path-based well-known URL, then the root one. + unauthorized = httpx2.Response(401, request=mcp_request) + prm_request = await flow.asend(unauthorized) + assert (prm_request.method, str(prm_request.url)) == ( + "GET", + "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp", + ) + return flow, prm_request + + +async def _redirect(request: httpx2.Request, status: int, location: str) -> httpx2.Response: + """A redirect answer to `request`, as httpx2 hands it back when it does not follow it.""" + transport = httpx2.MockTransport(lambda r: httpx2.Response(status, headers={"location": location})) + async with httpx2.AsyncClient(transport=transport) as client: + return await client.send(request) + + +@pytest.mark.anyio +async def test_auth_flow_follows_a_same_origin_redirect_of_its_own_request(oauth_provider: OAuthClientProvider): + """SDK-defined: a request the OAuth flow makes (here protected-resource metadata discovery) + follows a redirect that stays within its origin and keeps its method, like an MCP request.""" + flow, prm_request = await _start_discovery(oauth_provider) + + follow_up = await flow.asend(await _redirect(prm_request, 307, "/.well-known/oauth-protected-resource/v1/mcp/")) + + assert (follow_up.method, str(follow_up.url)) == ( + "GET", + "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp/", + ) + await flow.aclose() + + +@pytest.mark.anyio +async def test_auth_flow_does_not_follow_a_redirect_of_its_own_request_to_another_origin( + oauth_provider: OAuthClientProvider, +): + """SDK-defined: a redirect of a flow request to another origin is handed to the flow unfollowed, + which treats it as "not served here" and moves to its next discovery URL.""" + flow, prm_request = await _start_discovery(oauth_provider) + + next_request = await flow.asend(await _redirect(prm_request, 307, "https://elsewhere.example/prm")) + + assert (next_request.method, str(next_request.url)) == ( + "GET", + "https://api.example.com/.well-known/oauth-protected-resource", + ) + await flow.aclose() + + +@pytest.mark.anyio +async def test_auth_flow_stops_following_a_redirecting_request_after_a_few_hops( + oauth_provider: OAuthClientProvider, +): + """SDK-defined: a flow request that keeps redirecting within its origin is followed a bounded + number of times; the redirect after that is handed to the flow unfollowed.""" + flow, request = await _start_discovery(oauth_provider) + + hops = 0 + while str(request.url) != "https://api.example.com/.well-known/oauth-protected-resource": + request = await flow.asend( + await _redirect(request, 307, f"/.well-known/oauth-protected-resource/v1/mcp/{hops}") + ) + hops += 1 + + assert hops == 6 # five followed, the sixth handed back and taken as "try the next URL" + await flow.aclose() + + +@pytest.mark.anyio +@pytest.mark.parametrize(("status", "keep_trying"), [(404, True), (307, True), (500, False)]) +async def test_auth_metadata_response_says_whether_to_try_the_next_discovery_url( + status: int, keep_trying: bool +) -> None: + """SDK-defined: a 4xx or a 3xx (redirects are not followed on these requests) from a discovery + candidate means the metadata is not served there and the next well-known URL is tried; a 5xx + stops discovery.""" + assert await handle_auth_metadata_response(httpx2.Response(status)) == (keep_trying, None) + + @pytest.mark.parametrize( ("protocol_version", "expected"), [ @@ -977,35 +1070,33 @@ class TestRegistrationResponse: @pytest.mark.anyio async def test_handle_registration_response_reads_before_accessing_text(self): - """Test that response.aread() is called before accessing response.text.""" + """The registration error carries the response text, which for a streamed response means + reading it first (a streamed httpx2 response raises ResponseNotRead otherwise).""" + response = httpx2.Response(400, stream=httpx2.ByteStream(b"Registration failed with error")) - # Track if aread() was called - class MockResponse(httpx2.Response): - def __init__(self): - self.status_code = 400 - self._aread_called = False - self._text = "Registration failed with error" + with pytest.raises(OAuthRegistrationError) as exc_info: + await handle_registration_response(response) - async def aread(self): - self._aread_called = True - return b"test content" + assert str(exc_info.value) == snapshot("Registration failed: 400 Registration failed with error") - @property - def text(self): - if not self._aread_called: - raise RuntimeError("Response.text accessed before response.aread()") # pragma: no cover - return self._text + @pytest.mark.anyio + async def test_registration_error_names_an_unfollowed_redirect(self): + """SDK-defined: when the registration endpoint answered with a redirect that was not followed, + the error says where it pointed (without userinfo or query) instead of only the bare status.""" + request = httpx2.Request("POST", "https://as.example/register") + async with httpx2.AsyncClient( + transport=httpx2.MockTransport( + lambda r: httpx2.Response(307, headers={"location": "https://u:p@elsewhere.example/register?state=x"}) + ) + ) as client: + response = await client.send(request) - mock_response = MockResponse() + with pytest.raises(OAuthRegistrationError) as exc_info: + await handle_registration_response(response) - # This should call aread() before accessing text - with pytest.raises(Exception) as exc_info: - await handle_registration_response(mock_response) - - # Verify aread() was called - assert mock_response._aread_called - # Verify the error message includes the response text - assert "Registration failed: 400" in str(exc_info.value) + assert str(exc_info.value) == snapshot( + "Registration failed: 307 (redirected to https://elsewhere.example/register; not followed) " + ) @pytest.mark.anyio @@ -1502,8 +1593,11 @@ class TestAuthFlow: request=request, ) - # Trigger step-up - should get token exchange request - token_exchange_request = await auth_flow.asend(response_403) + # Trigger step-up - discovery runs first (nothing published here), then the token exchange + prm_request = await auth_flow.asend(response_403) + prm_request = await auth_flow.asend(httpx2.Response(404, request=prm_request)) + asm_request = await auth_flow.asend(httpx2.Response(404, request=prm_request)) + token_exchange_request = await auth_flow.asend(httpx2.Response(404, request=asm_request)) # Verify scope was updated to the union of prior and challenged scopes (SEP-2350) assert oauth_provider.context.client_metadata.scope == "read write admin:write admin:delete" @@ -1576,7 +1670,10 @@ async def test_403_step_up_preserves_scope_from_stored_token( headers={"WWW-Authenticate": 'Bearer error="insufficient_scope", scope="write"'}, request=request, ) - token_exchange_request = await auth_flow.asend(response_403) + prm_request = await auth_flow.asend(response_403) + prm_request = await auth_flow.asend(httpx2.Response(404, request=prm_request)) + asm_request = await auth_flow.asend(httpx2.Response(404, request=prm_request)) + token_exchange_request = await auth_flow.asend(httpx2.Response(404, request=asm_request)) assert reauthorize_scope == "read write" @@ -1593,6 +1690,101 @@ async def test_403_step_up_preserves_scope_from_stored_token( pass +@pytest.mark.anyio +async def test_scope_step_up_discovers_the_authorization_server_before_reauthorizing( + oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken +): + """SDK-defined: a 403 scope challenge runs discovery first when no metadata is held, so + re-authorization targets the advertised server. + + Steps: + 1. A restarted client holds a token and a registration but no authorization server metadata. + 2. The first response is 403 insufficient_scope -> the next requests are PRM (at the challenge's + `resource_metadata` URL) then ASM discovery. + 3. The authorization redirect and the token request use the discovered server's endpoints, and + the requested scope is the SEP-2350 union. + """ + await mock_storage.set_tokens(valid_tokens) + await mock_storage.set_client_info( + OAuthClientInformationFull( + client_id="registered-client", redirect_uris=[AnyUrl("http://localhost:3030/callback")] + ) + ) + redirects: list[str] = [] + + async def record_redirect(url: str) -> None: + redirects.append(url) + + async def echo_callback() -> AuthorizationCodeResult: + state = parse_qs(urlparse(redirects[-1]).query)["state"][0] + return AuthorizationCodeResult(code="auth_code", state=state) + + oauth_provider.context.redirect_handler = record_redirect + oauth_provider.context.callback_handler = echo_callback + + auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await auth_flow.__anext__() + response_403 = httpx2.Response( + 403, + headers={ + "WWW-Authenticate": 'Bearer error="insufficient_scope", scope="admin",' + ' resource_metadata="https://api.example.com/v1/mcp/resource-metadata"' + }, + request=request, + ) + + prm_request = await auth_flow.asend(response_403) + assert prm_request.method == "GET" + assert str(prm_request.url) == "https://api.example.com/v1/mcp/resource-metadata" + prm_response = httpx2.Response( + 200, + content=b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}', + request=prm_request, + ) + asm_request = await auth_flow.asend(prm_response) + assert str(asm_request.url) == "https://auth.example.com/.well-known/oauth-authorization-server" + asm_response = httpx2.Response( + 200, + content=( + b'{"issuer": "https://auth.example.com",' + b' "authorization_endpoint": "https://auth.example.com/authorize",' + b' "token_endpoint": "https://auth.example.com/token"}' + ), + request=asm_request, + ) + + token_request = await auth_flow.asend(asm_response) + assert redirects[-1].startswith("https://auth.example.com/authorize?") + assert parse_qs(urlparse(redirects[-1]).query)["scope"] == ["read write admin"] + assert token_request.method == "POST" + assert str(token_request.url) == "https://auth.example.com/token" + + final_request = await auth_flow.asend( + httpx2.Response( + 200, json={"access_token": "stepped_up", "token_type": "Bearer", "expires_in": 3600}, request=token_request + ) + ) + assert final_request.headers["Authorization"] == "Bearer stepped_up" + with pytest.raises(StopAsyncIteration): + await auth_flow.asend(httpx2.Response(200, request=final_request)) + + +@pytest.mark.anyio +async def test_403_without_a_scope_challenge_is_returned_to_the_caller( + oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken +): + """SDK-defined: a 403 that is not an insufficient_scope challenge ends the flow; the request is + not retried.""" + await mock_storage.set_tokens(valid_tokens) + + auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await auth_flow.__anext__() + response_403 = httpx2.Response(403, headers={"WWW-Authenticate": 'Bearer error="access_denied"'}, request=request) + + with pytest.raises(StopAsyncIteration): + await auth_flow.asend(response_403) + + @pytest.mark.parametrize( ( "issuer_url", @@ -1818,8 +2010,8 @@ class TestLegacyServerFallback: prm_request_1 = await auth_flow.asend(response) assert str(prm_request_1.url) == "https://custom.prm.com/.well-known/oauth-protected-resource" - # Returns 500 - prm_response_1 = httpx2.Response(500, request=prm_request_1) + # Not served there + prm_response_1 = httpx2.Response(404, request=prm_request_1) # Try path-based fallback prm_request_2 = await auth_flow.asend(prm_response_1) @@ -2891,6 +3083,25 @@ def test_credentials_match_issuer_different_issuer(): assert credentials_match_issuer(info, "https://other", None) is False +@pytest.mark.parametrize( + ("recorded", "current"), + [("https://as.example.com", "https://as.example.com/"), ("https://as.example.com/", "https://as.example.com")], + ids=["current-has-root-slash", "recorded-has-root-slash"], +) +def test_credentials_match_issuer_treats_root_slash_as_the_same_issuer(recorded: str, current: str): + """SDK-defined: a root issuer recorded with and without its trailing slash names the same server.""" + info = OAuthClientInformationFull(client_id="c", redirect_uris=[AnyUrl("http://localhost/cb")], issuer=recorded) + assert credentials_match_issuer(info, current, None) is True + + +def test_credentials_match_issuer_root_slash_tolerance_does_not_extend_to_other_paths(): + """SDK-defined: a trailing slash on a non-root path is a different issuer.""" + info = OAuthClientInformationFull( + client_id="c", redirect_uris=[AnyUrl("http://localhost/cb")], issuer="https://as.example.com/tenant" + ) + assert credentials_match_issuer(info, "https://as.example.com/tenant/", None) is False + + def test_credentials_match_issuer_no_recorded_issuer_is_left_alone(): """Credentials with no bound issuer (pre-registered / legacy) carry no binding to enforce.""" info = OAuthClientInformationFull(client_id="c", redirect_uris=[AnyUrl("http://localhost/cb")]) @@ -3008,15 +3219,14 @@ async def test_handle_refresh_response_adopts_rotated_refresh_token_when_returne @pytest.mark.anyio -async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed( +async def test_issuer_binding_evaluated_against_the_server_origin_when_prm_discovery_failed( oauth_provider: OAuthClientProvider, ): - """SEP-2352: on the legacy no-PRM path the binding check uses the ASM-discovered issuer. + """SEP-2352: on the legacy no-PRM path the binding check uses the resource server's origin. - PRM discovery fails (404) so ``auth_server_url`` stays ``None`` and the post-PRM check is - skipped; when ASM discovery then succeeds via the root well-known fallback, the discovered - metadata's issuer is compared against the stored credentials' bound issuer and a mismatch - triggers re-registration. + PRM discovery fails (404) so ``auth_server_url`` stays ``None``; the expected issuer is then the + origin the legacy well-known URL is built from, so stored credentials bound to another issuer are + discarded before ASM discovery runs, and re-registration follows. """ oauth_provider.context.current_tokens = None oauth_provider.context.token_expiry_time = None @@ -3037,9 +3247,11 @@ async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed( prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) assert str(prm_req.url) == "https://api.example.com/.well-known/oauth-protected-resource" - # ASM discovery via root fallback (no auth_server_url) succeeds with a different issuer. + # ASM discovery via root fallback (no auth_server_url): the stale credentials are already + # gone when the request is issued. asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) assert str(asm_req.url) == "https://api.example.com/.well-known/oauth-authorization-server" + assert oauth_provider.context.client_info is None asm_response = httpx2.Response( 200, content=( @@ -3060,6 +3272,208 @@ async def test_issuer_binding_re_evaluated_after_asm_when_prm_discovery_failed( await auth_flow.aclose() +@pytest.mark.anyio +async def test_legacy_fallback_metadata_naming_a_different_issuer_is_refused(oauth_provider: OAuthClientProvider): + """RFC 8414 section 3.3 on the legacy no-PRM path: metadata served from the resource server's + own well-known must name that origin as its issuer; anything else stops the flow before any + authorization or token request is built.""" + auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await auth_flow.__anext__() + + # 401 without WWW-Authenticate; both PRM well-knowns 404; legacy root ASM discovery. + prm_req = await auth_flow.asend(httpx2.Response(401, request=request)) + prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + assert str(asm_req.url) == "https://api.example.com/.well-known/oauth-authorization-server" + + asm_response = httpx2.Response( + 200, + content=( + b'{"issuer": "https://other-as.example.com", ' + b'"authorization_endpoint": "https://other-as.example.com/authorize", ' + b'"token_endpoint": "https://other-as.example.com/token"}' + ), + request=asm_req, + ) + with pytest.raises(OAuthFlowError) as exc_info: + await auth_flow.asend(asm_response) + + assert str(exc_info.value) == snapshot( + "Authorization server metadata issuer mismatch: https://other-as.example.com != https://api.example.com" + ) + + +@pytest.mark.anyio +@pytest.mark.parametrize( + "first_response", + [(401, {}), (403, {"WWW-Authenticate": 'Bearer error="insufficient_scope", scope="admin"'})], + ids=["401", "403-insufficient-scope"], +) +async def test_legacy_fallback_without_metadata_re_registers_instead_of_presenting_credentials_bound_elsewhere( + oauth_provider: OAuthClientProvider, + mock_storage: MockTokenStorage, + valid_tokens: OAuthToken, + first_response: tuple[int, dict[str, str]], +): + """SEP-2352 on the legacy no-PRM path when no metadata is served at all, whether the flow starts + from a 401 or from a 403 scope challenge with no metadata held. + + Steps: + 1. Storage holds a token and a confidential client bound to a different authorization server. + 2. Both PRM well-knowns 404 -> the expected issuer is the resource server's origin, so the stored + client is discarded before ASM discovery. + 3. The origin's ASM well-known 404s too -> the flow registers a fresh client at the + origin's default `/register` and authorizes with it; the token request to the origin's + default `/token` carries the new client and none of the discarded credentials. + """ + await mock_storage.set_tokens(valid_tokens) + await mock_storage.set_client_info( + OAuthClientInformationFull( + client_id="confidential-client", + client_secret="stored-secret", + token_endpoint_auth_method="client_secret_post", + redirect_uris=[AnyUrl("http://localhost:3030/callback")], + issuer="https://other-as.example.com", + ) + ) + redirects: list[str] = [] + + async def record_redirect(url: str) -> None: + redirects.append(url) + + async def echo_callback() -> AuthorizationCodeResult: + state = parse_qs(urlparse(redirects[-1]).query)["state"][0] + return AuthorizationCodeResult(code="auth_code", state=state) + + oauth_provider.context.redirect_handler = record_redirect + oauth_provider.context.callback_handler = echo_callback + + auth_flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await auth_flow.__anext__() + + status, headers = first_response + prm_req = await auth_flow.asend(httpx2.Response(status, headers=headers, request=request)) + prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + assert str(asm_req.url) == "https://api.example.com/.well-known/oauth-authorization-server" + + # No metadata at the origin either: register at the origin's default endpoint. + register_req = await auth_flow.asend(httpx2.Response(404, request=asm_req)) + assert register_req.method == "POST" + assert str(register_req.url) == "https://api.example.com/register" + register_response = httpx2.Response( + 201, + json={ + "client_id": "origin-client", + "redirect_uris": ["http://localhost:3030/callback"], + "token_endpoint_auth_method": "none", + }, + request=register_req, + ) + + token_req = await auth_flow.asend(register_response) + assert str(token_req.url) == "https://api.example.com/token" + assert redirects[-1].startswith("https://api.example.com/authorize?") + token_form = parse_qs(token_req.content.decode()) + assert token_form["client_id"] == ["origin-client"] + assert "client_secret" not in token_form + await auth_flow.aclose() + + +@pytest.mark.anyio +@pytest.mark.parametrize("status", [500, 503, 429]) +async def test_a_failing_resource_metadata_request_stops_the_flow_and_keeps_stored_credentials( + oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken, status: int +): + """SDK-defined: a server error (or 429) on a protected resource metadata request says nothing about + whether the server publishes that metadata. The remaining well-known locations are still tried, but + when none answers the flow stops instead of taking the legacy path, and a registration bound to the + advertised authorization server and its tokens stay as they were.""" + bound = OAuthClientInformationFull( + client_id="registered-client", + redirect_uris=[AnyUrl("http://localhost:3030/callback")], + issuer="https://auth.example.com", + ) + await mock_storage.set_client_info(bound) + await mock_storage.set_tokens(valid_tokens) + flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await flow.__anext__() + prm_request = await flow.asend(httpx2.Response(401, request=request)) + root_prm_request = await flow.asend(httpx2.Response(status, request=prm_request)) + assert str(root_prm_request.url) == "https://api.example.com/.well-known/oauth-protected-resource" + + with pytest.raises(OAuthFlowError) as exc_info: + await flow.asend(httpx2.Response(404, request=root_prm_request)) + + assert str(exc_info.value) == f"Protected resource metadata request failed: HTTP {status}" + assert oauth_provider.context.client_info == bound + assert oauth_provider.context.current_tokens == valid_tokens + assert await mock_storage.get_client_info() == bound + + +@pytest.mark.anyio +async def test_a_failing_resource_metadata_location_does_not_matter_when_another_one_answers( + oauth_provider: OAuthClientProvider, +): + """SDK-defined: the well-known locations are tried in order; an error at one of them is forgotten + once a later one returns the metadata.""" + flow = oauth_provider.async_auth_flow(httpx2.Request("GET", "https://api.example.com/v1/mcp")) + request = await flow.__anext__() + prm_request = await flow.asend(httpx2.Response(401, request=request)) + root_prm_request = await flow.asend(httpx2.Response(503, request=prm_request)) + prm = b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}' + + asm_request = await flow.asend(httpx2.Response(200, content=prm, request=root_prm_request)) + + assert str(asm_request.url) == "https://auth.example.com/.well-known/oauth-authorization-server" + await flow.aclose() + + +@pytest.mark.anyio +async def test_legacy_fallback_accepts_a_root_slash_issuer_for_a_server_url_in_any_spelling( + client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage +): + """SDK-defined: on the legacy no-PRM path the expected issuer is the resource server's origin; a + `server_url` written with an upper-case host and an explicit default port still matches metadata + naming that origin, with or without the trailing slash a root issuer is often rendered with, and + the flow proceeds to registration.""" + + async def redirect_handler(url: str) -> None: + raise NotImplementedError + + async def callback_handler() -> AuthorizationCodeResult: + raise NotImplementedError + + provider = OAuthClientProvider( + server_url="https://API.Example.com:443/v1/mcp", + client_metadata=client_metadata, + storage=mock_storage, + redirect_handler=redirect_handler, + callback_handler=callback_handler, + ) + auth_flow = provider.async_auth_flow(httpx2.Request("GET", "https://API.Example.com:443/v1/mcp")) + request = await auth_flow.__anext__() + prm_req = await auth_flow.asend(httpx2.Response(401, request=request)) + prm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + asm_req = await auth_flow.asend(httpx2.Response(404, request=prm_req)) + asm_response = httpx2.Response( + 200, + content=( + b'{"issuer": "https://api.example.com/", ' + b'"authorization_endpoint": "https://api.example.com/authorize", ' + b'"token_endpoint": "https://api.example.com/token", ' + b'"registration_endpoint": "https://api.example.com/register"}' + ), + request=asm_req, + ) + + register_req = await auth_flow.asend(asm_response) + + assert register_req.method == "POST" + assert str(register_req.url) == "https://api.example.com/register" + await auth_flow.aclose() + + @pytest.mark.anyio @pytest.mark.parametrize( "asm_responses", diff --git a/tests/client/test_http_unicode.py b/tests/client/test_http_unicode.py index ef9511fb..9996c192 100644 --- a/tests/client/test_http_unicode.py +++ b/tests/client/test_http_unicode.py @@ -112,11 +112,9 @@ async def unicode_session() -> AsyncIterator[ClientSession]: async with ( session_manager.run(), - # follow_redirects matches the SDK's own client factory; Starlette's Mount 307-redirects - # the bare /mcp path to /mcp/. - httpx2.AsyncClient( - transport=StreamingASGITransport(app), base_url=BASE_URL, follow_redirects=True - ) as http_client, + # Starlette's Mount 307-redirects the bare /mcp path to /mcp/; the transport follows that + # same-origin redirect itself. + httpx2.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http_client, streamable_http_client(f"{BASE_URL}/mcp", http_client=http_client) as (read_stream, write_stream), ClientSession(read_stream, write_stream) as session, ): diff --git a/tests/client/test_output_schema_validation.py b/tests/client/test_output_schema_validation.py index 60f6fadc..10715e63 100644 --- a/tests/client/test_output_schema_validation.py +++ b/tests/client/test_output_schema_validation.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path from typing import Any import pytest @@ -10,6 +11,7 @@ from mcp_types import ( TextContent, Tool, ) +from referencing.exceptions import Unresolvable from mcp import Client from mcp.server import Server, ServerRequestContext @@ -163,3 +165,26 @@ async def test_tool_not_listed_warning(caplog: pytest.LogCaptureFixture): assert result.is_error is False assert "Tool mystery_tool not listed" in caplog.text + + +# jsonschema's fallback retriever emits this DeprecationWarning; keep it a plain warning so the +# assertions below decide the outcome rather than the suite's warnings-as-errors filter. +@pytest.mark.filterwarnings("default:Automatically retrieving remote references:DeprecationWarning") +@pytest.mark.anyio +async def test_output_schema_ref_outside_the_document_is_rejected(tmp_path: Path): + """A `$ref` to a URI outside the output schema is not resolved, and a result whose validation + reaches one fails as an invalid schema (spec `$ref` resolution; applying it to `file:` URIs too + is SDK-defined).""" + target = tmp_path / "schema.json" + target.write_text("{}", encoding="utf-8") + server = _make_server( + tools=[Tool(name="probe", input_schema={"type": "object"}, output_schema={"$ref": target.as_uri()})], + structured_content={"v": 1}, + ) + + async with Client(server) as client: + with pytest.raises(RuntimeError) as exc_info: + await client.call_tool("probe", {}) + # SDK-authored prefix only; the tail is `referencing`'s text. + assert str(exc_info.value).startswith("Invalid schema for tool probe: ") + assert isinstance(exc_info.value.__cause__, Unresolvable) diff --git a/tests/client/test_stdio.py b/tests/client/test_stdio.py index 91f829ff..0101e45a 100644 --- a/tests/client/test_stdio.py +++ b/tests/client/test_stdio.py @@ -14,14 +14,18 @@ import math import os import signal import sys +import threading from collections.abc import Callable from contextlib import AsyncExitStack, suppress from pathlib import Path +from types import SimpleNamespace from typing import TextIO, cast import anyio import anyio.abc +import anyio.from_thread import anyio.lowlevel +import anyio.to_thread import pytest import trio import trio.testing @@ -199,6 +203,9 @@ def install_fake_process( """ terminated: list[FakeProcess] = [] + async def fake_get_executable_command(command: str) -> str: + return command + async def fake_spawn( command: str, args: list[str], @@ -212,6 +219,7 @@ def install_fake_process( terminated.append(proc) proc.exit(-15) + monkeypatch.setattr(stdio, "_get_executable_command", fake_get_executable_command) monkeypatch.setattr(stdio, "_create_platform_compatible_process", fake_spawn) monkeypatch.setattr(stdio, "_terminate_process_tree", fake_terminate_tree) if grace_period is not None: @@ -568,6 +576,45 @@ async def test_a_command_that_cannot_be_execed_raises_enoent() -> None: assert exc_info.value.errno == errno.ENOENT +@pytest.mark.anyio +async def test_cancellation_during_windows_command_resolution_returns_before_resolution_finishes( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Cancelling `stdio_client` does not wait for blocked Windows command resolution.""" + resolution_started = anyio.Event() + resolution_release = threading.Event() + resolution_finished = threading.Event() + + def blocking_resolver(command: str) -> str: + anyio.from_thread.run_sync(resolution_started.set) + resolution_release.wait() + resolution_finished.set() + return command + + monkeypatch.setattr(stdio, "sys", SimpleNamespace(platform="win32")) + monkeypatch.setattr(stdio, "get_windows_executable_command", blocking_resolver) + + cancel_scope = anyio.CancelScope() + client_stopped = anyio.Event() + + async def run_client() -> None: + with cancel_scope: + async with AsyncExitStack() as stack: + await stack.enter_async_context(stdio_client(FAKE_PARAMS)) + client_stopped.set() + + with anyio.fail_after(5): + async with anyio.create_task_group() as tg: + tg.start_soon(run_client) + await resolution_started.wait() + cancel_scope.cancel() + try: + await client_stopped.wait() + finally: + resolution_release.set() + await anyio.to_thread.run_sync(resolution_finished.wait) + + @pytest.mark.anyio async def test_cancellation_during_spawn_leaks_no_streams(monkeypatch: pytest.MonkeyPatch) -> None: """Cancellation while the spawn is still in flight must not leak the internal streams. diff --git a/tests/client/test_streamable_http.py b/tests/client/test_streamable_http.py index d21f520d..c6e62ad9 100644 --- a/tests/client/test_streamable_http.py +++ b/tests/client/test_streamable_http.py @@ -26,18 +26,24 @@ from mcp_types import ( JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, + ListToolsResult, + PaginatedRequestParams, ) from mcp_types.version import LATEST_MODERN_VERSION +from starlette.applications import Starlette +from starlette.routing import Mount from starlette.types import Receive, Scope, Send +from mcp import Client, MCPError from mcp.client.streamable_http import ( MAX_RECONNECTION_ATTEMPTS, RequestContext, StreamableHTTPTransport, streamable_http_client, ) -from mcp.server import Server +from mcp.server import Server, ServerRequestContext from mcp.server._streamable_http_modern import handle_modern_request +from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from mcp.server.subscriptions import InMemorySubscriptionBus, ListenHandler, ServerEvent from mcp.shared._context_streams import ContextSendStream, create_context_streams from mcp.shared.dispatcher import CallOptions, DispatchContext @@ -748,3 +754,217 @@ async def test_resolving_an_abandoned_request_after_the_reader_closed_is_contain _abandoned_request_context(http, send), "evt-7", None, MAX_RECONNECTION_ATTEMPTS ) send.close() + + +@pytest.mark.anyio +async def test_trailing_slash_redirect_within_origin_is_followed_by_the_transport() -> None: + """SDK-defined: a redirect that stays on the endpoint's origin (here Starlette's Mount sending + /mcp to /mcp/) is followed by the transport itself, so a caller-supplied client left at + httpx2's no-follow default still connects.""" + session_manager = StreamableHTTPSessionManager(app=Server("redirect-test")) + app = Starlette(routes=[Mount("/mcp", app=session_manager.handle_request)]) + urls: list[str] = [] + + async def record(request: httpx2.Request) -> None: + urls.append(str(request.url)) + + with anyio.fail_after(5): + async with ( + session_manager.run(), + httpx2.AsyncClient(transport=StreamingASGITransport(app), event_hooks={"request": [record]}) as http, + Client(streamable_http_client("http://mcp.example/mcp", http_client=http)) as client, + ): + assert client.server_info is not None + assert client.server_info.name == "redirect-test" + + assert urls[:2] == ["http://mcp.example/mcp", "http://mcp.example/mcp/"] + + +class _RedirectPromptsListElsewhere(httpx2.AsyncBaseTransport): + """Serves `app` in process, except that a prompts/list POST is answered with a redirect to + another origin.""" + + def __init__(self, app: Starlette) -> None: + self.inner = StreamingASGITransport(app) + + async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response: + await request.aread() + if request.method == "POST" and json.loads(request.content).get("method") == "prompts/list": + return httpx2.Response(307, headers={"location": "http://other.example/mcp/"}) + return await self.inner.handle_async_request(request) + + async def __aenter__(self) -> "_RedirectPromptsListElsewhere": + await self.inner.__aenter__() + return self + + async def __aexit__(self, *args: Any) -> None: + await self.inner.__aexit__(*args) + + +@pytest.mark.anyio +async def test_redirect_to_another_origin_fails_that_call_and_keeps_the_session() -> None: + """SDK-defined: a redirect pointing outside the endpoint's origin is not followed, whatever the + caller's client is configured to do: the call it answered fails with MCPError naming the + location, nothing is sent to the other origin, and the session stays usable.""" + + async def list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult: + return ListToolsResult(tools=[]) + + session_manager = StreamableHTTPSessionManager(app=Server("redirect-test", on_list_tools=list_tools)) + app = Starlette(routes=[Mount("/mcp", app=session_manager.handle_request)]) + urls: list[str] = [] + + async def record(request: httpx2.Request) -> None: + urls.append(str(request.url)) + + with anyio.fail_after(5): + async with ( + session_manager.run(), + httpx2.AsyncClient( + transport=_RedirectPromptsListElsewhere(app), event_hooks={"request": [record]}, follow_redirects=True + ) as http, + Client(streamable_http_client("http://mcp.example/mcp/", http_client=http)) as client, + ): + with pytest.raises(MCPError) as exc_info: + await client.list_prompts() + assert (await client.list_tools()).tools == [] + + assert exc_info.value.error.code == INVALID_REQUEST + assert exc_info.value.error.message == snapshot( + "Redirect to http://other.example/mcp/ not followed; use that URL as the endpoint if it is the intended server" + ) + assert [url for url in urls if "other.example" in url] == [] + + +@pytest.mark.anyio +async def test_redirected_notification_is_dropped_and_the_next_message_still_goes_out() -> None: + """SDK-defined: a notification whose POST is redirected outside the origin has no waiter to + resolve, so it is logged and dropped; the transport keeps serving the write stream.""" + urls: list[str] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + urls.append(str(request.url)) + return httpx2.Response(307, headers={"location": "http://other.example/mcp"}) + + with anyio.fail_after(5): + async with ( + httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http, + streamable_http_client("http://test/mcp", http_client=http) as (read, write), + ): + await write.send( + SessionMessage(JSONRPCNotification(jsonrpc="2.0", method="notifications/roots/list_changed")) + ) + await write.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=7, method="tools/list", params={}))) + reply = await read.receive() + assert isinstance(reply, SessionMessage) + assert isinstance(reply.message, JSONRPCError) + assert reply.message.id == 7 + assert reply.message.error.code == INVALID_REQUEST + assert urls == ["http://test/mcp", "http://test/mcp"] + + +@pytest.mark.anyio +async def test_get_stream_gives_up_without_retrying_when_the_endpoint_redirects_elsewhere() -> None: + """SDK-defined: the standalone GET stream is not opened through a redirect to another origin, + and since the same GET would be redirected again the transport logs it and stops instead of + spending its reconnection attempts.""" + gets: list[str] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + gets.append(str(request.url)) + return httpx2.Response(307, headers={"location": "http://other.example/mcp"}) + + transport = StreamableHTTPTransport("http://test/mcp") + transport.session_id = "session-1" + send, receive = create_context_streams[SessionMessage | Exception](1) + with anyio.fail_after(5): + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http: + await transport.handle_get_stream(http, send) + assert gets == ["http://test/mcp"] + send.close() + receive.close() + + +@pytest.mark.anyio +async def test_resumption_redirected_elsewhere_resolves_that_request_with_an_error() -> None: + """SDK-defined: a resumption GET answered with a redirect to another origin is not followed; + the resumed request is resolved with an error naming the location rather than left waiting.""" + seen: list[tuple[str, str | None]] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + seen.append((f"{request.method} {request.url}", request.headers.get("last-event-id"))) + return httpx2.Response(307, headers={"location": "http://other.example/mcp"}) + + with anyio.fail_after(5): + async with ( + httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http, + streamable_http_client("http://test/mcp", http_client=http) as (read, write), + ): + await write.send( + SessionMessage( + JSONRPCRequest(jsonrpc="2.0", id="resume-1", method="tools/call", params={}), + metadata=ClientMessageMetadata(resumption_token="evt-41"), + ) + ) + reply = await read.receive() + assert isinstance(reply, SessionMessage) + assert isinstance(reply.message, JSONRPCError) + assert reply.message.id == "resume-1" + assert reply.message.error.code == INVALID_REQUEST + assert reply.message.error.message == snapshot( + "Redirect to http://other.example/mcp not followed; use that URL as the endpoint if it is the intended server" + ) + assert seen == [("GET http://test/mcp", "evt-41")] + + +async def _redirected_call_error(url: str, location: str) -> str: + """Send one request through streamable_http_client to a server answering `url` with a 307 to + `location`, and return the message of the error that resolves it.""" + + def handler(request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(307, headers={"location": location}) + + with anyio.fail_after(5): + async with ( + httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http, + streamable_http_client(url, http_client=http) as (read, write), + ): + await write.send(SessionMessage(JSONRPCRequest(jsonrpc="2.0", id=1, method="tools/list", params={}))) + reply = await read.receive() + assert isinstance(reply, SessionMessage) + assert isinstance(reply.message, JSONRPCError) + return reply.message.error.message + + +@pytest.mark.anyio +async def test_https_endpoint_redirected_to_plain_http_is_explained_and_the_https_form_suggested() -> None: + """SDK-authored text: a redirect of an HTTPS endpoint to plain HTTP on the same host (the usual + sign of a TLS-terminating proxy the server does not trust) never suggests the http:// URL.""" + message = await _redirected_call_error("https://mcp.example/mcp", "http://mcp.example/mcp/") + assert message == snapshot("""\ +Redirect to http://mcp.example/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +The server is likely behind a TLS-terminating proxy whose forwarded headers it does not trust, +often combined with a trailing-slash difference. Try https://mcp.example/mcp/ instead, or fix the proxy settings.\ +""") + + +@pytest.mark.anyio +async def test_unfollowed_redirect_location_is_named_without_its_query_string() -> None: + """SDK-authored text: the location is reported without query or userinfo, which may carry state + that does not belong in an error message or a log line.""" + message = await _redirected_call_error("http://mcp.example/mcp", "https://sso.example/login?state=s3cr3t&nonce=n") + assert message == snapshot( + "Redirect to https://sso.example/login not followed; use that URL as the endpoint if it is the intended server" + ) + + +@pytest.mark.anyio +async def test_https_endpoint_redirected_to_plain_http_elsewhere_never_suggests_the_http_url() -> None: + """SDK-authored text: the downgrade explanation applies whatever host the http:// location names, + so the message never offers a plain-HTTP URL as the endpoint to configure.""" + message = await _redirected_call_error("https://mcp.example/mcp", "http://backend.lan:8000/mcp/") + assert message == snapshot("""\ +Redirect to http://backend.lan:8000/mcp/ not followed: it would downgrade this HTTPS endpoint to plain HTTP. +The server is likely behind a TLS-terminating proxy whose forwarded headers it does not trust, +often combined with a trailing-slash difference. Try https://backend.lan:8000/mcp/ instead, or fix the proxy settings.\ +""") diff --git a/tests/conftest.py b/tests/conftest.py index 9ade27e7..4350b76f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,9 @@ import os from collections.abc import AsyncIterator, Iterator +import httpcore2 as _httpcore2 import pytest +from blockbuster import BlockBuster # OpenTelemetry's `set_tracer_provider` is set-once per process, so the suite # uses a single span-capture mechanism: logfire's `capfire` fixture (its @@ -17,12 +19,36 @@ from logfire.testing import CaptureLogfire # noqa: E402 import mcp.shared._otel # noqa: E402 +# Load httpx2's lazy default transport before BlockBuster starts. +del _httpcore2 + @pytest.fixture(scope="session") def anyio_backend() -> str: return "asyncio" +@pytest.fixture(autouse=True) +def blockbuster() -> Iterator[None]: + bb = BlockBuster(["mcp", "mcp_types"]) + # Coverage reads source files while collecting data. + bb.functions["os.stat"].can_block_in("coverage/python.py", "get_python_source") + bb.functions["io.BufferedReader.read"].can_block_in("coverage/python.py", "read_python_source") + # jsonschema discovers its bundled schemas during its first import. + bb.functions["os.listdir"].can_block_in("/jsonschema_specifications/_core.py", "_schemas") + bb.functions["os.scandir"].can_block_in("/jsonschema_specifications/_core.py", "_schemas") + bb.functions["io.TextIOWrapper.read"].can_block_in("/jsonschema_specifications/_core.py", "_schemas") + # These public synchronous conversions read the media file by design. + bb.functions["io.BufferedReader.read"].can_block_in( + "mcp/server/mcpserver/utilities/types.py", ("to_image_content", "to_audio_content") + ) + bb.activate() + try: + yield + finally: + bb.deactivate() + + @pytest.fixture(scope="module", autouse=True) async def _module_runner_lease(anyio_backend: str) -> AsyncIterator[None]: """Share one event loop across each module's tests instead of one per test. diff --git a/tests/docs/test_translations.py b/tests/docs/test_translations.py index 81ba1cb8..b5eddae0 100644 --- a/tests/docs/test_translations.py +++ b/tests/docs/test_translations.py @@ -9,6 +9,7 @@ repository a `tmp_path` tree, so every test is offline and deterministic. """ import json +import threading from collections.abc import Sequence from pathlib import Path @@ -197,15 +198,24 @@ class FakeTranslator: def run( capsys: pytest.CaptureFixture[str], root: Path, *argv: str, translator: t.Translator | None = None ) -> tuple[int, str, str]: + """`(exit code, stdout, stderr)` of one command line.""" code = t.main(list(argv), root=root, translator=translator) captured = capsys.readouterr() return code, captured.out, captured.err +def translate( + capsys: pytest.CaptureFixture[str], root: Path, *argv: str, jobs: int = 1, translator: t.Translator | None = None +) -> tuple[int, str, str]: + """`translate ARGV --jobs JOBS`: one page at a time by default, so scripted replies pair with pages in + nav order and the log has one possible order.""" + return run(capsys, root, "translate", *argv, "--jobs", str(jobs), translator=translator) + + def translate_all(capsys: pytest.CaptureFixture[str], root: Path) -> None: """Publish faithful translations of the three pages and the notices through the real command.""" fake = FakeTranslator([INDEX_JA, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - assert run(capsys, root, "translate", "--lang", "ja", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", translator=fake)[0] == 0 def test_sections_tile_the_page_and_blank_lines_belong_to_the_heading_after_them() -> None: @@ -552,7 +562,7 @@ def test_glossary_of_the_wrong_shape_stops_translate_with_exit_2_but_never_break path = root / "i18n" / "ja" / "glossary.json" write(path, json.dumps(glossary)) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=FakeTranslator([])) + code, out, err = translate(capsys, root, "--lang", "ja", translator=FakeTranslator([])) assert (code, out) == (2, "") assert err.startswith(f"translations: {path}: {message}") # an exception's own text is the interpreter's @@ -567,14 +577,15 @@ def test_translate_writes_pages_with_provenance_and_a_second_run_makes_no_calls( root = make_repo(tmp_path) fake = FakeTranslator([INDEX_JA, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, err) == (0, "") assert out == snapshot("""\ -translated: index.md (2 of 2 sections) -translated: tools.md (3 of 3 sections) -translated: translations.md (1 of 1 sections) -translated: i18n/notices.md (4 of 4 sections) +translating 4 pages (ja), 1 at a time, with test-model +ja: translated index.md (2 of 2 sections) +ja: translated tools.md (3 of 3 sections) +ja: translated translations.md (1 of 1 sections) +ja: translated i18n/notices.md (4 of 4 sections) usage: 4000 input / 1600 output / 3600 cache-write / 400 cache-read tokens """) assert (root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8") == snapshot("""\ @@ -621,7 +632,7 @@ Use these renderings; the notes are binding: - server → ć‚µćƒ¼ćƒćƒ¼. Katakana, long vowel kept.\ """) - code, out, err = run(capsys, root, "translate", "--lang", "ja") + code, out, err = translate(capsys, root, "--lang", "ja") assert (code, out, err) == (0, "ja: nothing to translate\n", "") assert run(capsys, root, "status") == snapshot( @@ -637,10 +648,10 @@ def test_docs_translate_model_overrides_the_registry_model_for_the_run( root = make_repo(tmp_path) monkeypatch.delenv("DOCS_TRANSLATE_MODEL", raising=False) fake = FakeTranslator([INDEX_JA, INDEX_JA]) - assert run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 monkeypatch.setenv("DOCS_TRANSLATE_MODEL", "trial-model") - code, _, _ = run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake) + code, _, _ = translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake) assert (code, fake.models) == (0, ["test-model", "trial-model"]) assert "model" not in (root / "i18n" / "ja" / "pages" / "index.md").read_text(encoding="utf-8") @@ -656,14 +667,14 @@ def test_translate_pages_retranslates_the_named_pages_from_scratch_even_when_a_t translate_all(capsys, root) fake = FakeTranslator([INDEX_JA.replace("ćøć‚ˆć†ć“ć", "ćøć‚ˆć†ć“ćļ¼")]) - code, out, _ = run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake) + code, out, _ = translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake) - assert (code, out.split("\n")[0]) == (0, "translated: index.md (2 of 2 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated index.md (2 of 2 sections)") assert fake.conversations[0] == [t.Message("user", t.translate_request(INDEX))] assert "MCP ćøć‚ˆć†ć“ćļ¼" in (root / "i18n" / "ja" / "pages" / "index.md").read_text(encoding="utf-8") assert run(capsys, root, "status")[1] == snapshot("ja (ę—„ęœ¬čŖž): 0 missing, 0 outdated, 4 current, 0 removable\n") - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "nope.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "nope.md", translator=fake) assert (code, err) == snapshot( (2, "translations: not translatable pages (nav paths such as servers/tools.md): ['nope.md']\n") @@ -684,9 +695,9 @@ def test_outdated_page_retranslates_the_changed_section_and_carries_the_rest_for ) fake = FakeTranslator([reply]) - code, out, _ = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, _ = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, out.split("\n")[0]) == (0, "translated: tools.md (1 of 3 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[0][0].content == snapshot("""\ This page was translated before. Retranslate it: translate the sections listed below afresh from the current English, applying the current language instructions and glossary @@ -765,9 +776,9 @@ def test_banned_rendering_is_a_finding_in_a_retranslated_section_but_not_in_a_ca repaired = TOOLS_JA.replace("å¤±ę•—ć‚’ä¼ćˆć‚‹ć«ćÆä¾‹å¤–ć‚’é€å‡ŗć—ć¾ć™ć€‚", "ćƒ•ć‚”ćƒ³ć‚Æć‚·ćƒ§ćƒ³ć‹ć‚‰é€å‡ŗć—ć¾ć™ć€‚") fake = FakeTranslator([slipped, repaired]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[0]) == (0, "", [], "translated: tools.md (1 of 3 sections)") + assert (code, err, fake.replies, out.split("\n")[1]) == (0, "", [], "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[1][2].content == snapshot("""\ Your translation broke the following structural rules. Fix each problem and return the full corrected page, changing nothing else: @@ -793,9 +804,9 @@ def test_link_dropped_in_a_carried_section_costs_no_repair_turn_and_the_stored_s ) fake = FakeTranslator([reply]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, out.split("\n")[0]) == (0, "", "translated: tools.md (1 of 3 sections)") + assert (code, err, out.split("\n")[1]) == (0, "", "ja: translated tools.md (1 of 3 sections)") assert [len(conversation) for conversation in fake.conversations] == [1] # one call, no repair turn body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1] assert t.sections(body)[0] == t.sections(TOOLS_JA)[0].replace("# ćƒ„ćƒ¼ćƒ«\n", "# ćƒ„ćƒ¼ćƒ« {#tools}\n") @@ -819,9 +830,9 @@ def test_removed_english_section_is_reassembled_with_no_client_but_an_edited_one monkeypatch.setattr(t, "anthropic_translator", no_credentials) write(root / "docs" / "tools.md", TOOLS.split("## Errors")[0].rstrip("\n") + "\n") - code, out, _ = run(capsys, root, "translate", "--lang", "ja") + code, out, _ = translate(capsys, root, "--lang", "ja") - assert (code, out.split("\n")[0]) == (0, "translated: tools.md (0 of 2 sections)") + assert (code, out.split("\n")[1]) == (0, "ja: translated tools.md (0 of 2 sections)") body = t.split_front_matter((root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8"))[1] previous = TOOLS_JA.split("## ć‚Øćƒ©ćƒ¼")[0].rstrip("\n") + "\n" assert body == previous.replace("# ćƒ„ćƒ¼ćƒ«\n", "# ćƒ„ćƒ¼ćƒ« {#tools}\n").replace( @@ -831,7 +842,7 @@ def test_removed_english_section_is_reassembled_with_no_client_but_an_edited_one write(root / "docs" / "tools.md", TOOLS.split("## Your first tool")[0].rstrip("\n") + "\n") write(root / "docs" / "index.md", INDEX.replace("Welcome to MCP.", "Welcome!")) - code, out, err = run(capsys, root, "translate", "--lang", "ja") + code, out, err = translate(capsys, root, "--lang", "ja") assert (code, out, err) == snapshot((2, "", "translations: no API credentials: set ANTHROPIC_API_KEY\n")) assert run(capsys, root, "status", "--lang", "ja")[1] == snapshot("""\ @@ -841,10 +852,10 @@ ja (ę—„ęœ¬čŖž): 0 missing, 2 outdated, 2 current, 0 removable """) fake = FakeTranslator([INDEX_JA.replace("MCP ćøć‚ˆć†ć“ćć€‚", "ć‚ˆć†ć“ćļ¼")]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[:2]) == snapshot( - (0, "", [], ["translated: index.md (1 of 2 sections)", "translated: tools.md (0 of 1 sections)"]) + assert (code, err, fake.replies, out.split("\n")[1:3]) == snapshot( + (0, "", [], ["ja: translated index.md (1 of 2 sections)", "ja: translated tools.md (0 of 1 sections)"]) ) @@ -858,19 +869,20 @@ def test_a_failing_page_does_not_stop_the_run_and_the_exit_code_is_1( truncated = t.Completion(TOOLS_JA[:40], t.Usage(10, 64_000, 0, 0), "max_tokens") fake = FakeTranslator([t.PageError("API request failed: overloaded"), truncated, refusal, NOTICES_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, out, err) == snapshot( ( 1, """\ -translated: i18n/notices.md (4 of 4 sections) +translating 4 pages (ja), 1 at a time, with test-model +ja: translated i18n/notices.md (4 of 4 sections) usage: 1020 input / 64400 output / 900 cache-write / 100 cache-read tokens """, """\ -error: index.md: API request failed: overloaded -error: tools.md: the reply was cut off at 64000 output tokens -error: translations.md: the model declined to translate this page +ja: error: index.md: API request failed: overloaded +ja: error: tools.md: the reply was cut off at 64000 output tokens +ja: error: translations.md: the model declined to translate this page """, ) ) @@ -883,12 +895,185 @@ def test_rejected_credentials_stop_the_run_with_exit_2(tmp_path: Path, capsys: p root = make_repo(tmp_path) fake = FakeTranslator([t.ConfigError("the API rejected the credentials: invalid x-api-key"), INDEX_JA]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, out, err) == snapshot((2, "", "translations: the API rejected the credentials: invalid x-api-key\n")) + assert (code, out, err) == snapshot( + ( + 2, + """\ +translating 4 pages (ja), 1 at a time, with test-model +usage: 0 input / 0 output / 0 cache-write / 0 cache-read tokens +""", + "translations: the API rejected the credentials: invalid x-api-key\n", + ) + ) assert fake.replies == [INDEX_JA] +def test_jobs_keeps_that_many_pages_in_flight_and_publishes_each_before_starting_another( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with `--jobs 2` over four pages, two requests are always waiting on the model together + (neither of a pair is answered until both wait, from two threads), never three, and by the time the + third and fourth requests are made an earlier page is already on disk: a finished page is written + when it lands, and only then does the next one start.""" + root = make_repo(tmp_path) + generated = root / "i18n" / "ja" / "pages" + replies = {"# Home": INDEX_JA, "# Tools": TOOLS_JA, "# Translations": TRANSLATIONS_JA, "# Notices": NOTICES_JA} + + class Pairs: + """Answers requests two at a time, noting how many were in flight and what was published by then.""" + + def __init__(self) -> None: + self.both, self.lock = threading.Barrier(2, timeout=5), threading.Lock() + self.in_flight = self.most = 0 + self.published: list[list[str]] = [] + + def complete(self, *, model: str, system: str, messages: Sequence[t.Message], max_tokens: int) -> t.Completion: + with self.lock: + self.in_flight += 1 + self.most = max(self.most, self.in_flight) + self.published.append(sorted(path.name for path in generated.glob("*.md"))) + try: + self.both.wait() + title = next(title for title in replies if title in messages[0].content) + return t.Completion(replies[title], t.Usage(10, 4, 0, 9)) + finally: + with self.lock: + self.in_flight -= 1 + + fake = Pairs() + + code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=fake) + + assert (code, err, fake.most) == (0, "", 2) + assert fake.published[:2] == [[], []] + assert all(fake.published[2:]) and len(fake.published) == 4 + assert sorted(out.splitlines()) == [ + "ja: translated i18n/notices.md (4 of 4 sections)", + "ja: translated index.md (2 of 2 sections)", + "ja: translated tools.md (3 of 3 sections)", + "ja: translated translations.md (1 of 1 sections)", + "translating 4 pages (ja), 2 at a time, with test-model", + "usage: 40 input / 16 output / 0 cache-write / 36 cache-read tokens", + ] + assert run(capsys, root, "status") == (0, "ja (ę—„ęœ¬čŖž): 0 missing, 0 outdated, 4 current, 0 removable\n", "") + + +class RejectedTogether: + """Lets two requests in together, then rejects the credentials of those whose page title is in `rejected`.""" + + def __init__(self, rejected: Sequence[str]) -> None: + self.rejected, self.calls = rejected, 0 + self.both, self.lock = threading.Barrier(2, timeout=5), threading.Lock() + + def complete(self, *, model: str, system: str, messages: Sequence[t.Message], max_tokens: int) -> t.Completion: + with self.lock: + self.calls += 1 + self.both.wait() + if any(title in messages[0].content for title in self.rejected): + raise t.ConfigError("the API rejected the credentials: invalid bearer token") + return t.Completion(TOOLS_JA, t.Usage(10, 4, 0, 9)) + + +def test_rejected_credentials_keep_a_page_that_lands_from_the_same_flight( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with two pages in flight together, the credentials being rejected for one does not throw + away the other: whichever lands first, the good page is written and usage reported before exit 2.""" + root = make_repo(tmp_path) + fake = RejectedTogether(["# Home"]) + + code, out, err = translate(capsys, root, "--lang", "ja", "--pages", "index.md", "tools.md", jobs=2, translator=fake) + + assert (code, fake.calls, err) == (2, 2, "translations: the API rejected the credentials: invalid bearer token\n") + assert out == snapshot("""\ +translating 2 pages (ja), 2 at a time, with test-model +ja: translated tools.md (3 of 3 sections) +usage: 10 input / 4 output / 0 cache-write / 9 cache-read tokens +""") + assert sorted(path.name for path in (root / "i18n" / "ja" / "pages").glob("*.md")) == ["tools.md"] + + +def test_rejected_credentials_with_pages_in_flight_start_no_further_page( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: with `--jobs 2` over four pages, once the credentials are rejected (here for both pages in + flight) the two remaining pages are never requested and nothing is written.""" + root = make_repo(tmp_path) + fake = RejectedTogether(["# Home", "# Tools"]) + + code, out, err = translate(capsys, root, "--lang", "ja", jobs=2, translator=fake) + + assert (code, fake.calls, err) == (2, 2, "translations: the API rejected the credentials: invalid bearer token\n") + assert out == snapshot("""\ +translating 4 pages (ja), 2 at a time, with test-model +usage: 0 input / 0 output / 0 cache-write / 0 cache-read tokens +""") + assert not (root / "i18n" / "ja" / "pages").exists() + + +def test_translate_without_lang_works_through_every_language_with_that_languages_own_prompt( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """Tool-defined: no `--lang` means every language in the registry, page by page across languages + (each language's first page before any second page), each page sent with its own language's system + prompt and reported under its code, with one usage total; `--lang` may be repeated and names a + language once however often it is given.""" + root = make_repo(tmp_path) + write( + root / "i18n" / "languages.yml", LANGUAGES + " - code: ko\n name: ķ•œźµ­ģ–“\n theme: ko\n hreflang: ko\n" + ) + write(root / "i18n" / "ko" / "instructions.md", "Korean rules.\n") + write(root / "i18n" / "ko" / "glossary.json", json.dumps({"keep": ["MCP"], "terms": []})) + fake = FakeTranslator( + [INDEX_JA, INDEX_JA, TOOLS_JA, TOOLS_JA, TRANSLATIONS_JA, TRANSLATIONS_JA, NOTICES_JA, NOTICES_JA] + ) + + code, out, err = translate(capsys, root, translator=fake) + + assert (code, err) == (0, "") + assert out == snapshot("""\ +translating 8 pages (ja, ko), 1 at a time, with test-model +ja: translated index.md (2 of 2 sections) +ko: translated index.md (2 of 2 sections) +ja: translated tools.md (3 of 3 sections) +ko: translated tools.md (3 of 3 sections) +ja: translated translations.md (1 of 1 sections) +ko: translated translations.md (1 of 1 sections) +ja: translated i18n/notices.md (4 of 4 sections) +ko: translated i18n/notices.md (4 of 4 sections) +usage: 8000 input / 3200 output / 7200 cache-write / 800 cache-read tokens +""") + assert [system.split("\n")[2] for system in fake.systems] == snapshot( + [ + "# Target language: ę—„ęœ¬čŖž (`ja`)", + "# Target language: ķ•œźµ­ģ–“ (`ko`)", + "# Target language: ę—„ęœ¬čŖž (`ja`)", + "# Target language: ķ•œźµ­ģ–“ (`ko`)", + "# Target language: ę—„ęœ¬čŖž (`ja`)", + "# Target language: ķ•œźµ­ģ–“ (`ko`)", + "# Target language: ę—„ęœ¬čŖž (`ja`)", + "# Target language: ķ•œźµ­ģ–“ (`ko`)", + ] + ) + assert fake.systems[1] == snapshot("""\ +General rules. + +# Target language: ķ•œźµ­ģ–“ (`ko`) + +Korean rules. + +## Glossary + +These terms always stay in English, spelled exactly like this: + +- MCP\ +""") + again = translate(capsys, root, "--lang", "ko", "--lang", "ja", "ko") + assert again == (0, "ko: nothing to translate\nja: nothing to translate\n", "") + + def test_repair_turn_feeds_the_findings_back_and_accepts_the_fixed_reply( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: @@ -898,7 +1083,7 @@ def test_repair_turn_feeds_the_findings_back_and_accepts_the_fixed_reply( broken = TOOLS_JA.replace("`async` 恫", "éžåŒęœŸć«").replace("!!! note", "!!! warning") fake = FakeTranslator([INDEX_JA, broken, TOOLS_JA, TRANSLATIONS_JA, NOTICES_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert [message.role for message in fake.conversations[2]] == ["user", "assistant", "user"] @@ -926,7 +1111,7 @@ def test_code_block_moved_into_another_section_is_repaired_not_published( root = make_repo(tmp_path) fake = FakeTranslator([FENCE_MOVED_JA, TOOLS_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "tools.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "tools.md", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert fake.conversations[1][2].content == snapshot("""\ @@ -954,9 +1139,9 @@ def test_code_block_moved_out_of_a_retranslated_section_gets_repair_turns_like_a moved = FENCE_MOVED_JA.replace("å¤±ę•—ć‚’ä¼ćˆć‚‹ć«ćÆä¾‹å¤–ć‚’é€å‡ŗć—ć¾ć™ć€‚", "失敗するには `ToolError` を送出します。") fake = FakeTranslator([moved, fixed]) - code, out, err = run(capsys, root, "translate", "--lang", "ja", translator=fake) + code, out, err = translate(capsys, root, "--lang", "ja", translator=fake) - assert (code, err, fake.replies, out.split("\n")[0]) == (0, "", [], "translated: tools.md (1 of 3 sections)") + assert (code, err, fake.replies, out.split("\n")[1]) == (0, "", [], "ja: translated tools.md (1 of 3 sections)") assert fake.conversations[1][2].content == snapshot("""\ Your translation broke the following structural rules. Fix each problem and return the full corrected page, changing nothing else: @@ -983,7 +1168,7 @@ def test_shortened_list_and_table_are_fed_back_for_repair_before_the_page_is_pub shortened = LISTED_JA.replace(" 1. å…„ć‚Œå­\n* 恕悓\n", "ļ¼ˆä»„äø‹åŒę§˜ļ¼‰\n").replace("| c | d |\n", "") fake = FakeTranslator([shortened, LISTED_JA]) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "translations.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "translations.md", translator=fake) assert (code, err, fake.replies) == (0, "", []) assert fake.conversations[1][2].content == snapshot("""\ @@ -1008,11 +1193,11 @@ def test_page_still_broken_after_two_repair_turns_fails_and_keeps_the_previous_t missing_fence = TOOLS_JA.replace('```python title="server.py"\n--8<-- "docs_src/server.py"\n```\n\n', "") fake = FakeTranslator([missing_fence] * 3) - code, _, err = run(capsys, root, "translate", "--lang", "ja", "--pages", "tools.md", translator=fake) + code, _, err = translate(capsys, root, "--lang", "ja", "--pages", "tools.md", translator=fake) assert (code, fake.replies) == (1, []) assert err == snapshot( - "error: tools.md: unfixed after 2 repairs: ## Your first tool: 0 code fences vs 1 in the English: keep each where it is, add none\n" + "ja: error: tools.md: unfixed after 2 repairs: ## Your first tool: 0 code fences vs 1 in the English: keep each where it is, add none\n" ) assert (root / "i18n" / "ja" / "pages" / "tools.md").read_text(encoding="utf-8") == before @@ -1058,7 +1243,7 @@ def test_stage_overlays_translations_injects_notices_and_rewrites_api_links( notice links so they hold under any path prefix, and assets ride along.""" root = make_repo(tmp_path) fake = FakeTranslator([INDEX_JA]) - assert run(capsys, root, "translate", "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 + assert translate(capsys, root, "--lang", "ja", "--pages", "index.md", translator=fake)[0] == 0 write(root / "docs" / "api" / "mcp" / "index.md", "# API stub\n") code, out, err = run(capsys, root, "stage", "--lang", "ja") @@ -1185,7 +1370,7 @@ def test_stage_keeps_showing_the_generated_code_block_after_its_english_changes_ assert (code, err, staged.split("\n")[2]) == (0, "", '!!! note "č‹±čŖžē‰ˆć‚ˆć‚Šå¤ć„ēæ»čØ³"') assert ('title="server.py"' in staged, 'title="app.py"' in staged) == (True, False) - assert run(capsys, root, "translate", "--lang", "ja", translator=FakeTranslator([TOOLS_JA]))[0] == 0 + assert translate(capsys, root, "--lang", "ja", translator=FakeTranslator([TOOLS_JA]))[0] == 0 code, _, err = run(capsys, root, "stage", "--lang", "ja") staged = staged_page(root, "tools.md") diff --git a/tests/docs_src/test_apps.py b/tests/docs_src/test_apps.py index e91d12a3..b82b9c03 100644 --- a/tests/docs_src/test_apps.py +++ b/tests/docs_src/test_apps.py @@ -5,9 +5,8 @@ from typing import Any import pytest from mcp_types import TextContent, TextResourceContents -from docs_src.apps import tutorial001, tutorial002, tutorial003 +from docs_src.apps import tutorial001, tutorial001_client, tutorial002, tutorial003 from mcp import Client -from mcp.client import advertise from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID # See test_index.py for why this is a per-module mark and not a conftest hook. @@ -33,11 +32,10 @@ async def test_the_ui_resource_is_served_as_the_app_mime_type() -> None: async def test_one_tool_two_answers() -> None: - """tutorial001: the canonical degradation pattern: raw data for a client that - negotiated Apps, a human sentence for one that did not.""" - async with Client( - tutorial001.mcp, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})] - ) as ui_client: + """tutorial001_client's `APPS_SUPPORT` declaration, driven in-process against + tutorial001's server: the client that negotiated Apps (with the required + `mimeTypes`) gets raw data, one that did not gets the human sentence.""" + async with Client(tutorial001.mcp, extensions=[tutorial001_client.APPS_SUPPORT]) as ui_client: rich = await ui_client.call_tool("get_time", {}) async with Client(tutorial001.mcp) as text_client: plain = await text_client.call_tool("get_time", {}) @@ -45,13 +43,6 @@ async def test_one_tool_two_answers() -> None: assert plain.content == [TextContent(type="text", text="The time is 2026-06-26T12:00:00Z.")] -async def test_the_clock_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial001: `main()` declares Apps support with the required `mimeTypes` and - receives the rich answer the page promises.""" - await tutorial001.main() - assert "2026-06-26T12:00:00Z" in capsys.readouterr().out - - async def test_capability_advertised_under_server_extensions() -> None: """tutorial001: passing `extensions=[apps]` advertises `io.modelcontextprotocol/ui`.""" async with Client(tutorial001.mcp) as client: diff --git a/tests/docs_src/test_asgi.py b/tests/docs_src/test_asgi.py index ef237bc2..d241dc71 100644 --- a/tests/docs_src/test_asgi.py +++ b/tests/docs_src/test_asgi.py @@ -13,7 +13,7 @@ from starlette.routing import Mount, Route from docs_src.asgi import tutorial001, tutorial002, tutorial003, tutorial004, tutorial005, tutorial006 from mcp import Client -from mcp.server import MCPServer +from mcp.server import MCPServer, Server # See test_index.py for why this is a per-module mark and not a conftest hook. pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")] @@ -44,6 +44,8 @@ async def test_streamable_http_app_takes_runs_options_except_port() -> None: "event_store", "retry_interval", "max_request_body_size", + "session_idle_timeout", + "max_sessions", "transport_security", "host", } @@ -68,6 +70,18 @@ async def test_streamable_http_app_applies_the_configured_request_body_limit() - assert response.status_code == 413 +async def test_streamable_http_app_applies_the_configured_session_limits() -> None: + """The documented `session_idle_timeout` and `max_sessions` options reach the session manager, from + both the high-level and the low-level factory.""" + server = MCPServer("Notes") + server.streamable_http_app(session_idle_timeout=5, max_sessions=7) + assert (server.session_manager.session_idle_timeout, server.session_manager.max_sessions) == (5, 7) + + lowlevel = Server("Notes") + lowlevel.streamable_http_app(session_idle_timeout=None, max_sessions=None) + assert (lowlevel.session_manager.session_idle_timeout, lowlevel.session_manager.max_sessions) == (None, None) + + async def test_mounting_at_the_root_keeps_the_default_path() -> None: """tutorial002: `Mount("/")` plus the default `streamable_http_path` leaves the endpoint at `/mcp`.""" (mount,) = tutorial002.app.routes diff --git a/tests/docs_src/test_caching.py b/tests/docs_src/test_caching.py index 751ca861..9a697fa2 100644 --- a/tests/docs_src/test_caching.py +++ b/tests/docs_src/test_caching.py @@ -64,9 +64,13 @@ async def test_the_handler_value_wins_over_the_map_per_field() -> None: async def test_the_client_program_on_the_page_makes_three_fetches_for_four_calls( capsys: pytest.CaptureFixture[str], ) -> None: - """tutorial003: a cache hit, an expiry, and `cache_mode="refresh"` make four calls cost three fetches.""" - await tutorial003.main() - assert capsys.readouterr().out == "4 calls, 3 fetches\n" + """tutorial003's four calls, driven in-process against tutorial002's server with the demo's own + clock: a cache hit, an expiry, and `cache_mode="refresh"` make them cost three fetches, each one + a line the server printed. The first result carries the handler's TTL and the map's scope.""" + async with Client(tutorial002.server, cache=CacheConfig(clock=lambda: tutorial003.clock.now)) as client: + tools = await tutorial003.run(client) + assert (tools.ttl_ms, tools.cache_scope) == (1_000, "public") + assert capsys.readouterr().out == "tools/list served\n" * 3 def _counting_tools_server(*, ttl_ms: int | None = 60_000) -> tuple[Server[Any], list[str | None]]: diff --git a/tests/docs_src/test_client.py b/tests/docs_src/test_client.py index d07ee4c9..7121e98d 100644 --- a/tests/docs_src/test_client.py +++ b/tests/docs_src/test_client.py @@ -1,10 +1,14 @@ -"""`docs/client/index.md`: every claim the page makes, proved against the real SDK.""" +"""`docs/client/index.md`: every claim the page makes, proved against the real SDK. + +`tutorial001` is the page's `server.py`. The other snippets are `client.py` programs that reach it +by URL, so each test drives the same calls in-process against `tutorial001.mcp` instead. +""" import pytest from inline_snapshot import snapshot from mcp_types import Prompt, PromptArgument, PromptReference, TextContent, TextResourceContents, Tool -from docs_src.client import tutorial001, tutorial002, tutorial003, tutorial004, tutorial005, tutorial006, tutorial007 +from docs_src.client import tutorial001, tutorial007 from mcp import Client, MCPDeprecationWarning, MCPError from mcp.shared.metadata_utils import get_display_name @@ -12,31 +16,23 @@ from mcp.shared.metadata_utils import get_display_name pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDeprecationWarning")] -async def test_every_client_program_on_the_page_runs(capsys: pytest.CaptureFixture[str]) -> None: - """Each `main()` is the literal client program shown on the page; all seven run clean in-memory.""" - await tutorial001.main() - await tutorial002.main() - await tutorial003.main() - await tutorial004.main() - await tutorial005.main() - await tutorial006.main() - await tutorial007.main() - assert "Bookshop" in capsys.readouterr().out - - async def test_connected_properties_are_populated_inside_the_block() -> None: - """tutorial001: server_info, server_capabilities, protocol_version and instructions are just there.""" + """tutorial001_client's four prints, in-process against server.py: server_info, capabilities, protocol_version + and instructions are just there, and a capability the server lacks is None.""" async with Client(tutorial001.mcp) as client: assert client.server_info is not None assert client.server_info.name == "Bookshop" assert client.protocol_version == "2026-07-28" assert client.instructions == "Search the catalog before recommending a book." assert client.server_capabilities.tools is not None + assert client.server_capabilities.resources is not None + assert client.server_capabilities.prompts is not None + assert client.server_capabilities.completions is not None assert client.server_capabilities.logging is None async def test_a_client_is_not_reusable_after_the_block_ends() -> None: - """tutorial001: `async with` is the whole lifecycle. Construct a new Client per connection.""" + """The lifecycle bullet: `async with` is the whole lifecycle. Construct a new Client per connection.""" client = Client(tutorial001.mcp) async with client: assert client.server_info is not None @@ -46,13 +42,14 @@ async def test_a_client_is_not_reusable_after_the_block_ends() -> None: async def test_list_tools_returns_the_full_definition() -> None: - """tutorial002: each listed tool carries its name, title, description and the derived input schema.""" - async with Client(tutorial002.mcp) as client: - (tool,) = (await client.list_tools()).tools - assert tool.name == "search_books" - assert tool.title == "Search the catalog" - assert tool.description == "Search the catalog by title or author." - assert tool.input_schema == snapshot( + """tutorial002's listing, in-process: each tool carries its name, title, description and the derived + input schema, and a tool registered without `title=` lists with title None.""" + async with Client(tutorial001.mcp) as client: + (search, lookup) = (await client.list_tools()).tools + assert search.name == "search_books" + assert search.title == "Search the catalog" + assert search.description == "Search the catalog by title or author." + assert search.input_schema == snapshot( { "type": "object", "properties": { @@ -63,6 +60,8 @@ async def test_list_tools_returns_the_full_definition() -> None: "title": "search_booksArguments", } ) + assert lookup.name == "lookup_book" + assert lookup.title is None def test_get_display_name_prefers_the_title() -> None: @@ -74,8 +73,8 @@ def test_get_display_name_prefers_the_title() -> None: async def test_call_tool_result_has_three_things_to_read() -> None: - """tutorial003: content for the model, structured_content for code, is_error for both.""" - async with Client(tutorial003.mcp) as client: + """tutorial003's call, in-process: content for the model, structured_content for code, is_error for both.""" + async with Client(tutorial001.mcp) as client: result = await client.call_tool("lookup_book", {"title": "Dune"}) assert not result.is_error (block,) = result.content @@ -85,8 +84,8 @@ async def test_call_tool_result_has_three_things_to_read() -> None: async def test_a_raising_tool_is_a_result_not_an_exception() -> None: - """tutorial003 `!!! check`: the ToolError's message comes back in content with is_error=True.""" - async with Client(tutorial003.mcp) as client: + """The `!!! check`: the ToolError's message comes back in content with is_error=True.""" + async with Client(tutorial001.mcp) as client: result = await client.call_tool("lookup_book", {"title": "Solaris"}) assert result.is_error (block,) = result.content @@ -97,7 +96,7 @@ async def test_a_raising_tool_is_a_result_not_an_exception() -> None: async def test_an_unknown_tool_name_is_a_result_not_an_exception() -> None: """The `!!! warning`: a tool the server doesn't have comes back as is_error=True, not as MCPError.""" - async with Client(tutorial003.mcp) as client: + async with Client(tutorial001.mcp) as client: result = await client.call_tool("does_not_exist", {}) assert result.is_error (block,) = result.content @@ -107,8 +106,9 @@ async def test_an_unknown_tool_name_is_a_result_not_an_exception() -> None: async def test_resources_and_templates_are_two_separate_lists() -> None: - """tutorial004: concrete resources and parameterised templates come back from different verbs.""" - async with Client(tutorial004.mcp) as client: + """tutorial004's two listings, in-process: concrete resources and parameterised templates come back + from different verbs.""" + async with Client(tutorial001.mcp) as client: (resource,) = (await client.list_resources()).resources assert resource.uri == "catalog://genres" (template,) = (await client.list_resource_templates()).resource_templates @@ -116,8 +116,9 @@ async def test_resources_and_templates_are_two_separate_lists() -> None: async def test_read_resource_fills_in_a_template() -> None: - """tutorial004: read_resource takes a plain str URI; narrow the contents with isinstance.""" - async with Client(tutorial004.mcp) as client: + """tutorial004's read, in-process: read_resource takes a plain str URI, the server matches it to the + template, and the contents narrow with isinstance.""" + async with Client(tutorial001.mcp) as client: (contents,) = (await client.read_resource("catalog://genres/poetry")).contents assert isinstance(contents, TextResourceContents) assert contents.text == "3 books filed under poetry." @@ -126,7 +127,7 @@ async def test_read_resource_fills_in_a_template() -> None: async def test_resource_subscriptions_are_listen_based_on_the_modern_wire() -> None: """The Resources section: at 2026-07-28 `resources.subscribe` is True (served via subscriptions/listen) while the legacy subscribe_resource verb answers -32601.""" - async with Client(tutorial004.mcp) as client: + async with Client(tutorial001.mcp) as client: assert client.server_capabilities.resources is not None assert client.server_capabilities.resources.subscribe is True with pytest.raises(MCPError) as exc_info: @@ -138,8 +139,8 @@ async def test_resource_subscriptions_are_listen_based_on_the_modern_wire() -> N async def test_list_prompts_describes_the_arguments() -> None: - """tutorial005: a listed prompt carries its name, title and the arguments it needs.""" - async with Client(tutorial005.mcp) as client: + """tutorial005's listing, in-process: a listed prompt carries its name, title and the arguments it needs.""" + async with Client(tutorial001.mcp) as client: (prompt,) = (await client.list_prompts()).prompts assert prompt == snapshot( Prompt( @@ -152,8 +153,8 @@ async def test_list_prompts_describes_the_arguments() -> None: async def test_get_prompt_renders_the_messages() -> None: - """tutorial005: get_prompt returns the rendered messages a host hands to the model.""" - async with Client(tutorial005.mcp) as client: + """tutorial005's render, in-process: get_prompt returns the messages a host hands to the model.""" + async with Client(tutorial001.mcp) as client: result = await client.get_prompt("recommend", {"genre": "poetry"}) (message,) = result.messages assert message.role == "user" @@ -163,8 +164,9 @@ async def test_get_prompt_renders_the_messages() -> None: async def test_complete_suggests_values_for_an_argument() -> None: - """tutorial006: complete takes a ref and a name/value pair and returns the matching values.""" - async with Client(tutorial006.mcp) as client: + """tutorial006's call, in-process: complete takes a ref and a name/value pair and returns the matching + values.""" + async with Client(tutorial001.mcp) as client: result = await client.complete( ref=PromptReference(type="ref/prompt", name="recommend"), argument={"name": "genre", "value": "p"}, @@ -172,16 +174,17 @@ async def test_complete_suggests_values_for_an_argument() -> None: assert result.completion.values == ["poetry"] -async def test_a_single_page_server_ends_the_pagination_loop_immediately() -> None: - """tutorial007: every list_* takes cursor=; next_cursor is None when there is nothing left.""" - async with Client(tutorial007.mcp) as client: - page = await client.list_tools(cursor=None) - assert page.next_cursor is None - assert [tool.name for tool in page.tools] == ["search_books", "reserve_book"] +async def test_the_pagination_loop_collects_every_tool_from_a_single_page_server() -> None: + """tutorial007's `list_all_tools`, driven in-process against server.py: MCPServer answers in one page, + so the loop ends on the first `next_cursor is None` with every tool collected.""" + async with Client(tutorial001.mcp) as client: + tools = await tutorial007.list_all_tools(client) + assert [tool.name for tool in tools] == ["search_books", "lookup_book"] + assert (await client.list_tools(cursor=None)).next_cursor is None async def test_raise_exceptions_is_a_constructor_flag() -> None: """The `## In tests` section: `raise_exceptions=True` is accepted by the in-memory Client.""" async with Client(tutorial001.mcp, raise_exceptions=True) as client: result = await client.call_tool("search_books", {"query": "dune"}) - assert result.structured_content == {"result": "Found 3 books matching 'dune'."} + assert result.structured_content == {"result": "Found 3 books matching 'dune' (showing up to 10)."} diff --git a/tests/docs_src/test_extensions.py b/tests/docs_src/test_extensions.py index 2a141337..b0c7d2e0 100644 --- a/tests/docs_src/test_extensions.py +++ b/tests/docs_src/test_extensions.py @@ -11,9 +11,12 @@ from docs_src.extensions import ( tutorial002, tutorial003, tutorial004, + tutorial004_client, tutorial005, tutorial006, + tutorial006_client, tutorial007, + tutorial007_client, ) from mcp import Client, MCPError from mcp.client import advertise @@ -42,13 +45,15 @@ def test_a_prefixless_identifier_fails_at_class_definition() -> None: async def test_extension_settings_advertised_under_capabilities() -> None: - """tutorial003: `settings()` becomes the entry at `capabilities.extensions[identifier]`.""" + """tutorial003: `settings()` becomes the entry at `capabilities.extensions[identifier]`, + which is the first line tutorial003_client prints.""" async with Client(tutorial003.mcp) as client: assert client.server_capabilities.extensions == {"com.example/stamps": {"sealed": True}} async def test_contributed_tool_is_listed_and_callable() -> None: - """tutorial003: a `ToolBinding` registers like any `add_tool` call: listed and callable.""" + """tutorial003: a `ToolBinding` registers like any `add_tool` call: listed and callable, + with the content tutorial003_client prints.""" async with Client(tutorial003.mcp) as client: listed = await client.list_tools() assert [tool.name for tool in listed.tools] == ["stamp"] @@ -56,28 +61,23 @@ async def test_contributed_tool_is_listed_and_callable() -> None: assert result.content == [TextContent(type="text", text="[stamped] hello")] -async def test_the_stamps_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial003: `main()` is the literal client program on the page; both printed - lines match the page's comments.""" - await tutorial003.main() - out = capsys.readouterr().out - assert "{'com.example/stamps': {'sealed': True}}" in out - assert "[stamped] hello" in out - - -async def test_the_search_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial004: `main()` declares the extension and gets the vendor method's result.""" - await tutorial004.main() - assert "['mcp-0', 'mcp-1', 'mcp-2']" in capsys.readouterr().out +async def test_declaring_client_gets_the_vendor_method_result() -> None: + """tutorial004_client's request against tutorial004's server, driven in-process: a client + that advertises the extension gets the vendor method's typed result, and the client's + own copy of the wire types agrees with the server's.""" + async with Client(tutorial004.mcp, extensions=[advertise(tutorial004_client.EXTENSION_ID)]) as client: + request = tutorial004_client.SearchRequest(params=tutorial004_client.SearchParams(query="mcp", limit=3)) + result = await client.session.send_request(request, tutorial004_client.SearchResult) + assert result.items == ["mcp-0", "mcp-1", "mcp-2"] async def test_vendor_method_rejects_a_non_declaring_client_with_32021() -> None: """tutorial004: `require_client_extension` answers a non-declaring client with `-32021` and the machine-readable `requiredCapabilities` payload.""" async with Client(tutorial004.mcp) as client: - request = tutorial004.SearchRequest(params=tutorial004.SearchParams(query="mcp")) + request = tutorial004_client.SearchRequest(params=tutorial004_client.SearchParams(query="mcp")) with pytest.raises(MCPError) as exc_info: - await client.session.send_request(request, tutorial004.SearchResult) + await client.session.send_request(request, tutorial004_client.SearchResult) assert exc_info.value.code == MISSING_REQUIRED_CLIENT_CAPABILITY assert exc_info.value.error.data == {"requiredCapabilities": {"extensions": {"com.example/search": {}}}} @@ -85,10 +85,12 @@ async def test_vendor_method_rejects_a_non_declaring_client_with_32021() -> None async def test_version_pinned_method_is_not_found_on_a_legacy_connection() -> None: """tutorial004: `protocol_versions={"2026-07-28"}` makes the method METHOD_NOT_FOUND at any other wire version; for a legacy client it doesn't exist.""" - async with Client(tutorial004.mcp, mode="legacy", extensions=[advertise(tutorial004.EXTENSION_ID)]) as client: - request = tutorial004.SearchRequest(params=tutorial004.SearchParams(query="mcp")) + async with Client( + tutorial004.mcp, mode="legacy", extensions=[advertise(tutorial004_client.EXTENSION_ID)] + ) as client: + request = tutorial004_client.SearchRequest(params=tutorial004_client.SearchParams(query="mcp")) with pytest.raises(MCPError) as exc_info: - await client.session.send_request(request, tutorial004.SearchResult) + await client.session.send_request(request, tutorial004_client.SearchResult) assert exc_info.value.code == METHOD_NOT_FOUND @@ -104,10 +106,12 @@ async def test_interceptor_observes_the_call_and_passes_the_result_through( assert messages == ["tool 'add' called"] -async def test_the_receipts_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial006: `main()` runs as printed and the output is the redeemed result, never the claimed shape.""" - await tutorial006.main() - assert "goods for r-117" in capsys.readouterr().out +async def test_declaring_client_receives_the_redeemed_result_not_the_claimed_shape() -> None: + """tutorial006_client's `Receipts` against tutorial006's server, driven in-process: + `call_tool("buy")` returns what the resolver redeemed, never the claimed receipt shape.""" + async with Client(tutorial006.mcp, extensions=[tutorial006_client.Receipts()]) as client: + result = await client.call_tool("buy", {"item": "lamp"}) + assert result.content == [TextContent(type="text", text="goods for r-117")] async def test_a_client_without_the_extension_is_refused_by_the_gate() -> None: @@ -120,13 +124,17 @@ async def test_a_client_without_the_extension_is_refused_by_the_gate() -> None: async def test_session_tier_allow_claimed_returns_the_raw_shape() -> None: """The page's escape hatch: `allow_claimed=True` returns the parsed claim model, not the resolved result.""" - async with Client(tutorial006.mcp, extensions=[tutorial006.Receipts()]) as client: + async with Client(tutorial006.mcp, extensions=[tutorial006_client.Receipts()]) as client: result = await client.session.call_tool("buy", {"item": "lamp"}, allow_claimed=True) - assert isinstance(result, tutorial006.ReceiptResult) + assert isinstance(result, tutorial006_client.ReceiptResult) assert result.receipt_token == "r-117" -async def test_the_jobs_client_program_runs_as_shown(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial007: a vendor request with `name_param` round-trips `send_request` with no registration.""" - await tutorial007.main() - assert "job-7 is running" in capsys.readouterr().out +async def test_name_param_request_round_trips_with_no_client_registration() -> None: + """tutorial007_client's `JobStatusRequest` against tutorial007's server, driven in-process: + a vendor request declaring `name_param` round-trips `send_request` with no client-side + registration.""" + async with Client(tutorial007.mcp, extensions=[advertise(tutorial007_client.EXTENSION_ID)]) as client: + request = tutorial007_client.JobStatusRequest(params=tutorial007_client.JobParams(job_id="job-7")) + result = await client.session.send_request(request, tutorial007_client.JobStatus) + assert result.status == "job-7 is running" diff --git a/tests/docs_src/test_identity_assertion.py b/tests/docs_src/test_identity_assertion.py index 1d69c5eb..3a15ef94 100644 --- a/tests/docs_src/test_identity_assertion.py +++ b/tests/docs_src/test_identity_assertion.py @@ -151,6 +151,7 @@ async def test_the_whole_grant_is_one_token_request() -> None: issuer_url=AnyHttpUrl(tutorial002.ISSUER), resource_server_url=AnyHttpUrl(MCP_SERVER_URL), required_scopes=["notes:read"], + validate_token_resource=True, ), ) diff --git a/tests/docs_src/test_legacy_clients.py b/tests/docs_src/test_legacy_clients.py index 3018e4fc..90daf1bd 100644 --- a/tests/docs_src/test_legacy_clients.py +++ b/tests/docs_src/test_legacy_clients.py @@ -6,7 +6,7 @@ import httpx2 import pytest from mcp_types import INVALID_REQUEST, ResourceUpdatedNotification, TextContent -from docs_src.legacy_clients import tutorial001, tutorial002, tutorial003 +from docs_src.legacy_clients import tutorial001, tutorial001_client, tutorial002, tutorial003 from mcp import Client, MCPError from mcp.client.streamable_http import streamable_http_client from mcp.server import MCPServer @@ -25,14 +25,21 @@ MCP_HEADERS = {"Accept": "application/json, text/event-stream", "Content-Type": URL = "http://localhost:8000/mcp" -async def test_one_resolve_tool_serves_a_legacy_and_a_modern_client_at_once( - capsys: pytest.CaptureFixture[str], -) -> None: - """tutorial001's `main()`, exactly as the page renders it: two eras of client, one server, one answer.""" - await tutorial001.main() - assert capsys.readouterr().out == ( - """2025-11-25 {'result': "Reserved 2 of 'Dune'."}\n2026-07-28 {'result': "Reserved 2 of 'Dune'."}\n""" - ) +async def test_one_resolve_tool_serves_a_legacy_and_a_modern_client_at_once() -> None: + """tutorial001_client's flow, driven in-process against tutorial001's server: two eras of client open at + once with the page's `answer` callback, one `Resolve` tool, and the two lines the page prints.""" + lines: list[tuple[str, object]] = [] + async with ( + Client(tutorial001.mcp, mode="legacy", elicitation_callback=tutorial001_client.answer) as legacy, + Client(tutorial001.mcp, elicitation_callback=tutorial001_client.answer) as modern, + ): + for client in (legacy, modern): + result = await client.call_tool("reserve", {"title": "Dune"}) + lines.append((client.protocol_version, result.structured_content)) + assert lines == [ + ("2025-11-25", {"result": "Reserved 2 of 'Dune'."}), + ("2026-07-28", {"result": "Reserved 2 of 'Dune'."}), + ] async def test_neither_era_of_client_sees_the_resolved_parameter() -> None: @@ -53,6 +60,8 @@ def test_streamable_http_app_has_no_era_knob() -> None: "event_store", "retry_interval", "max_request_body_size", + "session_idle_timeout", + "max_sessions", "transport_security", "host", } @@ -76,6 +85,20 @@ async def test_a_legacy_session_is_minted_in_process_and_a_stray_session_id_is_a assert stray.status_code == 404 +def test_legacy_sessions_expire_and_are_capped_by_default() -> None: + """The session lifetime section: a session is closed after 30 idle minutes and each worker process + holds at most 10 000 of them, unless `run()` / `streamable_http_app()` say otherwise.""" + server = MCPServer("Bookshop") + server.streamable_http_app() + assert server.session_manager.session_idle_timeout == 30 * 60 + assert server.session_manager.max_sessions == 10_000 + + server = MCPServer("Bookshop") + server.streamable_http_app(session_idle_timeout=None, max_sessions=None) + assert server.session_manager.session_idle_timeout is None + assert server.session_manager.max_sessions is None + + async def test_stateless_http_never_mints_a_session() -> None: """The `stateless_http=True` section: the same legacy `initialize` no longer gets an `Mcp-Session-Id`.""" app = MCPServer("Bookshop").streamable_http_app(stateless_http=True) @@ -98,13 +121,13 @@ async def test_stateless_http_kills_the_legacy_back_channel_and_only_the_legacy_ httpx2.AsyncClient(transport=transport) as http, ): modern_target = streamable_http_client(URL, http_client=http) - async with Client(modern_target, elicitation_callback=tutorial001.answer) as modern: + async with Client(modern_target, elicitation_callback=tutorial001_client.answer) as modern: assert modern.protocol_version == "2026-07-28" result = await modern.call_tool("reserve", {"title": "Dune"}) assert result.content == [TextContent(type="text", text="Reserved 2 of 'Dune'.")] legacy_target = streamable_http_client(URL, http_client=http) - async with Client(legacy_target, mode="legacy", elicitation_callback=tutorial001.answer) as legacy: + async with Client(legacy_target, mode="legacy", elicitation_callback=tutorial001_client.answer) as legacy: assert legacy.protocol_version == "2025-11-25" with pytest.raises(MCPError) as exc_info: # pragma: no branch await legacy.call_tool("reserve", {"title": "Dune"}) diff --git a/tests/docs_src/test_lowlevel.py b/tests/docs_src/test_lowlevel.py index 7d58e941..8a89aa01 100644 --- a/tests/docs_src/test_lowlevel.py +++ b/tests/docs_src/test_lowlevel.py @@ -11,6 +11,7 @@ from mcp_types import ( RequestParams, TextContent, ) +from starlette.routing import Route from docs_src.lowlevel import tutorial001, tutorial002, tutorial003, tutorial004, tutorial005, tutorial006 from mcp import Client, MCPError @@ -45,6 +46,13 @@ async def test_the_client_does_not_care_which_server_class_it_connects_to() -> N assert result.structured_content is None +def test_the_last_line_is_an_asgi_app_uvicorn_can_serve() -> None: + """tutorial001: the Try-it's `uvicorn server:app` target is a Starlette app with one route at `/mcp`.""" + (route,) = tutorial001.app.routes + assert isinstance(route, Route) + assert route.path == "/mcp" + + async def test_only_the_handlers_you_passed_become_capabilities() -> None: """tutorial001: two tool handlers advertise `tools` and nothing else.""" async with Client(tutorial001.server) as client: diff --git a/tests/docs_src/test_oauth_clients.py b/tests/docs_src/test_oauth_clients.py index 801fbb2c..db876139 100644 --- a/tests/docs_src/test_oauth_clients.py +++ b/tests/docs_src/test_oauth_clients.py @@ -9,6 +9,7 @@ from pydantic import AnyUrl, ValidationError from docs_src.oauth_clients import tutorial001, tutorial002 from mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthRegistrationError, OAuthTokenError, TokenStorage from mcp.client.auth.extensions.client_credentials import ( + ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider, static_assertion_provider, ) @@ -80,6 +81,14 @@ async def test_client_credentials_provider_builds_its_own_metadata() -> None: assert metadata.scope == "user" +@pytest.mark.parametrize("provider_class", [ClientCredentialsOAuthProvider, PrivateKeyJWTOAuthProvider]) +async def test_issuer_is_an_optional_keyword_on_both_machine_to_machine_providers(provider_class: type) -> None: + """tutorial002 passes `issuer=`; the page says leaving it out is allowed, on either provider.""" + issuer = inspect.signature(provider_class.__init__).parameters["issuer"] + assert issuer.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD + assert issuer.default is None + + async def test_the_two_remaining_keyword_arguments_have_defaults() -> None: """The page names `client_metadata_url` and `validate_resource_url` as the remainder.""" parameters = inspect.signature(OAuthClientProvider.__init__).parameters @@ -96,6 +105,7 @@ async def test_the_one_more_provider_is_private_key_jwt() -> None: storage=tutorial002.InMemoryTokenStorage(), client_id="reporting-agent", assertion_provider=static_assertion_provider("a.prebuilt.jwt"), + issuer="http://localhost:9000", ) assert isinstance(provider, OAuthClientProvider) assert isinstance(provider, httpx2.Auth) diff --git a/tests/docs_src/test_pagination.py b/tests/docs_src/test_pagination.py index ab5949df..e1c9eb9c 100644 --- a/tests/docs_src/test_pagination.py +++ b/tests/docs_src/test_pagination.py @@ -1,7 +1,6 @@ """`docs/advanced/pagination.md`: every claim the page makes, proved against the real SDK.""" import pytest -from mcp_types import Resource from docs_src.pagination import tutorial001, tutorial002 from mcp import Client, MCPError @@ -48,27 +47,20 @@ async def test_the_last_page_carries_no_cursor() -> None: assert page.next_cursor is None -async def test_the_loop_collects_all_one_hundred() -> None: - """tutorial001: the `cursor=` loop visits ten pages and reassembles the whole catalog.""" +async def test_the_client_loop_collects_all_one_hundred_in_order() -> None: + """tutorial002's `list_all_resources()`, driven in-process against tutorial001's server: the `cursor=` loop + stitches the pages back into the whole catalog, in order, with no gaps and no repeats.""" async with Client(tutorial001.server) as client: - resources: list[Resource] = [] - cursor: str | None = None - pages = 0 - while True: - page = await client.list_resources(cursor=cursor) - resources.extend(page.resources) - pages += 1 - if page.next_cursor is None: - break - cursor = page.next_cursor - assert pages == 10 - assert len({resource.uri for resource in resources}) == 100 + resources = await tutorial002.list_all_resources(client) + assert [resource.name for resource in resources] == tutorial001.BOOKS -async def test_the_client_program_on_the_page_runs(capsys: pytest.CaptureFixture[str]) -> None: - """tutorial002: `main()` is the literal client program on the page and prints the stitched total.""" - await tutorial002.main() - assert capsys.readouterr().out == "100 resources\n" +async def test_the_client_loop_runs_once_against_a_server_that_does_not_page() -> None: + """tutorial002's loop against the `MCPServer` above: `next_cursor` is `None` on the first response, so one + pass returns the whole catalog.""" + async with Client(mcp) as client: + resources = await tutorial002.list_all_resources(client) + assert [resource.name for resource in resources] == [f"book-{n}" for n in range(1, 101)] async def test_an_invented_cursor_is_an_error() -> None: diff --git a/tests/docs_src/test_progress.py b/tests/docs_src/test_progress.py index a05577fb..cfe07315 100644 --- a/tests/docs_src/test_progress.py +++ b/tests/docs_src/test_progress.py @@ -43,7 +43,7 @@ async def test_each_report_becomes_one_callback_invocation_in_order() -> None: async def test_over_a_wire_dispatcher_callbacks_race_the_result() -> None: - """The `!!! info`: only the in-memory connection runs the callback inline. + """The `!!! info`: only the in-process connection runs the callback inline. On a wire dispatcher (`mode="legacy"` here) each progress notification starts its own task, so `call_tool` can return while a slow callback is still running. The callbacks below block on an diff --git a/tests/docs_src/test_protocol_versions.py b/tests/docs_src/test_protocol_versions.py index 06b8a3a0..691b6cc2 100644 --- a/tests/docs_src/test_protocol_versions.py +++ b/tests/docs_src/test_protocol_versions.py @@ -1,11 +1,16 @@ -"""`docs/protocol-versions.md`: every claim the page makes, proved against the real SDK.""" +"""`docs/protocol-versions.md`: every claim the page makes, proved against the real SDK. + +The page's snippets are URL clients for the Bookshop `server.py` from The Client page +(`docs_src/client/tutorial001.py`). These tests open the same connections in-process +against that server object, so each `mode=` is exercised without a port. +""" import re import pytest from mcp_types import SERVER_INFO_META_KEY, DiscoverResult, Implementation, ServerCapabilities -from docs_src.protocol_versions import tutorial001, tutorial002, tutorial003, tutorial004 +from docs_src.client import tutorial001 as bookshop from mcp import Client # See test_index.py for why this is a per-module mark and not a conftest hook. @@ -13,8 +18,9 @@ pytestmark = [pytest.mark.anyio, pytest.mark.filterwarnings("error::mcp.MCPDepre async def test_auto_lands_on_the_modern_version() -> None: - """tutorial001: the default `mode="auto"` probes `server/discover` and adopts the result.""" - async with Client(tutorial001.mcp) as client: + """tutorial001's connection, in-process: the default `mode="auto"` probes `server/discover` + and adopts the result, so the page's `2026-07-28` output block is what it prints.""" + async with Client(bookshop.mcp) as client: assert client.protocol_version == "2026-07-28" assert client.server_info is not None assert client.server_info.name == "Bookshop" @@ -23,8 +29,9 @@ async def test_auto_lands_on_the_modern_version() -> None: async def test_legacy_forces_the_initialize_handshake() -> None: - """tutorial002: `mode="legacy"` runs `initialize` against the very same server.""" - async with Client(tutorial002.mcp, mode="legacy") as client: + """tutorial002's connection, in-process: `mode="legacy"` runs `initialize` against the very + same server and lands on the newest handshake-era version.""" + async with Client(bookshop.mcp, mode="legacy") as client: assert client.protocol_version == "2025-11-25" assert client.server_info is not None assert client.server_info.name == "Bookshop" @@ -33,14 +40,16 @@ async def test_legacy_forces_the_initialize_handshake() -> None: async def test_version_pin_sends_nothing_and_knows_nothing() -> None: - """tutorial003: a pin adopts the version locally; `server_info` is None and capabilities are blank.""" - async with Client(tutorial003.mcp, mode="2026-07-28") as client: + """tutorial003's connection, in-process: a pin adopts the version locally, so `server_info` + is None and every capability is blank, yet a tool call still round-trips.""" + async with Client(bookshop.mcp, mode="2026-07-28") as client: assert client.protocol_version == "2026-07-28" + assert client.session.initialize_result is None # The `!!! check` fence is the literal `print(client.server_info)` output: None. assert client.server_info is None assert client.server_capabilities == ServerCapabilities() result = await client.call_tool("search_books", {"query": "dune"}) - assert result.structured_content == {"result": "Found 3 books matching 'dune'."} + assert result.structured_content == {"result": "Found 3 books matching 'dune' (showing up to 10)."} def test_handshake_era_version_is_not_a_valid_pin() -> None: @@ -52,18 +61,20 @@ def test_handshake_era_version_is_not_a_valid_pin() -> None: "got '2025-06-18' ('2025-06-18' is a handshake-era version; use mode='legacy')" ), ): - Client(tutorial003.mcp, mode="2025-06-18") + Client(bookshop.mcp, mode="2025-06-18") async def test_prior_discover_round_trips() -> None: - """tutorial004: save `discover_result`, reconnect with it, and the identity comes back.""" - async with Client(tutorial004.mcp) as client: + """tutorial004's flow, driven in-process against the Bookshop server: save `discover_result`, + reconnect pinned with it, and the identity comes back without any negotiation.""" + async with Client(bookshop.mcp) as client: saved = client.session.discover_result assert saved is not None assert saved.supported_versions == ["2026-07-28"] - async with Client(tutorial004.mcp, mode="2026-07-28", prior_discover=saved) as client: + async with Client(bookshop.mcp, mode="2026-07-28", prior_discover=saved) as client: assert client.protocol_version == "2026-07-28" + assert client.session.discover_result is saved assert client.server_info is not None assert client.server_info.name == "Bookshop" assert client.server_capabilities.tools is not None @@ -71,14 +82,14 @@ async def test_prior_discover_round_trips() -> None: async def test_discover_result_survives_json() -> None: """`DiscoverResult` is a Pydantic model: dump it to JSON, validate it back, reconnect with it.""" - async with Client(tutorial004.mcp) as client: + async with Client(bookshop.mcp) as client: saved = client.session.discover_result assert saved is not None restored = DiscoverResult.model_validate_json(saved.model_dump_json()) assert restored == saved - async with Client(tutorial004.mcp, mode="2026-07-28", prior_discover=restored) as client: + async with Client(bookshop.mcp, mode="2026-07-28", prior_discover=restored) as client: assert client.server_info is not None assert client.server_info.name == "Bookshop" @@ -94,9 +105,9 @@ async def test_prior_discover_is_ignored_unless_mode_is_a_pin() -> None: ) }, ) - async with Client(tutorial004.mcp, prior_discover=stale) as client: + async with Client(bookshop.mcp, prior_discover=stale) as client: assert client.server_info is not None assert client.server_info.name == "Bookshop" - async with Client(tutorial004.mcp, mode="legacy", prior_discover=stale) as client: + async with Client(bookshop.mcp, mode="legacy", prior_discover=stale) as client: assert client.session.discover_result is None assert client.protocol_version == "2025-11-25" diff --git a/tests/interaction/_requirements.py b/tests/interaction/_requirements.py index 11bffba7..235fb65c 100644 --- a/tests/interaction/_requirements.py +++ b/tests/interaction/_requirements.py @@ -2848,11 +2848,11 @@ REQUIREMENTS: dict[str, Requirement] = { source=f"{SPEC_BASE_URL}/basic/authorization#access-token-usage", behavior="The resource server validates that the token audience matches its resource identifier.", transports=("streamable-http",), - note="Auth is enforced at the HTTP layer.", + note="Auth is enforced at the HTTP layer; the conformant tests enable AuthSettings.validate_token_resource.", divergence=Divergence( note=( - "BearerAuthBackend never inspects AccessToken.resource; a token issued for a different " - "resource is accepted. Spec MUST." + "Off by default: without AuthSettings.validate_token_resource the bearer gate does not compare " + "AccessToken.resource with resource_server_url and the check is the token verifier's. Spec MUST." ), ), ), diff --git a/tests/interaction/auth/_harness.py b/tests/interaction/auth/_harness.py index 856a1fe9..16b84b1d 100644 --- a/tests/interaction/auth/_harness.py +++ b/tests/interaction/auth/_harness.py @@ -182,6 +182,7 @@ def auth_settings( required_scopes: Sequence[str] = ("mcp",), valid_scopes: Sequence[str] | None = None, identity_assertion_enabled: bool = False, + validate_token_resource: bool = False, ) -> AuthSettings: """Build `AuthSettings` for the co-hosted authorization + resource server. @@ -194,7 +195,8 @@ def auth_settings( `identity_assertion_enabled` advertises and accepts the SEP-990 ID-JAG grant (RFC 7523 jwt-bearer); the provider must implement `exchange_identity_assertion` for the endpoint to - issue tokens. + issue tokens. `validate_token_resource` makes the bearer gate refuse tokens whose verifier + does not report them as issued for the resource URL. """ required = list(required_scopes) valid = list(valid_scopes) if valid_scopes is not None else required @@ -202,6 +204,7 @@ def auth_settings( issuer_url=AnyHttpUrl(BASE_URL), resource_server_url=AnyHttpUrl(f"{BASE_URL}/mcp"), required_scopes=required, + validate_token_resource=validate_token_resource, client_registration_options=ClientRegistrationOptions( enabled=True, valid_scopes=valid, default_scopes=required ), diff --git a/tests/interaction/auth/_provider.py b/tests/interaction/auth/_provider.py index 0c54d4fd..4e422947 100644 --- a/tests/interaction/auth/_provider.py +++ b/tests/interaction/auth/_provider.py @@ -157,6 +157,7 @@ class InMemoryAuthorizationServerProvider( token=refresh, client_id=client.client_id, scopes=authorization_code.scopes, + resource=authorization_code.resource, ) del self.codes[authorization_code.code] return OAuthToken( @@ -183,9 +184,11 @@ class InMemoryAuthorizationServerProvider( if self._fail_next_refresh: self._fail_next_refresh = False raise TokenError(error="invalid_grant", error_description="refresh denied by harness") - access = self.mint_access_token(client_id=client.client_id, scopes=scopes) + access = self.mint_access_token(client_id=client.client_id, scopes=scopes, resource=refresh_token.resource) new_refresh = f"refresh_{secrets.token_hex(16)}" - self.refresh_tokens[new_refresh] = RefreshToken(token=new_refresh, client_id=client.client_id, scopes=scopes) + self.refresh_tokens[new_refresh] = RefreshToken( + token=new_refresh, client_id=client.client_id, scopes=scopes, resource=refresh_token.resource + ) del self.refresh_tokens[refresh_token.token] return OAuthToken( access_token=access, diff --git a/tests/interaction/auth/test_bearer.py b/tests/interaction/auth/test_bearer.py index c70a27c5..aa42c50d 100644 --- a/tests/interaction/auth/test_bearer.py +++ b/tests/interaction/auth/test_bearer.py @@ -2,9 +2,9 @@ These tests mount only the resource-server side of the auth wiring (a `StaticTokenVerifier` seeded with hand-built tokens, no authorization-server provider) and speak raw HTTP, since -every assertion is about HTTP semantics the SDK `Client` cannot observe: the 401/403 status, -the `WWW-Authenticate` header structure, and that a wrong-audience token reaches the MCP -endpoint behind the gate. The flow side of the same 401 is `test_flow.py`'s flagship test. +every assertion is about HTTP semantics the SDK `Client` cannot observe: the 401/403 status +and the `WWW-Authenticate` header structure. The flow side of the same 401 is `test_flow.py`'s +flagship test. """ import time @@ -24,15 +24,23 @@ from tests.interaction.auth._harness import StaticTokenVerifier, auth_settings pytestmark = pytest.mark.anyio REQUIRED_SCOPE = "mcp:read" +RESOURCE = "http://127.0.0.1:8000/mcp" RESOURCE_METADATA_URL = "http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp" _FUTURE = int(time.time()) + 3600 _PAST = int(time.time()) - 3600 + TOKENS = { - "tok-valid": AccessToken(token="tok-valid", client_id="c", scopes=[REQUIRED_SCOPE], expires_at=_FUTURE), - "tok-expired": AccessToken(token="tok-expired", client_id="c", scopes=[REQUIRED_SCOPE], expires_at=_PAST), - "tok-noscope": AccessToken(token="tok-noscope", client_id="c", scopes=["other:thing"], expires_at=_FUTURE), + "tok-valid": AccessToken( + token="tok-valid", client_id="c", scopes=[REQUIRED_SCOPE], expires_at=_FUTURE, resource=RESOURCE + ), + "tok-expired": AccessToken( + token="tok-expired", client_id="c", scopes=[REQUIRED_SCOPE], expires_at=_PAST, resource=RESOURCE + ), + "tok-noscope": AccessToken( + token="tok-noscope", client_id="c", scopes=["other:thing"], expires_at=_FUTURE, resource=RESOURCE + ), "tok-wrong-aud": AccessToken( token="tok-wrong-aud", client_id="c", @@ -40,6 +48,7 @@ TOKENS = { expires_at=_FUTURE, resource="https://other.example/mcp", ), + "tok-no-aud": AccessToken(token="tok-no-aud", client_id="c", scopes=[REQUIRED_SCOPE], expires_at=_FUTURE), } @@ -47,7 +56,7 @@ TOKENS = { async def protected() -> AsyncIterator[httpx2.AsyncClient]: """A bearer-gated streamable-HTTP app (resource server only) on the in-process bridge.""" server = Server("rs") - settings = auth_settings(required_scopes=[REQUIRED_SCOPE]) + settings = auth_settings(required_scopes=[REQUIRED_SCOPE], validate_token_resource=True) async with mounted_app(server, auth=settings, token_verifier=StaticTokenVerifier(TOKENS)) as (http, _): yield http @@ -157,19 +166,38 @@ async def test_a_token_missing_a_required_scope_is_answered_403_insufficient_sco @requirement("hosting:auth:aud-validation") -async def test_a_token_with_a_mismatched_audience_is_accepted(protected: httpx2.AsyncClient) -> None: - """A token whose `resource` does not match the server's resource identifier is accepted. - - The spec mandates the resource server validate the token's audience; the bearer backend - never inspects `AccessToken.resource`, so the request passes the gate and the MCP endpoint - serves it. This pins current behaviour with the divergence recorded on the requirement. +@pytest.mark.parametrize("bearer", ["tok-wrong-aud", "tok-no-aud"]) +async def test_a_token_not_issued_for_this_resource_is_answered_401(protected: httpx2.AsyncClient, bearer: str) -> None: + """Spec-mandated audience check, which the SDK performs when `AuthSettings.validate_token_resource` + is set (off by default, the recorded divergence): a token whose verifier-reported `resource` + (RFC 8707) is another URL, or absent, is answered 401 `invalid_token` like an unrecognized token. """ - response = await post_mcp(protected, bearer="tok-wrong-aud") + response = await post_mcp(protected, bearer=bearer) + + assert response.status_code == 401 + assert parse_www_authenticate(response.headers["www-authenticate"])["error"] == "invalid_token" + + +@requirement("hosting:auth:aud-validation") +async def test_a_token_for_another_resource_is_served_when_validate_token_resource_is_off() -> None: + """The recorded divergence: with `AuthSettings` at their defaults the gate does not compare + `AccessToken.resource` with `resource_server_url`, so a token the verifier reports as issued + for another resource still reaches the MCP endpoint (SDK default; the check is the verifier's).""" + settings = auth_settings(required_scopes=[REQUIRED_SCOPE]) + async with mounted_app(Server("rs"), auth=settings, token_verifier=StaticTokenVerifier(TOKENS)) as (http, _): + response = await post_mcp(http, bearer="tok-wrong-aud") assert response.status_code == 200 - assert response.headers["content-type"].startswith("text/event-stream") - # The body is finite SSE: a result event followed by stream close. Pull the JSON-RPC response - # out of the buffered text to prove the MCP endpoint actually answered the initialize request. + + +@requirement("hosting:auth:aud-validation") +async def test_a_token_issued_for_this_resource_is_served(protected: httpx2.AsyncClient) -> None: + """The other half: a token the verifier reports as issued for `resource_server_url` passes the + gate and the MCP endpoint answers the initialize request.""" + response = await post_mcp(protected, bearer="tok-valid") + + assert response.status_code == 200 + # Finite SSE body: pull out the JSON-RPC result to prove the endpoint actually answered. [data] = [line.removeprefix("data: ") for line in response.text.splitlines() if line.startswith("data: ")] assert "protocolVersion" in JSONRPCResponse.model_validate_json(data).result diff --git a/tests/interaction/auth/test_lifecycle.py b/tests/interaction/auth/test_lifecycle.py index 8f45a015..32f96bc3 100644 --- a/tests/interaction/auth/test_lifecycle.py +++ b/tests/interaction/auth/test_lifecycle.py @@ -135,6 +135,31 @@ async def test_an_expired_access_token_is_transparently_refreshed_before_the_nex assert storage.tokens.expires_in == 3600 +@requirement("client-auth:refresh:transparent") +async def test_a_refreshed_access_token_is_still_accepted_when_the_server_validates_token_resource() -> None: + """With `AuthSettings.validate_token_resource` on, the access token minted by the refresh grant + still passes the gate: the provider carries the original grant's resource on `RefreshToken` and + onto the refreshed `AccessToken` (SDK-defined propagation), so the request after the refresh is + served rather than answered 401.""" + recorded, on_request = record_requests() + provider = InMemoryAuthorizationServerProvider(issue_expired_first=True) + server = Server("guarded", on_list_tools=list_tools) + settings = auth_settings(validate_token_resource=True) + + with anyio.fail_after(5): + async with connect_with_oauth(server, provider=provider, settings=settings, on_request=on_request) as ( + client, + _, + ): + result = await client.list_tools() + + assert result.tools[0].name == "echo" + assert [form_body(r)["grant_type"] for r in find(recorded, "POST", "/token")] == snapshot( + ["authorization_code", "refresh_token"] + ) + assert {t.resource for t in provider.access_tokens.values()} == {f"{BASE_URL}/mcp"} + + @requirement("client-auth:403-scope-upgrade") async def test_a_403_insufficient_scope_triggers_one_reauthorize_with_the_challenged_scope() -> None: """A 403 `insufficient_scope` challenge is answered by one re-authorize with the challenge's scope. @@ -373,6 +398,7 @@ async def test_client_credentials_provider_obtains_a_token_without_an_authorize_ client_id="m2m-client", client_secret="m2m-secret", scope="mcp", + issuer=BASE_URL, ) with anyio.fail_after(5): @@ -424,6 +450,7 @@ async def test_private_key_jwt_provider_authenticates_the_token_request_with_an_ client_id="m2m-jwt-client", assertion_provider=assertion_provider, scope="mcp", + issuer=BASE_URL, ) with anyio.fail_after(5): diff --git a/tests/interaction/transports/test_hosting_session.py b/tests/interaction/transports/test_hosting_session.py index 23c8da15..6aa2a921 100644 --- a/tests/interaction/transports/test_hosting_session.py +++ b/tests/interaction/transports/test_hosting_session.py @@ -107,23 +107,15 @@ async def test_delete_terminates_the_session_and_subsequent_requests_return_404( delete = await http.delete("/mcp", headers=base_headers(session_id=session_id)) assert delete.status_code == 200 - # The manager keeps the terminated transport registered, so the next request reaches the - # transport's own _terminated check rather than the manager's unknown-session path. - assert session_id in manager._server_instances + # The manager forgets a terminated session, so from then on the ID is simply unknown. + assert session_id not in manager._server_instances post = await http.post( "/mcp", json={"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, headers=base_headers(session_id=session_id), ) assert (post.status_code, post.json()) == snapshot( - ( - 404, - { - "jsonrpc": "2.0", - "id": None, - "error": {"code": -32600, "message": "Not Found: Session has been terminated"}, - }, - ) + (404, {"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Session not found"}}) ) diff --git a/tests/server/auth/middleware/test_bearer_auth.py b/tests/server/auth/middleware/test_bearer_auth.py index 6ab34367..b7efb937 100644 --- a/tests/server/auth/middleware/test_bearer_auth.py +++ b/tests/server/auth/middleware/test_bearer_auth.py @@ -4,6 +4,7 @@ import time from typing import Any, cast import pytest +from pydantic import AnyHttpUrl from starlette.authentication import AuthCredentials from starlette.datastructures import Headers from starlette.requests import Request @@ -273,6 +274,56 @@ class TestBearerAuthBackend: assert user.access_token == valid_access_token +class SingleTokenVerifier: + """A `TokenVerifier` that knows exactly one token.""" + + def __init__(self, access_token: AccessToken) -> None: + self.access_token = access_token + + async def verify_token(self, token: str) -> AccessToken | None: + return self.access_token if token == self.access_token.token else None + + +RS = "https://api.example.com/mcp" + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("resource_server_url", "token_resource", "accepted"), + [ + (None, "https://other.example.com/mcp", True), # nothing configured to compare against + (None, None, True), + (RS, None, False), # the verifier did not report what the token was issued for + (RS, RS, True), + (RS, RS + "/", True), + (RS, "https://API.EXAMPLE.COM:443/mcp", True), # same URL, different spelling + (RS, "https://api.example.com", False), + (RS, RS + "/child", False), + (RS, "https://api.example.com/other", False), + (RS, "https://other.example.com/mcp", False), + (RS, "api.example.com", False), # not a URL + ], +) +async def test_backend_accepts_only_tokens_issued_for_its_resource( + resource_server_url: str | None, token_resource: str | None, accepted: bool +): + """With `resource_server_url` set, only a token whose `resource` (RFC 8707) is that URL is + accepted and anything else is treated like an unrecognized token (spec-mandated audience + check); without it the verifier's answer stands (SDK-defined, the default wiring).""" + token = AccessToken(token="t", client_id="c", scopes=["read"], resource=token_resource) + backend = BearerAuthBackend( + SingleTokenVerifier(token), + resource_server_url=AnyHttpUrl(resource_server_url) if resource_server_url else None, + ) + + result = await backend.authenticate(Request({"type": "http", "headers": [(b"authorization", b"Bearer t")]})) + + if accepted: + assert result is not None and result[1].access_token == token + else: + assert result is None + + @pytest.mark.anyio class TestRequireAuthMiddleware: """Tests for the RequireAuthMiddleware class.""" diff --git a/tests/server/auth/test_routes.py b/tests/server/auth/test_routes.py index 58685c64..64c2c024 100644 --- a/tests/server/auth/test_routes.py +++ b/tests/server/auth/test_routes.py @@ -53,6 +53,7 @@ def test_auth_settings_preserves_path_less_issuer(): settings = AuthSettings( issuer_url="https://as.example.com", # type: ignore[arg-type] resource_server_url="https://rs.example.com", # type: ignore[arg-type] + validate_token_resource=True, ) assert str(settings.issuer_url) == "https://as.example.com" assert str(settings.resource_server_url) == "https://rs.example.com" @@ -63,6 +64,7 @@ def test_build_metadata_serves_issuer_without_trailing_slash(): settings = AuthSettings( issuer_url="https://as.example.com", # type: ignore[arg-type] resource_server_url="https://rs.example.com", # type: ignore[arg-type] + validate_token_resource=True, ) metadata = build_metadata(settings.issuer_url, None, ClientRegistrationOptions(), RevocationOptions()) diff --git a/tests/server/auth/test_settings.py b/tests/server/auth/test_settings.py new file mode 100644 index 00000000..db7dd26f --- /dev/null +++ b/tests/server/auth/test_settings.py @@ -0,0 +1,34 @@ +import warnings + +import pytest +from pydantic import AnyHttpUrl, ValidationError + +from mcp.server.auth.settings import AuthSettings +from mcp.shared.exceptions import MCPDeprecationWarning + +ISSUER = AnyHttpUrl("https://auth.example.com") +RESOURCE = AnyHttpUrl("https://mcp.example.com/mcp") + + +def test_validate_token_resource_requires_a_resource_server_url(): + """SDK-defined: asking the bearer gate to compare tokens against `resource_server_url` without + configuring one is refused at construction time rather than silently comparing nothing.""" + AuthSettings(issuer_url=ISSUER, resource_server_url=RESOURCE, validate_token_resource=True) + with pytest.raises(ValidationError, match="validate_token_resource requires resource_server_url"): + AuthSettings(issuer_url=ISSUER, resource_server_url=None, validate_token_resource=True) + + +def test_leaving_validate_token_resource_unset_warns_when_a_resource_server_url_is_configured(): + """Unset behaves as False but says so: a resource server that has not chosen gets an + `MCPDeprecationWarning` pointing at its own `AuthSettings(...)` call (3.0 flips the default).""" + with pytest.warns(MCPDeprecationWarning, match="validate_token_resource") as record: + settings = AuthSettings(issuer_url=ISSUER, resource_server_url=RESOURCE) + assert settings.validate_token_resource is None + assert record[0].filename == __file__ + + +@pytest.mark.parametrize("kwargs", [{"validate_token_resource": False}, {"resource_server_url": None}]) +def test_an_explicit_choice_or_no_resource_server_url_does_not_warn(kwargs: dict[str, object]): + with warnings.catch_warnings(): + warnings.simplefilter("error") + AuthSettings.model_validate({"issuer_url": ISSUER, "resource_server_url": RESOURCE, **kwargs}) diff --git a/tests/server/test_fastmcp.py b/tests/server/test_fastmcp.py new file mode 100644 index 00000000..b15c5191 --- /dev/null +++ b/tests/server/test_fastmcp.py @@ -0,0 +1,55 @@ +"""The removed v1 import path `mcp.server.fastmcp` fails with a pointer to the migration guide.""" + +import importlib +import sys + +import pytest +from inline_snapshot import snapshot + +import mcp.server +from mcp.server.mcpserver import MCPServer + + +def test_importing_fastmcp_raises_module_not_found_that_points_at_the_migration_guide() -> None: + """SDK-defined: the v1 path fails with the same exception type and `.name` as a module + that genuinely does not exist, but the message names the replacement and the guide.""" + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.import_module("mcp.server.fastmcp") + + assert exc_info.value.name == "mcp.server.fastmcp" + assert str(exc_info.value) == snapshot( + "No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer " + "(from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at " + "https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver " + "or pin 'mcp<2' to keep running v1 code." + ) + # A module that raises while executing is never cached, so nothing is left behind. + assert "mcp.server.fastmcp" not in sys.modules + assert not hasattr(mcp.server, "fastmcp") + + +def test_importing_a_fastmcp_submodule_raises_the_parent_pointer() -> None: + """SDK-defined: a deep v1 path executes `mcp.server.fastmcp` first, so it fails with that + module's message and `.name` rather than a bare error for the leaf.""" + with pytest.raises(ModuleNotFoundError) as parent: + importlib.import_module("mcp.server.fastmcp") + with pytest.raises(ModuleNotFoundError) as exc_info: + importlib.import_module("mcp.server.fastmcp.utilities.types") + + assert exc_info.value.name == "mcp.server.fastmcp" + assert str(exc_info.value) == str(parent.value) + + +def test_v1_first_import_shim_falls_back_to_mcpserver() -> None: + """SDK-defined: projects that support both majors try the v1 import and fall back on + `ModuleNotFoundError` (the narrowest guard seen in the wild), which is why the pointer is + raised as exactly that type and not as a bare `ImportError` or after a warning.""" + fell_back = False + try: + server_class: type = importlib.import_module("mcp.server.fastmcp").FastMCP + except ModuleNotFoundError: + fell_back = True + server_class = MCPServer + + assert fell_back + assert server_class is MCPServer diff --git a/tests/server/test_streamable_http_manager.py b/tests/server/test_streamable_http_manager.py index 1c0f88a6..1b8f9264 100644 --- a/tests/server/test_streamable_http_manager.py +++ b/tests/server/test_streamable_http_manager.py @@ -2,15 +2,28 @@ import json import logging -from collections.abc import Iterator -from typing import Any +import math +from collections.abc import AsyncIterator, Iterator +from contextlib import asynccontextmanager, contextmanager +from typing import Any, cast from unittest.mock import AsyncMock, patch import anyio import httpx2 import pytest -from mcp_types import INVALID_REQUEST, ListToolsResult, PaginatedRequestParams -from starlette.types import Message, Scope +from mcp_types import ( + INTERNAL_ERROR, + INVALID_REQUEST, + CallToolRequestParams, + CallToolResult, + ListToolsResult, + PaginatedRequestParams, + TextContent, +) +from mcp_types.version import LATEST_HANDSHAKE_VERSION +from starlette.applications import Starlette +from starlette.routing import Mount +from starlette.types import ASGIApp, Message, Receive, Scope, Send from mcp import Client from mcp.client.streamable_http import streamable_http_client @@ -18,7 +31,32 @@ from mcp.server import Server, ServerRequestContext, streamable_http_manager from mcp.server.auth.middleware.bearer_auth import AuthenticatedUser from mcp.server.auth.provider import AccessToken from mcp.server.streamable_http import MCP_SESSION_ID_HEADER, StreamableHTTPServerTransport -from mcp.server.streamable_http_manager import DEFAULT_MAX_REQUEST_BODY_SIZE, StreamableHTTPSessionManager +from mcp.server.streamable_http_manager import ( + DEFAULT_MAX_REQUEST_BODY_SIZE, + DEFAULT_MAX_SESSIONS, + DEFAULT_SESSION_IDLE_TIMEOUT, + StreamableHTTPSessionManager, +) +from tests.interaction.transports import StreamingASGITransport + +# The in-process app is mounted at this origin purely so URLs are well-formed; nothing listens here. +BASE_URL = "http://127.0.0.1:8000" + +_JSON_HEADERS = {"accept": "application/json, text/event-stream", "content-type": "application/json"} + +_INITIALIZE_BODY = json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": LATEST_HANDSHAKE_VERSION, + "capabilities": {}, + "clientInfo": {"name": "test", "version": "0"}, + }, + } +).encode() +"""A wire-level initialize request: the only request that may open a session.""" @pytest.mark.anyio @@ -286,19 +324,7 @@ async def test_stateless_requests_memory_cleanup(): app = Server("test-stateless-real-cleanup") manager = StreamableHTTPSessionManager(app=app, stateless=True) - # Track created transport instances - created_transports: list[StreamableHTTPServerTransport] = [] - - # Patch StreamableHTTPServerTransport constructor to track instances - - original_constructor = StreamableHTTPServerTransport - - def track_transport(*args: Any, **kwargs: Any) -> StreamableHTTPServerTransport: - transport = original_constructor(*args, **kwargs) - created_transports.append(transport) - return transport - - with patch.object(streamable_http_manager, "StreamableHTTPServerTransport", side_effect=track_transport): + with _created_transports() as created_transports: async with manager.run(): # Send a simple request sent_messages: list[Message] = [] @@ -420,50 +446,57 @@ class _IdleTimeoutObserver(logging.Handler): self.reaped.set() -@pytest.mark.anyio -async def test_idle_session_is_reaped(caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest): - """After idle timeout fires, the session returns 404.""" - app = Server("test-idle-reap") - manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=0.05) +def _observe_idle_timeout(caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest) -> _IdleTimeoutObserver: + """Install an observer for the manager's "idle timeout" log record for the rest of the test. - # The reap is observed through the manager's own "idle timeout" log record: the manager pops - # the session synchronously after emitting it, before its next await, so a waiter woken by - # the record always finds the session gone. caplog.set_level enables INFO so it is created. + The manager pops the session synchronously after emitting that record, before its next await, + so a waiter woken by it always finds the session gone. caplog.set_level enables INFO so the + record is created. + """ observer = _IdleTimeoutObserver() manager_logger = logging.getLogger(streamable_http_manager.__name__) manager_logger.addHandler(observer) request.addfinalizer(lambda: manager_logger.removeHandler(observer)) caplog.set_level(logging.INFO, logger=streamable_http_manager.__name__) + return observer + + +@contextmanager +def _created_transports() -> Iterator[list[StreamableHTTPServerTransport]]: + """Collect every transport a session manager creates while the context is open.""" + created: list[StreamableHTTPServerTransport] = [] + + def create(*args: Any, **kwargs: Any) -> StreamableHTTPServerTransport: + transport = StreamableHTTPServerTransport(*args, **kwargs) + created.append(transport) + return transport + + with patch.object(streamable_http_manager, "StreamableHTTPServerTransport", side_effect=create): + yield created + + +@asynccontextmanager +async def _served( + manager: StreamableHTTPSessionManager, endpoint: ASGIApp | None = None +) -> AsyncIterator[httpx2.AsyncClient]: + """Run `manager` behind an in-process HTTP client whose responses stream as they are produced. + + `endpoint` is the ASGI app mounted for it; by default the manager itself. + """ + app = Starlette(routes=[Mount("/", app=endpoint or manager.handle_request)]) + async with manager.run(), httpx2.AsyncClient(transport=StreamingASGITransport(app), base_url=BASE_URL) as http: + yield http + + +@pytest.mark.anyio +async def test_idle_session_is_reaped(caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest): + """After idle timeout fires, the session returns 404.""" + app = Server("test-idle-reap") + manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=0.05) + observer = _observe_idle_timeout(caplog, request) async with manager.run(): - sent_messages: list[Message] = [] - - async def mock_send(message: Message): - sent_messages.append(message) - - scope = { - "type": "http", - "method": "POST", - "path": "/mcp", - "headers": [(b"content-type", b"application/json")], - } - - async def mock_receive(): - return {"type": "http.request", "body": b"", "more_body": False} - - await manager.handle_request(scope, mock_receive, mock_send) - - session_id = None - for msg in sent_messages: # pragma: no branch - if msg["type"] == "http.response.start": # pragma: no branch - for header_name, header_value in msg.get("headers", []): # pragma: no branch - if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower(): - session_id = header_value.decode() - break - if session_id: # pragma: no branch - break - - assert session_id is not None, "Session ID not found in response headers" + session_id = await _open_session(manager, None) # Wait for the 50ms idle timeout to fire and the session to be unregistered. Re-requesting # the session to poll for the 404 would push its idle deadline forward and keep it alive. @@ -471,41 +504,402 @@ async def test_idle_session_is_reaped(caplog: pytest.LogCaptureFixture, request: await observer.reaped.wait() # Verify via public API: old session ID now returns 404 - response_messages: list[Message] = [] + assert await _request_session(manager, session_id, None) == 404 - async def capture_send(message: Message): - response_messages.append(message) - scope_with_session = { - "type": "http", - "method": "POST", - "path": "/mcp", - "headers": [ - (b"content-type", b"application/json"), - (b"mcp-session-id", session_id.encode()), - ], +@pytest.mark.anyio +async def test_request_in_flight_holds_the_session_open( + caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest +) -> None: + """A session does not expire while one of its requests is still being served, however long that takes; + the idle period is counted from the moment its last request completes.""" + tool_started = anyio.Event() + release_tool = anyio.Event() + + async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: + tool_started.set() + await release_tool.wait() + return CallToolResult(content=[TextContent(type="text", text="done")]) + + app = Server("test-in-flight", on_call_tool=handle_call_tool) + manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=30) + observer = _observe_idle_timeout(caplog, request) + + async with _served(manager) as http: + initialize = await http.post("/mcp", content=_INITIALIZE_BODY, headers=_JSON_HEADERS) + assert initialize.status_code == 200 + session_id = initialize.headers[MCP_SESSION_ID_HEADER] + transport = manager._server_instances[session_id] + session_headers = _JSON_HEADERS | {MCP_SESSION_ID_HEADER: session_id} + call_tool_body: dict[str, Any] = { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": {"name": "slow", "arguments": {}}, + } + responses: list[httpx2.Response] = [] + + async def call_tool() -> None: + responses.append(await http.post("/mcp", json=call_tool_body, headers=session_headers)) + + async with anyio.create_task_group() as tg: + tg.start_soon(call_tool) + with anyio.fail_after(5): + await tool_started.wait() + # While the call is being served the idle countdown is suspended. + assert transport.idle_scope is not None and transport.idle_scope.deadline == math.inf + # From here on a short idle period, counted from the moment the call completes. + transport._idle_timeout = 0.05 + release_tool.set() + + assert responses[0].status_code == 200 + assert '"done"' in responses[0].text + + # Nothing is in flight any more, so the idle period now runs out. + with anyio.fail_after(5): + await observer.reaped.wait() + assert session_id not in manager._server_instances + assert transport.is_terminated + followup = await http.post("/mcp", json={"jsonrpc": "2.0", "id": 3, "method": "ping"}, headers=session_headers) + assert followup.status_code == 404 + + +@pytest.mark.anyio +async def test_open_event_stream_holds_the_session_open( + caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest +) -> None: + """A client listening on the session's GET stream keeps the session, even if it sends nothing; + once the stream closes the idle period runs out and the session is gone.""" + manager = StreamableHTTPSessionManager(app=Server("test-get-stream"), session_idle_timeout=30) + observer = _observe_idle_timeout(caplog, request) + + async with _served(manager) as http: + initialize = await http.post("/mcp", content=_INITIALIZE_BODY, headers=_JSON_HEADERS) + assert initialize.status_code == 200 + session_id = initialize.headers[MCP_SESSION_ID_HEADER] + session_headers = _JSON_HEADERS | {MCP_SESSION_ID_HEADER: session_id} + + get_headers = {"accept": "text/event-stream", MCP_SESSION_ID_HEADER: session_id} + async with http.stream("GET", "/mcp", headers=get_headers) as stream: + assert stream.status_code == 200 + # The stream has been answered, so it is in flight: the idle countdown is suspended. + transport = manager._server_instances[session_id] + assert transport.idle_scope is not None and transport.idle_scope.deadline == math.inf + # From here on a short idle period, counted from the moment the stream closes. + transport._idle_timeout = 0.05 + + with anyio.fail_after(5): + await observer.reaped.wait() + assert session_id not in manager._server_instances + assert transport.is_terminated + followup = await http.post("/mcp", json={"jsonrpc": "2.0", "id": 2, "method": "ping"}, headers=session_headers) + assert followup.status_code == 404 + + +@pytest.mark.anyio +async def test_request_completing_under_an_open_event_stream_does_not_start_the_countdown( + caplog: pytest.LogCaptureFixture, request: pytest.FixtureRequest +) -> None: + """A request that completes while the session's GET stream is still open does not start the idle + period: the stream is still in flight, so the countdown only begins once it closes too.""" + manager = StreamableHTTPSessionManager(app=Server("test-get-stream-and-post"), session_idle_timeout=30) + observer = _observe_idle_timeout(caplog, request) + session_post_served = anyio.Event() + + async def endpoint(scope: Scope, receive: Receive, send: Send) -> None: + # Report once a POST for the open session has been served to the end, + # in-flight bookkeeping included. + await manager.handle_request(scope, receive, send) + if scope["method"] == "POST" and MCP_SESSION_ID_HEADER.encode() in dict(scope["headers"]): + session_post_served.set() + + async with _served(manager, endpoint) as http: + initialize = await http.post("/mcp", content=_INITIALIZE_BODY, headers=_JSON_HEADERS) + assert initialize.status_code == 200 + session_id = initialize.headers[MCP_SESSION_ID_HEADER] + session_headers = _JSON_HEADERS | {MCP_SESSION_ID_HEADER: session_id} + + get_headers = {"accept": "text/event-stream", MCP_SESSION_ID_HEADER: session_id} + async with http.stream("GET", "/mcp", headers=get_headers) as stream: + assert stream.status_code == 200 + transport = manager._server_instances[session_id] + assert transport.idle_scope is not None and transport.idle_scope.deadline == math.inf + ping = await http.post("/mcp", json={"jsonrpc": "2.0", "id": 2, "method": "ping"}, headers=session_headers) + assert ping.status_code == 200 + with anyio.fail_after(5): + await session_post_served.wait() + # The ping has completed, but the open stream still suspends the idle countdown. + assert transport.idle_scope.deadline == math.inf + # From here on a short idle period, counted from the moment the stream closes. + transport._idle_timeout = 0.05 + + with anyio.fail_after(5): + await observer.reaped.wait() + assert session_id not in manager._server_instances + assert transport.is_terminated + followup = await http.post("/mcp", json={"jsonrpc": "2.0", "id": 3, "method": "ping"}, headers=session_headers) + assert followup.status_code == 404 + + +def test_session_idle_timeout_defaults_to_thirty_minutes() -> None: + """Stateful sessions expire after 30 minutes without a request in flight unless configured otherwise.""" + manager = StreamableHTTPSessionManager(app=Server("test")) + assert manager.session_idle_timeout == DEFAULT_SESSION_IDLE_TIMEOUT == 30 * 60 + + +@pytest.mark.parametrize("session_idle_timeout", [0, -1, float("inf"), float("nan")]) +def test_session_idle_timeout_rejects_invalid_values(session_idle_timeout: float) -> None: + """The idle timeout is a positive, finite number of seconds, or None for sessions that never expire.""" + with pytest.raises(ValueError) as exc_info: + StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=session_idle_timeout) + assert str(exc_info.value) == "session_idle_timeout must be a positive, finite number of seconds" + + +@pytest.mark.anyio +async def test_session_idle_timeout_is_unused_in_stateless_mode() -> None: + """Stateless mode keeps no sessions, so the idle timeout is accepted and simply has nothing to expire.""" + manager = StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=30, stateless=True) + async with manager.run(): + response_start, _ = await _call(manager, _request_scope(), _INITIALIZE_BODY) + assert response_start["status"] == 200 + assert manager._server_instances == {} + + +@pytest.mark.anyio +@pytest.mark.parametrize("session_idle_timeout", [DEFAULT_SESSION_IDLE_TIMEOUT, None]) +async def test_deleted_session_is_forgotten(session_idle_timeout: float | None) -> None: + """A client DELETE ends the session and the manager stops tracking it; the ID is unknown afterwards.""" + manager = StreamableHTTPSessionManager(app=Server("test-delete"), session_idle_timeout=session_idle_timeout) + async with manager.run(): + session_id = await _open_session(manager, None) + assert session_id in manager._server_instances + + assert await _request_session(manager, session_id, None, method="DELETE") == 200 + assert session_id not in manager._server_instances + response_start, response_body = await _call(manager, _request_scope(session_id=session_id)) + assert response_start["status"] == 404 + assert json.loads(response_body) == { + "jsonrpc": "2.0", + "id": None, + "error": {"code": INVALID_REQUEST, "message": "Session not found"}, } - await manager.handle_request(scope_with_session, mock_receive, capture_send) - response_start = next( - (msg for msg in response_messages if msg["type"] == "http.response.start"), - None, - ) - assert response_start is not None - assert response_start["status"] == 404 +@pytest.mark.anyio +async def test_opening_request_that_fails_leaves_no_session() -> None: + """If serving the request that would open a session raises, the provisional session is discarded + there and then rather than left registered with its server task running.""" + manager = StreamableHTTPSessionManager(app=Server("test-failed-open")) + with _created_transports() as transports: + async with manager.run(): + with ( + patch.object( + StreamableHTTPServerTransport, "handle_request", AsyncMock(side_effect=RuntimeError("boom")) + ), + pytest.raises(RuntimeError, match="boom"), + anyio.fail_after(5), + ): + await _call(manager, _request_scope(), _INITIALIZE_BODY) + assert manager._server_instances == {} + assert manager._session_owners == {} + (transport,) = transports + assert transport.is_terminated -def test_session_idle_timeout_rejects_non_positive(): - with pytest.raises(ValueError, match="positive number"): - StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=-1) - with pytest.raises(ValueError, match="positive number"): - StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=0) +@pytest.mark.anyio +async def test_opening_request_that_is_cancelled_leaves_no_session() -> None: + """If the request that would open a session is cancelled while it is being served (the client went + away), the provisional session is discarded rather than left registered.""" + manager = StreamableHTTPSessionManager(app=Server("test-cancelled-open")) + entered = anyio.Event() + + async def hang(self: StreamableHTTPServerTransport, scope: Scope, receive: Receive, send: Send) -> None: + entered.set() + await anyio.sleep_forever() + + opening_request = anyio.CancelScope() + + async def open_session() -> None: + with opening_request: + await _call(manager, _request_scope(), _INITIALIZE_BODY) + + with _created_transports() as transports, patch.object(StreamableHTTPServerTransport, "handle_request", hang): + async with manager.run(): + async with anyio.create_task_group() as tg: + tg.start_soon(open_session) + with anyio.fail_after(5): + await entered.wait() + assert len(manager._server_instances) == 1 + opening_request.cancel() + assert manager._server_instances == {} + assert manager._session_owners == {} + (transport,) = transports + assert transport.is_terminated -def test_session_idle_timeout_rejects_stateless(): - with pytest.raises(RuntimeError, match="not supported in stateless"): - StreamableHTTPSessionManager(app=Server("test"), session_idle_timeout=30, stateless=True) +@pytest.mark.anyio +async def test_opening_request_whose_session_task_cannot_start_leaves_no_session() -> None: + """If the server task for a would-be session cannot be started, the provisional session is discarded + (forgotten, its transport terminated) rather than left registered without anything serving it.""" + manager = StreamableHTTPSessionManager(app=Server("test-unstartable-open")) + + @asynccontextmanager + async def connect_that_fails(self: StreamableHTTPServerTransport) -> AsyncIterator[None]: + raise RuntimeError("boom") + yield + + with _created_transports() as transports: + async with manager.run(): + with ( + patch.object(StreamableHTTPServerTransport, "connect", connect_that_fails), + pytest.raises(RuntimeError, match="boom"), + anyio.fail_after(5), + ): + await _call(manager, _request_scope(), _INITIALIZE_BODY) + assert manager._server_instances == {} + assert manager._session_owners == {} + (transport,) = transports + assert transport.is_terminated + + +@pytest.mark.anyio +async def test_stateless_request_that_is_cancelled_still_terminates_its_transport() -> None: + """If a stateless request is cancelled while it is being served (the client went away), its transport + is terminated all the same, which is what ends the per-request server task.""" + manager = StreamableHTTPSessionManager(app=Server("test-stateless-cancelled"), stateless=True) + entered = anyio.Event() + + async def hang(self: StreamableHTTPServerTransport, scope: Scope, receive: Receive, send: Send) -> None: + entered.set() + await anyio.sleep_forever() + + stateless_request = anyio.CancelScope() + + async def make_request() -> None: + with stateless_request: + await _call(manager, _request_scope(), _INITIALIZE_BODY) + + with _created_transports() as transports, patch.object(StreamableHTTPServerTransport, "handle_request", hang): + async with manager.run(): + async with anyio.create_task_group() as tg: + tg.start_soon(make_request) + with anyio.fail_after(5): + await entered.wait() + stateless_request.cancel() + (transport,) = transports + assert transport.is_terminated + + +@pytest.mark.anyio +@pytest.mark.parametrize( + ("method", "headers", "body", "expected_status"), + [ + ("POST", _JSON_HEADERS, b'{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}', 400), + ("POST", _JSON_HEADERS, b'{"jsonrpc": "2.0", "method": "notifications/initialized"}', 400), + ("POST", _JSON_HEADERS, b"{not json", 400), + ("POST", _JSON_HEADERS | {"accept": "text/plain"}, _INITIALIZE_BODY, 406), + ("GET", {"accept": "text/event-stream"}, b"", 400), + ("DELETE", _JSON_HEADERS, b"", 400), + ("PATCH", _JSON_HEADERS, b"", 405), + ], + ids=[ + "non-initialize-request", + "notification", + "malformed-json", + "unacceptable-accept-header", + "get-without-session", + "delete-without-session", + "unsupported-method", + ], +) +async def test_refused_opening_request_leaves_no_session( + method: str, headers: dict[str, str], body: bytes, expected_status: int +) -> None: + """Only an accepted initialize opens a session: a request without a session ID that is answered with an + error leaves nothing registered once the manager has answered it.""" + manager = StreamableHTTPSessionManager(app=Server("test-refused")) + scope: Scope = { + "type": "http", + "method": method, + "path": "/mcp", + "headers": [(name.encode(), value.encode()) for name, value in headers.items()], + } + with _created_transports() as transports: + async with manager.run(): + response_start, _ = await _call(manager, scope, body) + assert response_start["status"] == expected_status + assert manager._server_instances == {} + assert manager._session_owners == {} + (transport,) = transports + assert transport.is_terminated + + +@pytest.mark.anyio +async def test_new_session_is_refused_at_max_sessions() -> None: + """At the session limit a further initialize is answered 503 and opens nothing; room frees up as + sessions end.""" + manager = StreamableHTTPSessionManager(app=Server("test-cap"), max_sessions=1) + async with manager.run(): + first = await _open_session(manager, None) + + response_start, response_body = await _call(manager, _request_scope(), _INITIALIZE_BODY) + assert response_start["status"] == 503 + assert json.loads(response_body) == { + "jsonrpc": "2.0", + "id": None, + "error": {"code": INTERNAL_ERROR, "message": "Too many open sessions"}, + } + assert list(manager._server_instances) == [first] + + assert await _request_session(manager, first, None, method="DELETE") == 200 + second = await _open_session(manager, None) + assert list(manager._server_instances) == [second] + + +@pytest.mark.anyio +async def test_client_that_is_slow_to_send_its_opening_request_does_not_hold_up_others() -> None: + """While one client has yet to finish sending the request that would open its session, another + client can still open one.""" + manager = StreamableHTTPSessionManager(app=Server("test-slow-open")) + body_awaited = anyio.Event() + + async def stall() -> None: + # This client has sent its headers but never finishes sending the body. + body_awaited.set() + await anyio.sleep_forever() + + async def discard(message: Message) -> None: ... + + slow_client = anyio.CancelScope() + + async def open_slowly() -> None: + with slow_client: + await manager.handle_request(_request_scope(), cast(Receive, stall), discard) + + session_id: str | None = None + async with manager.run(): + async with anyio.create_task_group() as tg: + tg.start_soon(open_slowly) + with anyio.fail_after(5): + await body_awaited.wait() + session_id = await _open_session(manager, None) + slow_client.cancel() + assert session_id is not None + assert list(manager._server_instances) == [session_id] + + +def test_max_sessions_defaults_to_ten_thousand() -> None: + """A manager holds at most 10 000 concurrent stateful sessions unless configured otherwise.""" + manager = StreamableHTTPSessionManager(app=Server("test")) + assert manager.max_sessions == DEFAULT_MAX_SESSIONS == 10_000 + assert StreamableHTTPSessionManager(app=Server("test"), max_sessions=None).max_sessions is None + + +@pytest.mark.parametrize("max_sessions", [0, -1]) +def test_max_sessions_rejects_non_positive_values(max_sessions: int) -> None: + with pytest.raises(ValueError) as exc_info: + StreamableHTTPSessionManager(app=Server("test"), max_sessions=max_sessions) + assert str(exc_info.value) == "max_sessions must be a positive number of sessions or None" def _user(client_id: str, subject: str | None = None, issuer: str | None = None) -> AuthenticatedUser: @@ -535,19 +929,33 @@ def _request_scope( return scope -async def _open_session(manager: StreamableHTTPSessionManager, user: AuthenticatedUser | None) -> str: - """Create a new session as `user` and return its session ID.""" +async def _call(manager: StreamableHTTPSessionManager, scope: Scope, body: bytes = b"") -> tuple[Message, bytes]: + """Drive one request through the manager in process; return its `http.response.start` message and body.""" sent_messages: list[Message] = [] + body_delivered = False - async def mock_send(message: Message) -> None: + async def send(message: Message) -> None: sent_messages.append(message) - async def mock_receive() -> Message: - return {"type": "http.request", "body": b"", "more_body": False} - - await manager.handle_request(_request_scope(user=user), mock_receive, mock_send) + async def receive() -> Message: + # Deliver the body once, then block like a client holding the connection + # open; a streaming response ends when the server closes it. + nonlocal body_delivered + if body_delivered: + await anyio.sleep_forever() + body_delivered = True + return {"type": "http.request", "body": body, "more_body": False} + await manager.handle_request(scope, receive, send) response_start = next(msg for msg in sent_messages if msg["type"] == "http.response.start") + response_body = b"".join(msg.get("body", b"") for msg in sent_messages if msg["type"] == "http.response.body") + return response_start, response_body + + +async def _open_session(manager: StreamableHTTPSessionManager, user: AuthenticatedUser | None) -> str: + """Create a new session as `user` with an initialize request and return its session ID.""" + response_start, _ = await _call(manager, _request_scope(user=user), _INITIALIZE_BODY) + assert response_start["status"] == 200 headers = dict(response_start.get("headers", [])) return headers[MCP_SESSION_ID_HEADER.encode()].decode() @@ -556,26 +964,14 @@ async def _request_session( manager: StreamableHTTPSessionManager, session_id: str, user: AuthenticatedUser | None, method: str = "POST" ) -> int: """Send a request for an existing session as `user` and return the response status.""" - sent_messages: list[Message] = [] - - async def mock_send(message: Message) -> None: - sent_messages.append(message) - - async def mock_receive() -> Message: - return {"type": "http.request", "body": b"", "more_body": False} - - await manager.handle_request( - _request_scope(session_id=session_id, user=user, method=method), mock_receive, mock_send - ) - - response_start = next(msg for msg in sent_messages if msg["type"] == "http.response.start") + response_start, _ = await _call(manager, _request_scope(session_id=session_id, user=user, method=method)) return response_start["status"] @pytest.fixture async def manager_with_live_session(): - """A running manager around a real `Server`. Sessions remain registered until - `manager.run()` exits because `Server.run` blocks waiting for an initialize message.""" + """A running manager around a real `Server`. Sessions are opened with a real initialize and stay + registered until `manager.run()` exits because nothing in these tests ends them.""" manager = StreamableHTTPSessionManager(app=Server("test-session-credentials")) async with manager.run(): yield manager diff --git a/tests/server/test_streamable_http_router.py b/tests/server/test_streamable_http_router.py index 07aa0634..0c5796c1 100644 --- a/tests/server/test_streamable_http_router.py +++ b/tests/server/test_streamable_http_router.py @@ -141,3 +141,19 @@ async def test_json_post_answers_500_when_session_terminates_mid_request() -> No assert post.sent[0]["type"] == "http.response.start" assert post.sent[0]["status"] == 500 + + +@pytest.mark.anyio +async def test_terminated_transport_answers_404() -> None: + """A request that still reaches a transport after its session was terminated is answered 404.""" + transport = StreamableHTTPServerTransport(mcp_session_id="sid") + post = _AsgiPost( + b'{"jsonrpc":"2.0","id":"req-1","method":"ping"}', + [(b"accept", b"application/json, text/event-stream"), (b"content-type", b"application/json")], + ) + async with transport.connect(): + await transport.terminate() + await transport.handle_request(post.scope, post.receive, post.send) + + assert post.sent[0]["type"] == "http.response.start" + assert post.sent[0]["status"] == 404 diff --git a/tests/shared/test_httpx_utils.py b/tests/shared/test_httpx_utils.py index a94d7c92..a267ed79 100644 --- a/tests/shared/test_httpx_utils.py +++ b/tests/shared/test_httpx_utils.py @@ -1,16 +1,27 @@ -"""Tests for httpx2 utility functions.""" +"""Tests for the httpx2 helpers the client transports are built on.""" + +from collections.abc import AsyncGenerator, AsyncIterator +from typing import Any import httpx2 +import pytest -from mcp.shared._httpx_utils import create_mcp_http_client +from mcp.shared._httpx_utils import ( + create_mcp_http_client, + request_within_origin, + sse_within_origin, + stream_within_origin, +) + +pytestmark = pytest.mark.anyio -def test_default_settings(): - """Test that default settings are applied correctly.""" +def test_default_client_uses_mcp_timeouts_and_httpx_redirect_default(): + """The factory applies the transports' timeouts and leaves redirect following to the transports.""" client = create_mcp_http_client() - assert client.follow_redirects is True - assert client.timeout.connect == 30.0 + assert client.follow_redirects is False + assert client.timeout == httpx2.Timeout(30.0, read=300.0) def test_custom_parameters(): @@ -22,3 +33,253 @@ def test_custom_parameters(): assert client.headers["Authorization"] == "Bearer token" assert client.timeout.connect == 60.0 + + +class _Body(httpx2.AsyncByteStream): + """A response body served as a real stream, recording whether the client closed it.""" + + def __init__(self, data: bytes, closed: list[bool]) -> None: + self._data = data + self._closed = closed + + async def __aiter__(self) -> AsyncIterator[bytes]: + yield self._data + + async def aclose(self) -> None: + self._closed.append(True) + + +def _recording_client( + redirects: dict[str, tuple[int, str]], **client_kwargs: Any +) -> tuple[httpx2.AsyncClient, list[str], list[bool]]: + """A client whose server redirects each URL in `redirects` (status, Location) and answers 200 + to anything else; plus the `METHOD url` lines the server received and one entry per redirect + response body the client closed.""" + received: list[str] = [] + closed: list[bool] = [] + + def serve(request: httpx2.Request) -> httpx2.Response: + received.append(f"{request.method} {request.url}") + if str(request.url) in redirects: + status, location = redirects[str(request.url)] + return httpx2.Response(status, headers={"location": location}, stream=_Body(b"moved", closed)) + return httpx2.Response(200, text=request.content.decode() or "ok") + + return httpx2.AsyncClient(transport=httpx2.MockTransport(serve), **client_kwargs), received, closed + + +@pytest.mark.parametrize( + ("url", "location"), + [ + ("http://mcp.example/mcp", "http://mcp.example/mcp/"), + ("http://mcp.example/mcp", "/other/path"), + ("http://mcp.example:8080/mcp", "http://mcp.example:8080/v2/mcp"), + ("http://mcp.example/mcp", "http://MCP.EXAMPLE:80/mcp/"), + ("http://mcp.example/mcp", "https://mcp.example:443/mcp"), + ], +) +async def test_redirect_within_origin_is_followed_with_method_and_body(url: str, location: str): + """A redirect that stays on the request's origin (or upgrades it to https) is followed, and a + 307 keeps the method and body (SDK-defined policy; the re-send itself is httpx2's).""" + client, received, closed = _recording_client({url: (307, location)}) + + async with client, stream_within_origin(client, "POST", url, content=b"payload") as response: + await response.aread() + + assert response.status_code == 200 + assert response.text == "payload" + assert received == [f"POST {url}", f"POST {httpx2.URL(url).join(location)}"] + assert closed == [True] + + +@pytest.mark.parametrize( + "location", + [ + "http://other.example/mcp", + "http://mcp.example:8080/mcp", + "http://sub.mcp.example/mcp", + "https://mcp.example:8443/mcp", + "ftp://mcp.example/mcp", + ], +) +async def test_redirect_outside_origin_is_not_followed(location: str): + """A redirect to another origin is handed back unfollowed, the way httpx2 hands back a redirect + with following off, and the location is never requested (SDK-defined policy).""" + url = "http://mcp.example/mcp" + client, received, closed = _recording_client({url: (307, location)}) + + async with client, stream_within_origin(client, "POST", url, content=b"payload") as response: + pass + + assert response.status_code == 307 + assert response.next_request is not None + assert response.next_request.url == location + assert received == [f"POST {url}"] + assert closed == [True] + + +@pytest.mark.parametrize("status", [301, 302, 303]) +async def test_method_changing_redirect_of_a_post_is_not_followed(status: int): + """httpx2 turns a POST into a body-less GET for 301/302/303, which would drop the message, so a + same-origin redirect with one of those codes is handed back unfollowed (SDK-defined).""" + url = "http://mcp.example/mcp" + client, received, _ = _recording_client({url: (status, "/mcp/")}) + + async with client, stream_within_origin(client, "POST", url, content=b"payload") as response: + pass + + assert response.status_code == status + assert received == [f"POST {url}"] + + +@pytest.mark.parametrize("status", [301, 302, 303, 307, 308]) +async def test_same_origin_redirect_of_a_get_is_followed_for_every_redirect_status(status: int): + """A GET keeps its method under every redirect status, so the SSE GET follows all of them + within the origin (SDK-defined policy over httpx2's method rules).""" + url = "http://mcp.example/sse" + client, received, _ = _recording_client({url: (status, "/sse/")}) + + async with client, stream_within_origin(client, "GET", url) as response: + await response.aread() + + assert response.status_code == 200 + assert received == [f"GET {url}", "GET http://mcp.example/sse/"] + + +async def test_https_to_http_on_same_host_is_outside_origin(): + """Only the upgrade direction counts as staying on the origin; a downgrade is not followed.""" + url = "https://mcp.example/mcp" + client, received, _ = _recording_client({url: (302, "http://mcp.example/mcp")}) + + async with client, stream_within_origin(client, "GET", url) as response: + pass + + assert response.status_code == 302 + assert received == [f"GET {url}"] + + +async def test_client_configured_to_follow_redirects_is_still_scoped_to_origin(): + """The client's own follow_redirects=True does not widen the policy: the transport helper + decides per request (SDK-defined).""" + url = "http://mcp.example/mcp" + client, received, _ = _recording_client({url: (307, "http://other.example/mcp")}, follow_redirects=True) + + async with client, stream_within_origin(client, "POST", url) as response: + pass + + assert response.status_code == 307 + assert received == [f"POST {url}"] + + +async def test_redirect_past_the_client_max_redirects_budget_is_handed_back_unfollowed(): + """Same-origin hops are bounded by the client's max_redirects; the redirect after that is not + followed but handed back like any other, so a loop fails the one call rather than raising + (SDK-defined; max_redirects=0 therefore means "follow none").""" + url = "http://mcp.example/a" + client, received, closed = _recording_client( + { + "http://mcp.example/a": (307, "/b"), + "http://mcp.example/b": (307, "/c"), + "http://mcp.example/c": (307, "/d"), + }, + max_redirects=2, + ) + + async with client: + response = await request_within_origin(client, "GET", url) + + assert response.status_code == 307 + assert response.next_request is not None + assert response.next_request.url == "http://mcp.example/d" + assert received == ["GET http://mcp.example/a", "GET http://mcp.example/b", "GET http://mcp.example/c"] + assert closed == [True, True, True] + + +async def test_redirect_location_with_userinfo_is_not_followed(): + """A Location carrying user:password is handed back unfollowed even within the origin, since + httpx2 would otherwise send that userinfo as Basic auth (SDK-defined).""" + url = "http://mcp.example/mcp" + client, received, _ = _recording_client({url: (307, "http://user:secret@mcp.example/mcp/")}) + + async with client, stream_within_origin(client, "POST", url) as response: + pass + + assert response.status_code == 307 + assert received == [f"POST {url}"] + + +async def test_userinfo_of_the_configured_url_kept_by_a_relative_location_is_followed(): + """Userinfo the caller put in the endpoint URL is carried over by a relative Location (URL join + keeps the authority); that is the caller's own credential for the same origin, so the redirect + is followed as httpx2 itself would (SDK-defined).""" + url = "http://user:secret@mcp.example/mcp" + client, received, _ = _recording_client({url: (307, "/mcp/")}) + + async with client, stream_within_origin(client, "POST", url, content=b"payload") as response: + await response.aread() + + assert response.status_code == 200 + assert received == [f"POST {url}", "POST http://user:secret@mcp.example/mcp/"] + + +async def test_request_within_origin_returns_a_read_response(): + """The non-streaming form hands back a response whose body is already read.""" + url = "http://mcp.example/mcp" + client, received, _ = _recording_client({url: (307, "/mcp/")}) + + async with client: + response = await request_within_origin(client, "DELETE", url) + + assert response.status_code == 200 + assert response.text == "ok" + assert received == [f"DELETE {url}", "DELETE http://mcp.example/mcp/"] + + +async def test_sse_within_origin_sends_event_stream_headers_and_caller_headers(): + """The SSE form asks for an event stream exactly as client.sse() does, merged case-insensitively + with the caller's headers, and yields an EventSource over the final response.""" + seen: list[httpx2.Headers] = [] + + def serve(request: httpx2.Request) -> httpx2.Response: + seen.append(request.headers) + return httpx2.Response(200, headers={"content-type": "text/event-stream"}, text="data: hello\n\n") + + client = httpx2.AsyncClient(transport=httpx2.MockTransport(serve)) + async with client: + async with sse_within_origin(client, "http://mcp.example/sse") as source: + events = [event.data async for event in source] + async with sse_within_origin(client, "http://mcp.example/sse", headers={"accept": "x/y", "k": "v"}): + pass + + assert events == ["hello"] + assert seen[0]["accept"] == "text/event-stream" + assert seen[0]["cache-control"] == "no-store" + assert seen[1].get_list("accept") == ["x/y"] + assert seen[1]["cache-control"] == "no-store" + assert seen[1]["k"] == "v" + + +async def test_auth_flow_requests_are_not_redirected(): + """Requests an httpx2 Auth flow issues while a transport request is in flight (a token refresh, + say) inherit the per-request no-follow setting, so a redirect on them is handed back to the + auth flow rather than followed (httpx2 behaviour the transports rely on).""" + received: list[str] = [] + + class TokenThenRequest(httpx2.Auth): + async def async_auth_flow(self, request: httpx2.Request) -> AsyncGenerator[httpx2.Request, httpx2.Response]: + token_response = yield httpx2.Request("POST", "http://mcp.example/token", content=b"grant") + request.headers["x-token-status"] = str(token_response.status_code) + yield request + + def serve(request: httpx2.Request) -> httpx2.Response: + received.append(f"{request.method} {request.url}") + if request.url.path == "/token": + return httpx2.Response(307, headers={"location": "http://other.example/token"}) + return httpx2.Response(200, text=request.headers["x-token-status"]) + + client = httpx2.AsyncClient(transport=httpx2.MockTransport(serve), auth=TokenThenRequest(), follow_redirects=True) + async with client: + response = await request_within_origin(client, "POST", "http://mcp.example/mcp") + + assert response.text == "307" + assert received == ["POST http://mcp.example/token", "POST http://mcp.example/mcp"] diff --git a/tests/shared/test_sse.py b/tests/shared/test_sse.py index c27dd69d..77d1b28a 100644 --- a/tests/shared/test_sse.py +++ b/tests/shared/test_sse.py @@ -3,14 +3,13 @@ import json from collections.abc import AsyncGenerator from typing import Any -from unittest.mock import AsyncMock, MagicMock, Mock +from unittest.mock import Mock from urllib.parse import urlparse import anyio import httpx2 import mcp_types as types import pytest -from httpx2 import ServerSentEvent from inline_snapshot import snapshot from mcp_types import ( CallToolRequestParams, @@ -41,6 +40,7 @@ from mcp.server.sse import SseServerTransport from mcp.server.transport_security import TransportSecuritySettings from mcp.shared._httpx_utils import McpHttpClientFactory from mcp.shared.exceptions import MCPError +from mcp.shared.message import SessionMessage from tests.interaction.transports import StreamingASGITransport SERVER_NAME = "test_server_for_SSE" @@ -58,15 +58,13 @@ def in_process_client_factory(app: Starlette) -> McpHttpClientFactory: auth: httpx2.Auth | None = None, ) -> httpx2.AsyncClient: # The SSE GET runs until it observes a disconnect, so the bridge must let the - # application drain on close rather than cancelling it. follow_redirects matches - # create_mcp_http_client, the factory this one stands in for. + # application drain on close rather than cancelling it. return httpx2.AsyncClient( transport=StreamingASGITransport(app, cancel_on_close=False), base_url=BASE_URL, headers=headers, timeout=timeout, auth=auth, - follow_redirects=True, ) return factory @@ -400,10 +398,10 @@ async def test_sse_client_handles_empty_keepalive_pings() -> None: send an SSE event consisting of an event ID and an empty data field in order to prime the client to reconnect." - This test mocks the SSE event stream to include empty "message" events and - verifies the client skips them without crashing. + The event stream served here carries an endpoint event, an empty "message" + event (the case under test), then a real response; the client must skip the + empty one and deliver the response. """ - # Build a proper JSON-RPC response using types (not hardcoded strings) init_result = InitializeResult( protocol_version="2024-11-05", capabilities=ServerCapabilities(), @@ -415,41 +413,86 @@ async def test_sse_client_handles_empty_keepalive_pings() -> None: result=init_result.model_dump(by_alias=True, exclude_none=True), ) response_json = response.model_dump_json(by_alias=True, exclude_none=True) + event_stream = ( + "event: endpoint\ndata: /messages/?session_id=abc123\n\n" + "event: message\ndata: \n\n" + f"event: message\ndata: {response_json}\n\n" + ) - # Mock SSE events using httpx2's ServerSentEvent: an endpoint event, an - # empty keep-alive ping (the case under test), then a real response. - mock_event_source = MagicMock() - mock_event_source.__aiter__.return_value = [ - ServerSentEvent(event="endpoint", data="/messages/?session_id=abc123"), - ServerSentEvent(event="message", data=""), - ServerSentEvent(event="message", data=response_json), - ] - mock_event_source.response.raise_for_status = MagicMock() + def serve(request: httpx2.Request) -> httpx2.Response: + assert request.url.path == "/sse" + return httpx2.Response(200, headers={"content-type": "text/event-stream"}, text=event_stream) - mock_sse = MagicMock() - mock_sse.__aenter__ = AsyncMock(return_value=mock_event_source) - mock_sse.__aexit__ = AsyncMock(return_value=None) - - mock_client = MagicMock() - mock_client.__aenter__ = AsyncMock(return_value=mock_client) - mock_client.__aexit__ = AsyncMock(return_value=None) - mock_client.sse = MagicMock(return_value=mock_sse) - mock_client.post = AsyncMock(return_value=MagicMock(status_code=200, raise_for_status=MagicMock())) - - def mock_factory( + def factory( headers: dict[str, str] | None = None, timeout: httpx2.Timeout | None = None, auth: httpx2.Auth | None = None, ) -> httpx2.AsyncClient: - return mock_client + return httpx2.AsyncClient(transport=httpx2.MockTransport(serve)) - async with sse_client("http://test/sse", httpx_client_factory=mock_factory) as (read_stream, _): - # Read the message - should skip the empty one and get the real response - msg = await read_stream.receive() - # If we get here without error, the empty message was skipped successfully - assert not isinstance(msg, Exception) - assert isinstance(msg.message, types.JSONRPCResponse) - assert msg.message.id == 1 + with anyio.fail_after(5): + async with sse_client("http://test/sse", httpx_client_factory=factory) as (read_stream, _): + msg = await read_stream.receive() + + assert isinstance(msg, SessionMessage) + assert isinstance(msg.message, types.JSONRPCResponse) + assert msg.message.id == 1 + + +@pytest.mark.anyio +async def test_sse_client_follows_redirect_within_origin_on_connect() -> None: + """SDK-defined: a redirect of the SSE GET that stays on the endpoint's origin is followed by + the transport itself, with a client left at httpx2's no-follow default.""" + received: list[str] = [] + + def serve(request: httpx2.Request) -> httpx2.Response: + received.append(str(request.url)) + if request.url.path == "/sse": + return httpx2.Response(307, headers={"location": "/sse/"}) + assert request.url.path == "/sse/" + return httpx2.Response( + 200, headers={"content-type": "text/event-stream"}, text="event: endpoint\ndata: /messages/\n\n" + ) + + def factory( + headers: dict[str, str] | None = None, + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient(transport=httpx2.MockTransport(serve)) + + with anyio.fail_after(5): + async with sse_client("http://test/sse", httpx_client_factory=factory): + pass + + assert received == ["http://test/sse", "http://test/sse/"] + + +@pytest.mark.anyio +async def test_sse_client_does_not_follow_redirect_to_another_origin_on_connect() -> None: + """SDK-defined: a redirect of the SSE GET to another origin is not followed, even with a client + configured to follow redirects: connecting fails with HTTPStatusError for the redirect response + and that origin is never contacted.""" + received: list[str] = [] + + def serve(request: httpx2.Request) -> httpx2.Response: + received.append(str(request.url)) + return httpx2.Response(307, headers={"location": "http://other.example/sse"}) + + def factory( + headers: dict[str, str] | None = None, + timeout: httpx2.Timeout | None = None, + auth: httpx2.Auth | None = None, + ) -> httpx2.AsyncClient: + return httpx2.AsyncClient(transport=httpx2.MockTransport(serve), follow_redirects=True) + + with anyio.fail_after(5): + with pytest.raises(httpx2.HTTPStatusError) as exc_info: + async with sse_client("http://test/sse", httpx_client_factory=factory): + pytest.fail("should not connect") # pragma: no cover + + assert exc_info.value.response.status_code == 307 + assert received == ["http://test/sse"] @pytest.mark.anyio diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index aeef25a2..655d9941 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -7,8 +7,10 @@ entirely in process. from __future__ import annotations as _annotations import json +import logging import time from collections.abc import AsyncIterator +from concurrent.futures import ThreadPoolExecutor from contextlib import asynccontextmanager from dataclasses import dataclass, field from typing import Any @@ -356,10 +358,11 @@ async def running_app( def make_client(app: Starlette, headers: dict[str, str] | None = None) -> httpx2.AsyncClient: - """An httpx2 client served in process by `app`, with create_mcp_http_client's redirect default. + """An httpx2 client served in process by `app`. - (Starlette's Mount 307-redirects the bare /mcp path to /mcp/, which the SDK's own client - factory follows.) + Starlette's Mount 307-redirects the bare /mcp path to /mcp/. The MCP transport follows that + same-origin redirect itself; `follow_redirects=True` is here for the tests in this file that + POST to /mcp with this client directly. """ return httpx2.AsyncClient( transport=StreamingASGITransport(app), base_url=BASE_URL, headers=headers, follow_redirects=True @@ -599,6 +602,97 @@ def test_streamable_http_transport_init_validation() -> None: StreamableHTTPServerTransport(mcp_session_id="test\n") +@pytest.mark.parametrize("idle_timeout", [0, -1, float("inf"), float("nan")]) +def test_streamable_http_transport_rejects_invalid_idle_timeout(idle_timeout: float) -> None: + """A transport's idle timeout must be a positive, finite number of seconds; without one it never expires.""" + with pytest.raises(ValueError) as exc_info: + StreamableHTTPServerTransport(mcp_session_id="valid-id", idle_timeout=idle_timeout) + assert str(exc_info.value) == "idle_timeout must be a positive, finite number of seconds" + assert StreamableHTTPServerTransport(mcp_session_id="valid-id").idle_scope is None + + +def test_streamable_http_transport_with_idle_timeout_can_be_created_outside_an_event_loop() -> None: + """The idle scope is only created once connect() is entered, so a transport with a timeout can be + constructed without a running event loop.""" + # A bare thread has no async context; this one does, courtesy of the suite's shared runner. + with ThreadPoolExecutor(max_workers=1) as pool: + transport = pool.submit(StreamableHTTPServerTransport, mcp_session_id="valid-id", idle_timeout=5).result() + assert transport.idle_scope is None + + +@pytest.mark.anyio +async def test_streamable_http_transport_creates_its_idle_scope_on_connect() -> None: + """Entering connect() creates the idle scope the host enters around the session's message loop.""" + transport = StreamableHTTPServerTransport(mcp_session_id="valid-id", idle_timeout=5) + async with transport.connect(): + assert isinstance(transport.idle_scope, anyio.CancelScope) + await transport.terminate() + + +async def _post_to_transport(transport: StreamableHTTPServerTransport, body: dict[str, Any]) -> int: + """POST `body` straight to `transport` in process, as a client that then holds the connection open, + and return the status it answered with.""" + assert transport.mcp_session_id is not None + scope: Scope = { + "type": "http", + "method": "POST", + "path": "/mcp", + "query_string": b"", + "headers": [ + (b"content-type", b"application/json"), + (b"accept", b"application/json, text/event-stream"), + (MCP_SESSION_ID_HEADER.encode(), transport.mcp_session_id.encode()), + ], + } + sent: list[Message] = [] + + async def send(message: Message) -> None: + sent.append(message) + + request_body, incoming = anyio.create_memory_object_stream[Message](1) + async with request_body, incoming: + await request_body.send({"type": "http.request", "body": json.dumps(body).encode(), "more_body": False}) + with anyio.fail_after(5): + await transport.handle_request(scope, incoming.receive, send) + return next(message["status"] for message in sent if message["type"] == "http.response.start") + + +@pytest.mark.anyio +async def test_transport_whose_idle_period_ran_out_answers_as_terminated() -> None: + """Once the idle scope has fired, a request that still reaches the transport is answered 404 and the + transport is terminated, instead of being dispatched into the message loop the host is leaving.""" + transport = StreamableHTTPServerTransport(mcp_session_id="valid-id", idle_timeout=5) + ping = {"jsonrpc": "2.0", "id": 1, "method": "ping"} + async with transport.connect(): + assert transport.idle_scope is not None + # Exactly what the scope's deadline passing does. + transport.idle_scope.cancel() + + assert await _post_to_transport(transport, ping) == 404 + assert transport.is_terminated + assert await _post_to_transport(transport, ping) == 404 + + +@pytest.mark.anyio +async def test_transport_reports_stream_closure_when_host_exits_without_terminating( + caplog: pytest.LogCaptureFixture, +) -> None: + """A host that leaves connect() without terminating the transport closes the streams under the + message router, which reports it rather than passing it off as a client disconnect.""" + caplog.set_level(logging.ERROR, logger="mcp.server.streamable_http") + transport = StreamableHTTPServerTransport(mcp_session_id="valid-id") + + async with transport.connect(): + pass + + assert not transport.is_terminated + assert [ + record.getMessage() + for record in caplog.records + if record.name == "mcp.server.streamable_http" and record.levelno == logging.ERROR + ] == ["Unexpected closure of read stream in message router"] + + @pytest.mark.anyio async def test_session_termination(basic_app: Starlette) -> None: """DELETE terminates the session, after which requests for it return 404.""" @@ -639,7 +733,7 @@ async def test_session_termination(basic_app: Starlette) -> None: json={"jsonrpc": "2.0", "method": "ping", "id": 2}, ) assert response.status_code == 404 - assert "Session has been terminated" in response.text + assert response.json()["error"]["message"] == "Session not found" @pytest.mark.anyio @@ -996,7 +1090,9 @@ async def test_streamable_http_client_get_stream(basic_app: Starlette) -> None: assert resource_update_found, "ResourceUpdatedNotification not received via GET stream" -def create_session_id_capturing_client(app: Starlette) -> tuple[httpx2.AsyncClient, list[str]]: +def create_session_id_capturing_client( + app: Starlette, transport: httpx2.AsyncBaseTransport | None = None +) -> tuple[httpx2.AsyncClient, list[str]]: """Create an in-process httpx2 client that captures the session ID from responses.""" captured_ids: list[str] = [] @@ -1006,9 +1102,8 @@ def create_session_id_capturing_client(app: Starlette) -> tuple[httpx2.AsyncClie captured_ids.append(session_id) client = httpx2.AsyncClient( - transport=StreamingASGITransport(app), + transport=transport or StreamingASGITransport(app), base_url=BASE_URL, - follow_redirects=True, event_hooks={"response": [capture_session_id]}, ) return client, captured_ids @@ -1048,40 +1143,38 @@ async def test_streamable_http_client_session_termination(basic_app: Starlette) with pytest.raises(MCPError) as exc_info: # pragma: no branch await session.list_tools() assert exc_info.value.error.code == INVALID_REQUEST - assert "terminated" in exc_info.value.error.message.lower() + assert exc_info.value.error.message == "Session not found" @pytest.mark.anyio -async def test_streamable_http_client_session_termination_204( - basic_app: Starlette, monkeypatch: pytest.MonkeyPatch -) -> None: +async def test_streamable_http_client_session_termination_204(basic_app: Starlette) -> None: """Session termination also succeeds when the server answers the DELETE with 204. - This test patches the httpx2 client to return a 204 response for DELETEs. + The in-process server answers the DELETE with 200; a wrapping HTTP transport rewrites that to + 204 on the way back, which is what some servers send. """ - # Save the original delete method to restore later - original_delete = httpx2.AsyncClient.delete + class AnswerDeleteWith204(httpx2.AsyncBaseTransport): + def __init__(self, inner: StreamingASGITransport) -> None: + self.inner = inner - # Mock the client's delete method to return a 204 - async def mock_delete(self: httpx2.AsyncClient, *args: Any, **kwargs: Any) -> httpx2.Response: - # Call the original method to get the real response - response = await original_delete(self, *args, **kwargs) + async def handle_async_request(self, request: httpx2.Request) -> httpx2.Response: + response = await self.inner.handle_async_request(request) + if request.method != "DELETE" or response.status_code != 200: + return response + await response.aread() + return httpx2.Response(204, headers=response.headers, request=request) - # Create a new response with 204 status code but same headers - mocked_response = httpx2.Response( - 204, - headers=response.headers, - content=response.content, - request=response.request, - ) - return mocked_response + async def __aenter__(self) -> AnswerDeleteWith204: + await self.inner.__aenter__() + return self - # Apply the patch to the httpx2 client - monkeypatch.setattr(httpx2.AsyncClient, "delete", mock_delete) + async def __aexit__(self, *args: Any) -> None: + await self.inner.__aexit__(*args) - # Use httpx2 client with event hooks to capture session ID - httpx_client, captured_ids = create_session_id_capturing_client(basic_app) + httpx_client, captured_ids = create_session_id_capturing_client( + basic_app, transport=AnswerDeleteWith204(StreamingASGITransport(basic_app)) + ) async with httpx_client: async with streamable_http_client(f"{BASE_URL}/mcp", http_client=httpx_client) as ( @@ -1111,7 +1204,7 @@ async def test_streamable_http_client_session_termination_204( with pytest.raises(MCPError) as exc_info: # pragma: no branch await session.list_tools() assert exc_info.value.error.code == INVALID_REQUEST - assert "terminated" in exc_info.value.error.message.lower() + assert exc_info.value.error.message == "Session not found" @pytest.mark.anyio @@ -1312,7 +1405,9 @@ async def _handle_context_list_tools( ) -async def _handle_context_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult: +async def _handle_context_call_tool( + ctx: ServerRequestContext[Any, Request], params: CallToolRequestParams +) -> CallToolResult: assert params.name in ("echo_headers", "echo_context") assert isinstance(ctx.request, Request) diff --git a/tests/types/test_methods.py b/tests/types/test_methods.py index bd0d1f9b..d38dfa09 100644 --- a/tests/types/test_methods.py +++ b/tests/types/test_methods.py @@ -454,11 +454,20 @@ def test_elicit_result_surface_accepts_null_content_values_at_every_version_that surface.model_validate({"action": "accept", "content": {"name": "x", "age": None}}) -def test_server_capabilities_extensions_with_null_json_value_round_trips_at_2026(): - """Spec `JSONValue` includes `null`; the ts->json render dropped it from the vendored schema.""" - raw: dict[str, Any] = {"extensions": {"x": {"k": None}}} - parsed = v2026.ServerCapabilities.model_validate(raw) - assert parsed.model_dump(mode="json")["extensions"] == {"x": {"k": None}} +def test_discovery_capabilities_preserve_nested_json_values() -> None: + """Spec `JSONValue` permits nested containers and every JSON scalar, including numbers and null.""" + extensions = {"x": {"nested": [None, True, 1, 1.5, "text", {"child": [False]}]}} + raw = { + "supportedVersions": ["2026-07-28"], + "capabilities": {"extensions": extensions}, + "resultType": "complete", + "ttlMs": 0, + "cacheScope": "private", + } + parsed = methods.parse_server_result("server/discover", "2026-07-28", raw) + assert isinstance(parsed, types.DiscoverResult) + assert parsed.capabilities.extensions == extensions + assert methods.serialize_server_result("server/discover", "2026-07-28", raw) == raw def test_elicit_request_surface_accepts_loose_property_schemas(): diff --git a/tests/types/test_parity.py b/tests/types/test_parity.py index 35319921..fe8ac758 100644 --- a/tests/types/test_parity.py +++ b/tests/types/test_parity.py @@ -153,8 +153,6 @@ def _surface_classes(module: ModuleType) -> list[tuple[str, type[BaseModel]]]: continue if obj.__module__ != module.__name__ or obj.__name__ != name: continue # re-export or alias to another model - if getattr(obj, "__pydantic_root_model__", False): - continue # RootModel alias wrapper; the field-subset property does not apply out.append((f"{tail}.{name}", obj)) return out diff --git a/uv.lock b/uv.lock index a391152f..40b563e9 100644 --- a/uv.lock +++ b/uv.lock @@ -154,6 +154,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] +[[package]] +name = "blockbuster" +version = "1.5.27" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "forbiddenfruit", marker = "implementation_name == 'cpython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/c3/21678f5b979be2cbf0e68352f7330a84ea4e24674023e4de981c78a218cd/blockbuster-1.5.27.tar.gz", hash = "sha256:b8e9d988b9b91ba468c94530e219f26a00d3ff616b39ebf3da561a2a3eea9dd4", size = 96732, upload-time = "2026-08-17T23:53:13.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/c5/092e631bc1fba86f0a822be65c137c90a71b71ba0a0865e7e9a21f6ca05e/blockbuster-1.5.27-py3-none-any.whl", hash = "sha256:f0acf153d22a791bf5f142935332ef8530960ec215541b48a6037e6cea0a8645", size = 13517, upload-time = "2026-08-17T23:53:14.625Z" }, +] + [[package]] name = "certifi" version = "2025.8.3" @@ -436,62 +448,59 @@ toml = [ [[package]] name = "cryptography" -version = "46.0.7" +version = "50.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, - { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, - { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, - { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, - { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, - { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, - { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, - { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, - { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, - { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, - { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, - { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, - { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, - { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, - { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, - { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, - { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, - { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, - { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, - { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, - { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, - { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, - { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, - { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, - { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3e/e54cde8c01631a5a8226ccd617eab9e57fd5cfdad90f1a9e6bb570794631/cryptography-50.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5e34edd123674534acd70147f0ca331eaa2c74e6325fb2028c886aa26ba0b68c", size = 3963170, upload-time = "2026-07-31T14:24:51.968Z" }, + { url = "https://files.pythonhosted.org/packages/01/b6/0b9e125e90f3d2dcf599a218a899cda7326a3158cfa258723f0b398b08f6/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8eb5e1172eb569ea8a872796576e6a67c276351728b6455d5beb01242b027c6a", size = 4692441, upload-time = "2026-07-31T14:24:53.743Z" }, + { url = "https://files.pythonhosted.org/packages/53/c9/a5151588710785a96d7bc4de27d4cd62f263bbbcb203cfe29df537eb6505/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:910d11e1a385c654bf738bf3e6b8e6ed5de0f5610fcae2be9e5b398d8081d20e", size = 4699810, upload-time = "2026-07-31T14:24:55.746Z" }, + { url = "https://files.pythonhosted.org/packages/c7/1a/15b92b25eb6ce3089cd49377ae990a0f3ad485a510f968aed1f19dbdcdf2/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:62598a8a57f815db4c6259a4e97d857dab56697e7de8e8ab02352ab74da1995d", size = 4691924, upload-time = "2026-07-31T14:24:58.082Z" }, + { url = "https://files.pythonhosted.org/packages/62/15/219075012ab13e8905f3cd572204f4acb4b111df787104346b9bc0cea789/cryptography-50.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:07479a1cb08219ab719147e742e76090c9c773321959bb94946fffdd397a6437", size = 4699593, upload-time = "2026-07-31T14:24:59.951Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b5/c2c5fce26f0ee40d21bafe7f191d29a34b35a65ac4fe8a1191d1983612e9/cryptography-50.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c99c003e088647b8a5b7c145d6f78c335f6348332b62e142d411c4b63d1460b9", size = 3813796, upload-time = "2026-07-31T14:25:02.298Z" }, ] [[package]] @@ -555,7 +564,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -580,6 +589,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "forbiddenfruit" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756, upload-time = "2021-01-16T21:03:35.401Z" } + [[package]] name = "genson" version = "1.3.0" @@ -1026,6 +1041,7 @@ codegen = [ { name = "datamodel-code-generator" }, ] dev = [ + { name = "blockbuster" }, { name = "coverage", extra = ["toml"] }, { name = "dirty-equals" }, { name = "inline-snapshot" }, @@ -1084,6 +1100,7 @@ provides-extras = ["cli", "rich"] [package.metadata.requires-dev] codegen = [{ name = "datamodel-code-generator", specifier = "==0.57.0" }] dev = [ + { name = "blockbuster", specifier = ">=1.5.27" }, { name = "coverage", extras = ["toml"], specifier = ">=7.10.7,<=7.13" }, { name = "dirty-equals", specifier = ">=0.9.0" }, { name = "inline-snapshot", specifier = ">=0.23.0" }, @@ -1832,100 +1849,96 @@ wheels = [ [[package]] name = "pillow" -version = "12.1.1" +version = "12.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, - { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, - { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, - { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, - { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, - { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, - { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, - { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, - { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, - { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, - { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, - { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, - { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, - { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, - { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, - { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, - { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, - { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, - { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, - { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, - { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, - { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, - { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, - { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, - { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, - { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, - { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, - { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, - { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, - { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, - { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, - { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, - { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, - { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, - { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, - { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, - { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, - { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, - { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, - { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, - { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, - { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, - { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, - { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, + { url = "https://files.pythonhosted.org/packages/25/c2/669d88644cddb1485bd9534e63e8cf476c8e51cb3c3a1297677023505c0e/pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a", size = 5392418, upload-time = "2026-07-01T11:53:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ba/3762f376a2948e3036488d773a146e0ae6ecc2ca03ac20e2615bd0b2ba02/pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7", size = 4785287, upload-time = "2026-07-01T11:53:29.761Z" }, + { url = "https://files.pythonhosted.org/packages/07/50/b5d688cc9c52d4482f3d5bcab6ce20bc2a74a85d2343841c907444a3be2c/pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f", size = 6253754, upload-time = "2026-07-01T11:53:32.298Z" }, + { url = "https://files.pythonhosted.org/packages/4e/89/36f4cd76cf4baf05c50ababb976249153f18c959171c7f6ba09a6f217260/pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec", size = 6925605, upload-time = "2026-07-01T11:53:34.487Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c0/4de58cf6633b9e3a6061ef4be6fb91fc3c90b812ece886f531e3c523d777/pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468", size = 6327788, upload-time = "2026-07-01T11:53:36.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/3c/14d53682a19550dbbaf3b598f807d5457646c510805a44c7d7891cd1cd1a/pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed", size = 7036288, upload-time = "2026-07-01T11:53:38.712Z" }, + { url = "https://files.pythonhosted.org/packages/38/1d/36279e3c77efe034e4cc2b0393ee74ffdb5a62391dacbf9b916154f5f0b8/pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1", size = 6472396, upload-time = "2026-07-01T11:53:40.781Z" }, + { url = "https://files.pythonhosted.org/packages/48/7c/8fa0039574c476d7c6fa57dd7c32a130436877c6ec1e5ce1cc8ec44878c1/pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb", size = 7226887, upload-time = "2026-07-01T11:53:42.764Z" }, + { url = "https://files.pythonhosted.org/packages/fa/17/e324be141d173c1c919428066c3259f21c1b8982e564e01a4a81e96dbdcf/pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f", size = 2568039, upload-time = "2026-07-01T11:53:45.372Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" }, + { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" }, + { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" }, + { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" }, + { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" }, + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" }, + { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" }, + { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" }, ] [[package]] @@ -2128,11 +2141,14 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, ] [package.optional-dependencies] @@ -2142,15 +2158,15 @@ crypto = [ [[package]] name = "pymdown-extensions" -version = "11.0" +version = "11.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/47/67/f1e79672a5f91985577c7984c9709ca110e4fd37fe7fd167b60422e6ccc2/pymdown_extensions-11.0.tar.gz", hash = "sha256:8269cef0247f9e2d0a62fcea10860aba05c1cbab5470fd4b63230b96434dc589", size = 857049, upload-time = "2026-06-23T02:27:45.146Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b6/1ae53367e28b9cffa3be7574e13fbe4589694272fd47710fbdbafd3d63c6/pymdown_extensions-11.0-py3-none-any.whl", hash = "sha256:fbc4acb641814fa9d17521bbd21a5240ef739a662f11c06330c4b78c93e954d6", size = 269415, upload-time = "2026-06-23T02:27:43.826Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, ] [[package]] @@ -2168,7 +2184,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.4.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -2179,9 +2195,9 @@ dependencies = [ { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -2250,20 +2266,20 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.1.1" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] name = "python-multipart" -version = "0.0.22" +version = "0.0.32" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" }, + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, ] [[package]] @@ -2636,15 +2652,15 @@ wheels = [ [[package]] name = "starlette" -version = "0.49.1" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/3f/507c21db33b66fb027a332f2cb3abbbe924cc3a79ced12f01ed8645955c9/starlette-0.49.1.tar.gz", hash = "sha256:481a43b71e24ed8c43b11ea02f5353d77840e01480881b8cb5a26b8cae64a8cb", size = 2654703, upload-time = "2025-10-28T17:34:10.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/51/da/545b75d420bb23b5d494b0517757b351963e974e79933f01e05c929f20a6/starlette-0.49.1-py3-none-any.whl", hash = "sha256:d92ce9f07e4a3caa3ac13a79523bd18e3bc0042bb8ff2d759a8e7dd0e1859875", size = 74175, upload-time = "2025-10-28T17:34:09.13Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, ] [[package]] @@ -2786,11 +2802,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]]