Skip to content

Fix stateful session context propagation and affinity forwarding for appropriate list tools response#2974

Merged
crivetimihai merged 8 commits intomainfrom
2973_persist_serverid
Feb 17, 2026
Merged

Fix stateful session context propagation and affinity forwarding for appropriate list tools response#2974
crivetimihai merged 8 commits intomainfrom
2973_persist_serverid

Conversation

@kevalmahajan
Copy link
Copy Markdown
Member

@kevalmahajan kevalmahajan commented Feb 16, 2026

🐛 Bug-fix PR

Closes #2973

📌 Summary

Fixed a bug where server_id and other request context (headers, user auth) were lost when using stateful sessions and server_id was lost when using session_affinity. It ensures that:

Long-lived session tasks can dynamically recover context from the active request.
Internal session affinity forwarding for POST requests correctly preserves the server_id.

🔁 Reproduction Steps

  1. Configure MCPGATEWAY_USE_STATEFUL_SESSIONS=true and MCPGATEWAY_SESSION_AFFINITY_ENABLED=true.
  2. Connect to /servers/server-id/mcp and list the tools
    Before fix: Returns tools for "default_server_id" (empty/all).
    After fix: Returns tools strictly for <id>.

🐞 Root Cause

Two separate issues contributed to this bug:

  1. ContextVar Loss: in stateful sessions, the run_server task is long-lived and spawned separately. The contextvars (server_id_var) set during the initial connection or previous requests do not propagate to this isolated task context, causing handlers to see the default value.

  2. Forwarding Data Loss: When session affinity is enabled, stateful POST requests are forwarded internally to the /rpc endpoint in main.py. The original streamablehttp_transport extracted server_id from the URL, but
    main.py's /rpc endpoint expects it in the JSON body (params). The forwarding logic was not injecting the server_id from the URL into the JSON body, so main.py received server_id=None.

💡 Fix Description

  1. Dynamic Context Recovery: Introduced _get_request_context_or_default helper in streamablehttp_transport.py. This function first checks contextvars (fast path). If missing (stateful case), it inspects mcp_app.request_context to retrieve the underlying starlette.requests.Request object and extracts server_id, headers, and re-verifies user auth via
    verify_credentials.

  2. Affinity Forwarding Injection: Updated handle_streamable_http to extract the server_id from the URL regex and inject it into the params of the JSON-RPC body before forwarding the request to /rpc. This ensures correct routing in main.py.

🧪 Verification

Check Command Status
Lint suite make lint
Unit tests make test
Coverage ≥ 80 % make coverage
Manual regression no longer fails steps / screenshots

📐 MCP Compliance (if relevant)

  • Matches current MCP spec
  • No breaking change to MCP clients

✅ Checklist

  • Code formatted (make black isort pre-commit)
  • No secrets/credentials committed- [ ]

@kevalmahajan kevalmahajan marked this pull request as draft February 16, 2026 13:56
@kevalmahajan kevalmahajan marked this pull request as ready for review February 16, 2026 15:50
@kevalmahajan kevalmahajan added the bug Something isn't working label Feb 17, 2026
@crivetimihai crivetimihai self-assigned this Feb 17, 2026
@crivetimihai crivetimihai added this to the Release 1.0.0-RC1 milestone Feb 17, 2026
kevalmahajan and others added 5 commits February 17, 2026 18:08
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
- Remove debug log statement that leaked auth headers and user context
  at INFO level (security: token/PII exposure in production logs)
- Apply _get_request_context_or_default() to call_tool, read_resource,
  and list_resource_templates for consistent context recovery in
  stateful sessions (previously only list_* handlers were updated)
- Fix server_id injection to create params dict when absent, ensuring
  JSON-RPC requests without params still get proper server scoping
- Remove orphaned blank line in list_resources left from prior edit
- Clean up test imports: remove duplicate pytest, unused asyncio/types/
  transport imports, fix isort ordering
- Fix test property cleanup in test_list_resources_direct_proxy_meta_
  lookup_error to properly save/restore original descriptor
- Run black + isort on all changed files

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
…tion

Cover all branches in _get_request_context_or_default():
- Fast path when ContextVars are already populated
- Anonymous user string-to-dict conversion
- URL without /servers/{id}/mcp pattern (no server_id match)
- LookupError fallback (no active request context)
- Generic Exception fallback with error logging
- Cookie JWT token forwarding to require_auth_override

Cover server_id injection edge cases:
- JSON-RPC body without params key (params dict created)
- URL without server pattern (no injection occurs)

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
_get_request_context_or_default() was returning raw JWT payloads from
require_auth_override() without normalizing to the canonical user
context shape {email, teams, is_admin, is_authenticated} that MCP
handlers expect. This caused handlers to look up missing keys (email,
teams) from the raw JWT which uses different field names (sub,
token_use, nested user.is_admin).

Changes:
- Add _normalize_jwt_payload() that mirrors streamable_http_auth
  normalization: extracts email from sub/email, resolves teams via
  normalize_token_teams (API) or _resolve_teams_from_db_sync (session),
  and detects nested user.is_admin
- call_tool now extracts app_user_email from the already-recovered
  user_context instead of reading from stale ContextVar via
  get_user_email_from_context()
- Update test mocks to use realistic JWT payloads instead of
  pre-normalized dicts
- Add 6 dedicated tests for _normalize_jwt_payload covering API token,
  session token (admin/non-admin), nested is_admin, email fallback,
  and no-email edge case

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Review Changes Summary

Rebased onto main (clean, no conflicts) and addressed all review findings across three commits:

Commit 1: fix: address review findings for stateful session context propagation

  • Security: Removed debug log statement that leaked auth headers and user context at INFO level (token/PII exposure)
  • Consistency: Applied _get_request_context_or_default() to call_tool, read_resource, and list_resource_templates — previously only list_* handlers were updated
  • Bug fix: Fixed server_id injection to create params dict when absent from JSON-RPC body, ensuring paramless requests still get proper server scoping
  • Cleanup: Removed orphaned blank line, cleaned up duplicate/unused test imports, fixed test property cleanup, ran black + isort

Commit 2: test: add missing coverage for context propagation and affinity injection

  • Added 8 tests covering all branches in _get_request_context_or_default():
    • Fast path (ContextVars populated), anonymous user string-to-dict conversion, URL without server_id pattern, LookupError fallback, generic Exception fallback with error logging, cookie JWT token forwarding
  • Added server_id injection edge case tests: JSON-RPC body without params key, URL without server pattern

Commit 3: fix: normalize raw JWT payload in stateful session context recovery

Addresses data shape mismatch between require_auth_override() (raw JWT) and handler expectations (normalized context):

  • _normalize_jwt_payload(): New function that converts raw JWT fields (sub, token_use, nested user.is_admin) into canonical {email, teams, is_admin, is_authenticated} shape — mirrors streamable_http_auth normalization logic
    • Session tokens: resolves teams via _resolve_teams_from_db_sync()
    • API tokens: resolves teams via normalize_token_teams()
    • Detects admin from both top-level and nested user.is_admin
  • call_tool fix: Replaced get_user_email_from_context() (reads stale ContextVar) with extraction from already-recovered user_context dict
  • Test updates: Mocks now use realistic JWT payload shapes (sub, token_use, teams) instead of pre-normalized dicts; added 6 dedicated _normalize_jwt_payload unit tests

Test results

All 246 unit tests pass. Security review found no exploitable vulnerabilities.

Verify call_tool passes app_user_email from the recovered fallback
context (via _get_request_context_or_default) rather than reading
from the stale user_context_var ContextVar. This prevents regression
of the stateful session identity propagation fix.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
…DAR)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
@crivetimihai
Copy link
Copy Markdown
Member

Pre-existing issues identified during review

Two pre-existing gaps were identified during the review of this PR. Neither was introduced by this PR — they exist on main and in the original forwarding code. Filed as follow-up bugs:

1. [HIGH] server_id not injected for internally-forwarded Streamable HTTP requests — #3018

The internally-forwarded branch (x-forwarded-internally=true) posts the original JSON-RPC body to /rpc without injecting server_id from the URL path. The local-owner branch (added in this PR) correctly injects it. Cross-worker forwarded requests to /servers/{id}/mcp will lose server scoping.

2. [MEDIUM] Fallback auth token precedence differs from middleware — #3019

The stateful session fallback path resolves identity via require_auth() which prioritizes cookie tokens over Authorization headers. The primary middleware (streamable_http_auth) uses Authorization header only. If both exist with different users, identity can drift. Low practical risk — requires unusual conditions (both cookie and header with different identities).

@crivetimihai crivetimihai merged commit 6f3fd3a into main Feb 17, 2026
54 checks passed
@crivetimihai crivetimihai deleted the 2973_persist_serverid branch February 17, 2026 21:00
@crivetimihai crivetimihai added the revisit Revisit this PR at a later date to address further issues, or if problems arise. label Feb 17, 2026
vishu-bh pushed a commit that referenced this pull request Feb 18, 2026
… appropriate list tools response (#2974)

* maintain server_id context with session affinity

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* test cases for new improvements

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix: address review findings for stateful session context propagation

- Remove debug log statement that leaked auth headers and user context
  at INFO level (security: token/PII exposure in production logs)
- Apply _get_request_context_or_default() to call_tool, read_resource,
  and list_resource_templates for consistent context recovery in
  stateful sessions (previously only list_* handlers were updated)
- Fix server_id injection to create params dict when absent, ensuring
  JSON-RPC requests without params still get proper server scoping
- Remove orphaned blank line in list_resources left from prior edit
- Clean up test imports: remove duplicate pytest, unused asyncio/types/
  transport imports, fix isort ordering
- Fix test property cleanup in test_list_resources_direct_proxy_meta_
  lookup_error to properly save/restore original descriptor
- Run black + isort on all changed files

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add missing coverage for context propagation and affinity injection

Cover all branches in _get_request_context_or_default():
- Fast path when ContextVars are already populated
- Anonymous user string-to-dict conversion
- URL without /servers/{id}/mcp pattern (no server_id match)
- LookupError fallback (no active request context)
- Generic Exception fallback with error logging
- Cookie JWT token forwarding to require_auth_override

Cover server_id injection edge cases:
- JSON-RPC body without params key (params dict created)
- URL without server pattern (no injection occurs)

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: normalize raw JWT payload in stateful session context recovery

_get_request_context_or_default() was returning raw JWT payloads from
require_auth_override() without normalizing to the canonical user
context shape {email, teams, is_admin, is_authenticated} that MCP
handlers expect. This caused handlers to look up missing keys (email,
teams) from the raw JWT which uses different field names (sub,
token_use, nested user.is_admin).

Changes:
- Add _normalize_jwt_payload() that mirrors streamable_http_auth
  normalization: extracts email from sub/email, resolves teams via
  normalize_token_teams (API) or _resolve_teams_from_db_sync (session),
  and detects nested user.is_admin
- call_tool now extracts app_user_email from the already-recovered
  user_context instead of reading from stale ContextVar via
  get_user_email_from_context()
- Update test mocks to use realistic JWT payloads instead of
  pre-normalized dicts
- Add 6 dedicated tests for _normalize_jwt_payload covering API token,
  session token (admin/non-admin), nested is_admin, email fallback,
  and no-email edge case

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add regression test for call_tool recovered email propagation

Verify call_tool passes app_user_email from the recovered fallback
context (via _get_request_context_or_default) rather than reading
from the stale user_context_var ContextVar. This prevents regression
of the stateful session identity propagation fix.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add missing docstring params for _normalize_jwt_payload (flake8 DAR)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Vishu Bhatnagar <vishu.bhatnagar@ibm.com>
KKNithin pushed a commit to KKNithin/mcp-context-forge that referenced this pull request Feb 19, 2026
… appropriate list tools response (IBM#2974)

* maintain server_id context with session affinity

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* test cases for new improvements

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix: address review findings for stateful session context propagation

- Remove debug log statement that leaked auth headers and user context
  at INFO level (security: token/PII exposure in production logs)
- Apply _get_request_context_or_default() to call_tool, read_resource,
  and list_resource_templates for consistent context recovery in
  stateful sessions (previously only list_* handlers were updated)
- Fix server_id injection to create params dict when absent, ensuring
  JSON-RPC requests without params still get proper server scoping
- Remove orphaned blank line in list_resources left from prior edit
- Clean up test imports: remove duplicate pytest, unused asyncio/types/
  transport imports, fix isort ordering
- Fix test property cleanup in test_list_resources_direct_proxy_meta_
  lookup_error to properly save/restore original descriptor
- Run black + isort on all changed files

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add missing coverage for context propagation and affinity injection

Cover all branches in _get_request_context_or_default():
- Fast path when ContextVars are already populated
- Anonymous user string-to-dict conversion
- URL without /servers/{id}/mcp pattern (no server_id match)
- LookupError fallback (no active request context)
- Generic Exception fallback with error logging
- Cookie JWT token forwarding to require_auth_override

Cover server_id injection edge cases:
- JSON-RPC body without params key (params dict created)
- URL without server pattern (no injection occurs)

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: normalize raw JWT payload in stateful session context recovery

_get_request_context_or_default() was returning raw JWT payloads from
require_auth_override() without normalizing to the canonical user
context shape {email, teams, is_admin, is_authenticated} that MCP
handlers expect. This caused handlers to look up missing keys (email,
teams) from the raw JWT which uses different field names (sub,
token_use, nested user.is_admin).

Changes:
- Add _normalize_jwt_payload() that mirrors streamable_http_auth
  normalization: extracts email from sub/email, resolves teams via
  normalize_token_teams (API) or _resolve_teams_from_db_sync (session),
  and detects nested user.is_admin
- call_tool now extracts app_user_email from the already-recovered
  user_context instead of reading from stale ContextVar via
  get_user_email_from_context()
- Update test mocks to use realistic JWT payloads instead of
  pre-normalized dicts
- Add 6 dedicated tests for _normalize_jwt_payload covering API token,
  session token (admin/non-admin), nested is_admin, email fallback,
  and no-email edge case

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add regression test for call_tool recovered email propagation

Verify call_tool passes app_user_email from the recovered fallback
context (via _get_request_context_or_default) rather than reading
from the stale user_context_var ContextVar. This prevents regression
of the stateful session identity propagation fix.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add missing docstring params for _normalize_jwt_payload (flake8 DAR)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
crivetimihai added a commit that referenced this pull request Feb 24, 2026
… appropriate list tools response (#2974)

* maintain server_id context with session affinity

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* test cases for new improvements

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix: address review findings for stateful session context propagation

- Remove debug log statement that leaked auth headers and user context
  at INFO level (security: token/PII exposure in production logs)
- Apply _get_request_context_or_default() to call_tool, read_resource,
  and list_resource_templates for consistent context recovery in
  stateful sessions (previously only list_* handlers were updated)
- Fix server_id injection to create params dict when absent, ensuring
  JSON-RPC requests without params still get proper server scoping
- Remove orphaned blank line in list_resources left from prior edit
- Clean up test imports: remove duplicate pytest, unused asyncio/types/
  transport imports, fix isort ordering
- Fix test property cleanup in test_list_resources_direct_proxy_meta_
  lookup_error to properly save/restore original descriptor
- Run black + isort on all changed files

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add missing coverage for context propagation and affinity injection

Cover all branches in _get_request_context_or_default():
- Fast path when ContextVars are already populated
- Anonymous user string-to-dict conversion
- URL without /servers/{id}/mcp pattern (no server_id match)
- LookupError fallback (no active request context)
- Generic Exception fallback with error logging
- Cookie JWT token forwarding to require_auth_override

Cover server_id injection edge cases:
- JSON-RPC body without params key (params dict created)
- URL without server pattern (no injection occurs)

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: normalize raw JWT payload in stateful session context recovery

_get_request_context_or_default() was returning raw JWT payloads from
require_auth_override() without normalizing to the canonical user
context shape {email, teams, is_admin, is_authenticated} that MCP
handlers expect. This caused handlers to look up missing keys (email,
teams) from the raw JWT which uses different field names (sub,
token_use, nested user.is_admin).

Changes:
- Add _normalize_jwt_payload() that mirrors streamable_http_auth
  normalization: extracts email from sub/email, resolves teams via
  normalize_token_teams (API) or _resolve_teams_from_db_sync (session),
  and detects nested user.is_admin
- call_tool now extracts app_user_email from the already-recovered
  user_context instead of reading from stale ContextVar via
  get_user_email_from_context()
- Update test mocks to use realistic JWT payloads instead of
  pre-normalized dicts
- Add 6 dedicated tests for _normalize_jwt_payload covering API token,
  session token (admin/non-admin), nested is_admin, email fallback,
  and no-email edge case

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add regression test for call_tool recovered email propagation

Verify call_tool passes app_user_email from the recovered fallback
context (via _get_request_context_or_default) rather than reading
from the stale user_context_var ContextVar. This prevents regression
of the stateful session identity propagation fix.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add missing docstring params for _normalize_jwt_payload (flake8 DAR)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Signed-off-by: Nithin Katta <Nithin.Katta@ibm.com>
cafalchio pushed a commit that referenced this pull request Feb 26, 2026
… appropriate list tools response (#2974)

* maintain server_id context with session affinity

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* test cases for new improvements

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* linting

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>

* fix: address review findings for stateful session context propagation

- Remove debug log statement that leaked auth headers and user context
  at INFO level (security: token/PII exposure in production logs)
- Apply _get_request_context_or_default() to call_tool, read_resource,
  and list_resource_templates for consistent context recovery in
  stateful sessions (previously only list_* handlers were updated)
- Fix server_id injection to create params dict when absent, ensuring
  JSON-RPC requests without params still get proper server scoping
- Remove orphaned blank line in list_resources left from prior edit
- Clean up test imports: remove duplicate pytest, unused asyncio/types/
  transport imports, fix isort ordering
- Fix test property cleanup in test_list_resources_direct_proxy_meta_
  lookup_error to properly save/restore original descriptor
- Run black + isort on all changed files

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add missing coverage for context propagation and affinity injection

Cover all branches in _get_request_context_or_default():
- Fast path when ContextVars are already populated
- Anonymous user string-to-dict conversion
- URL without /servers/{id}/mcp pattern (no server_id match)
- LookupError fallback (no active request context)
- Generic Exception fallback with error logging
- Cookie JWT token forwarding to require_auth_override

Cover server_id injection edge cases:
- JSON-RPC body without params key (params dict created)
- URL without server pattern (no injection occurs)

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: normalize raw JWT payload in stateful session context recovery

_get_request_context_or_default() was returning raw JWT payloads from
require_auth_override() without normalizing to the canonical user
context shape {email, teams, is_admin, is_authenticated} that MCP
handlers expect. This caused handlers to look up missing keys (email,
teams) from the raw JWT which uses different field names (sub,
token_use, nested user.is_admin).

Changes:
- Add _normalize_jwt_payload() that mirrors streamable_http_auth
  normalization: extracts email from sub/email, resolves teams via
  normalize_token_teams (API) or _resolve_teams_from_db_sync (session),
  and detects nested user.is_admin
- call_tool now extracts app_user_email from the already-recovered
  user_context instead of reading from stale ContextVar via
  get_user_email_from_context()
- Update test mocks to use realistic JWT payloads instead of
  pre-normalized dicts
- Add 6 dedicated tests for _normalize_jwt_payload covering API token,
  session token (admin/non-admin), nested is_admin, email fallback,
  and no-email edge case

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* test: add regression test for call_tool recovered email propagation

Verify call_tool passes app_user_email from the recovered fallback
context (via _get_request_context_or_default) rather than reading
from the stale user_context_var ContextVar. This prevents regression
of the stateful session identity propagation fix.

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

* fix: add missing docstring params for _normalize_jwt_payload (flake8 DAR)

Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>

---------

Signed-off-by: Keval Mahajan <mahajankeval23@gmail.com>
Signed-off-by: Mihai Criveti <crivetimihai@gmail.com>
Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working revisit Revisit this PR at a later date to address further issues, or if problems arise.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Server ID Context Dropped During Stateful Session/ Session Affinity Processing

2 participants