# MCP stdout को साफ़ रखना प्रोटोकॉल सीमा की सुरक्षा कैसे करता है?

stdio MCP सीमा तभी भरोसेमंद रहती है जब stdout पर मौजूद हर byte प्रोटोकॉल का हिस्सा हो और हर विकृत या अस्पष्ट request action executor तक पहुंचने से पहले रुक जाए। ऐसा parser जो junk को अस्वीकार कर दे, लेकिन आधी-अधूरी समझी गई request को HTTP या SSH तक पहुंचने दे, सबसे खतरनाक जगह पर विफल हो गया है।

टीमें अक्सर stdout noise को एक परेशान करने वाली interoperability bug मानती हैं। यह बहुत नरम निष्कर्ष है। जब कोई agent बाहरी कार्रवाइयां कर सकता है, तब एक अतिरिक्त banner बातचीत को असंतुलित कर सकता है, error response छिपा सकता है या किसी ढीले client को गलत response को गलत request से जोड़ने के लिए उकसा सकता है। सही test केवल यह नहीं जांचता कि process साफ़-सुथरे ढंग से बंद हुआ। वह साबित करता है कि wire पर आए विकृत input से कोई बाहरी प्रभाव पैदा नहीं हुआ।

## stdout contract हर line पर एक JSON-RPC message स्वीकार करता है

Model Context Protocol का stdio transport stdout पर JSON-RPC messages मांगता है। ये messages नई lines से अलग होने चाहिए और इस stream में कोई असंबंधित output नहीं मिलना चाहिए। यही नियम उलटी दिशा में भी लागू होता है: client stdin पर protocol messages भेजता है और उस channel का उपयोग log pipe की तरह नहीं करता।

यह बात तब तक स्पष्ट लगती है जब तक कोई package installer notice प्रिंट न कर दे, कोई dependency warning न लिख दे या कोई व्यक्ति startup path में अस्थायी `print()` न डाल दे। इंसानी terminal पर ये lines नुकसानरहित हैं। Protocol stream पर ये ऐसे bytes हैं जिनका अर्थ peer को निकालना होगा। Peer के पास यह जानने का सुरक्षित तरीका नहीं है कि `loading credentials` banner है, malformed result है, response का टुकड़ा है या किसी compromised exchange की शुरुआत।

Transport contract के तीन हिस्से हैं, जिन्हें tests में साफ़-साफ़ लिखना चाहिए:

- हर पूरी line UTF-8 में decode हो और एक JSON value के रूप में parse हो।
- वह value उस दिशा के लिए मान्य JSON-RPC request, response या notification shape हो।
- Framing, JSON, protocol, schema और authorization checks पूरे होने तक कोई action शुरू न हो।

पहली शर्त noise पकड़ती है। दूसरी ऐसे object को पकड़ती है जो valid JSON तो है, लेकिन MCP message नहीं है। तीसरी उस गलती से बचाती है जो सबसे अधिक मायने रखती है: शुरुआती parsing को execution की अनुमति समझ लेना।

JSON-RPC 2.0 specification parse error और invalid request को अलग-अलग रखती है। Invalid JSON पर -32700 error code मिल सकता है, अगर peer framed response लिखने की स्थिति में हो। गलत request structure वाली JSON value invalid request होती है, जिसके लिए आम तौर पर -32600 इस्तेमाल होता है। ये codes conforming peer को failure समझने में मदद करते हैं। लेकिन ये यह नहीं बताते कि आपका अपना executor अछूता रहा या नहीं। Tests को इस सवाल का सीधा उत्तर देना चाहिए।

## एक banner authorized response को खराब कर सकता है

Startup banner authorization सफल होने के बाद भी पूरी तरह वैध action को तोड़ सकता है। इसलिए stdout की सफाई केवल inbound validation की चिंता नहीं है।

मान लीजिए server ने initialize request स्वीकार कर ली है और tool result लौटाने वाला है। Response के opening और closing lifecycle के बीच कोई dependency stdout पर `warning: configuration missing` लिख देती है। Strict client उस line को अस्वीकार करके disconnect हो जाता है। Loose client उसे छोड़कर आगे बढ़ता है। Strict client availability खो देता है। Loose client के पास अब ऐसा parser policy है जो security-sensitive exchange के भीतर अनधिकृत bytes स्वीकार करती है।

Loose client को सुविधाजनक दिखने के लिए पुरस्कृत न करें। जब client arbitrary lines को छोड़ देता है, तो उसके सामने ऐसे कई सवाल खड़े होते हैं जिनका वह भरोसेमंद उत्तर नहीं दे सकता। क्या उसने कोई diagnostic छोड़ दिया? क्या उसने किसी दूसरी request का response छोड़ दिया? क्या किसी wrapper ने कोई line दोहराई? क्या child process को प्रभावित कर सकने वाले attacker ने ऐसा text inject किया जिससे client state बदल जाए? Client stray byte sequence से intent का अनुमान नहीं लगा सकता।

Diagnostics stderr पर रखें। stderr के लिए अलग capture, retention और redaction rules तय करें, फिर process supervision को यह separation बनाए रखने दें। Release-only fault का एक आम कारण ऐसा wrapper है जो दोनों streams को मिला देता है, क्योंकि development के दौरान terminal output अधिक अच्छा दिखता था। वह wrapper चुपचाप protocol boundary नष्ट कर देता है।

किसी भी client library के उसे normalize करने से पहले outbound traffic को raw bytes के रूप में test करें। अगर library invalid sequence को exception में बदलकर मूल transcript छिपा देती है, तो failing test output में transcript सुरक्षित रखें। पहली खराब line को ठीक-ठीक देख पाना घंटों का अनुमान बचाता है।

## Denial action executor तक पहुंचने से पहले होना चाहिए

Rejected frame तभी सुरक्षित है जब external action करने वाला code उसे कभी देखे ही नहीं। Error response लौटाना उपयोगी है, लेकिन यही safety property नहीं है।

Final validation और authorization boundary के ठीक पीछे एक action recorder रखें। वास्तविक implementation में यह वह seam हो सकता है जो HTTP connection खोलने वाले function या SSH helper को wrap करे। Test में in-memory recorder या local fake service इस्तेमाल करें। Malformed-input tests को कभी real endpoint पर न भेजें और यह भरोसा न करें कि error path आपको बचा लेगा।

इस अंतर को समझना आसान नहीं, क्योंकि सामान्य request का रास्ता लंबा होता है। वह bytes के रूप में आती है, JSON बनती है, JSON-RPC object बनती है, MCP method call बनती है, tool schema से मिलाई जाती है, authorization decision पाती है और अंत में action बनती है। Developer इन सभी checks के पूरे होने से पहले audit record बना सकता है या request object तैयार कर सकता है। यह तभी स्वीकार्य है जब इनमें से कोई भी operation बाहरी दुनिया से संपर्क न कर सके और न ही किसी capability का उपयोग कर सके।

एक उपयोगी invariant यह है: executor को पूरी तरह typed और authorized action object मिले, raw JSON या partly validated request नहीं। अगर executor generic dictionary स्वीकार करता है, तो कोई न कोई उसे अंततः बहुत जल्दी call करेगा। Happy-path tests महीनों तक पास हो सकते हैं, क्योंकि सामान्य development में malformed frames कम ही आते हैं।

Rejection accounting को execution accounting से अलग रखें। Test यह कह पाने में सक्षम होना चाहिए कि parser ने एक frame reject किया, session बंद हुआ और executor को zero calls मिलीं। अगर ये घटनाएं एक ही व्यापक success या failure counter में मिल जाती हैं, तो आप साफ़ denial और बाद में विफल हुई action में फर्क नहीं कर पाएंगे।

## Test seam parsing के नीचे और execution के ऊपर रखें

सबसे छोटा उपयोगी harness एक strict wire validator और fake executor से बनता है। Validator bytes और protocol shape संभालता है। Fake executor काम करने की हर कोशिश record करता है। Production adapter अलग हो सकता है, लेकिन दोनों के बीच contract संकीर्ण रहना चाहिए।

यह Python example जानबूझकर छोटा रखा गया है। इसे `test_stdio_boundary.py` नाम से save करें, pytest install करें और `pytest -q test_stdio_boundary.py` चलाएं। `Gateway` को अपने gateway के adapter से बदलें, लेकिन `Recorder` के आसपास वाले assertions बनाए रखें।

```python
import io
import json
import pytest

class Recorder:
    def __init__(self):
        self.calls = []

    def execute(self, action):
        self.calls.append(action)
        return {'ok': True}

class Gateway:
    def __init__(self, executor):
        self.executor = executor
        self.closed = False

    def reject(self, code, reason):
        self.closed = True
        return {'jsonrpc': '2.0', 'id': None,
                'error': {'code': code, 'message': reason}}

    def receive_line(self, raw_line):
        if self.closed:
            return None
        try:
            message = json.loads(raw_line)
        except json.JSONDecodeError:
            return self.reject(-32700, 'parse error')

        if not isinstance(message, dict):
            return self.reject(-32600, 'invalid request')
        if message.get('jsonrpc') != '2.0':
            return self.reject(-32600, 'invalid request')
        if message.get('method') != 'tools/call':
            return self.reject(-32601, 'method not found')
        if not isinstance(message.get('id'), (str, int)) or isinstance(message.get('id'), bool):
            return self.reject(-32600, 'invalid request')

        params = message.get('params')
        if not isinstance(params, dict):
            return self.reject(-32602, 'invalid params')
        if not isinstance(params.get('name'), str):
            return self.reject(-32602, 'invalid params')
        if not isinstance(params.get('arguments', {}), dict):
            return self.reject(-32602, 'invalid params')

        action = {'name': params['name'], 'arguments': params.get('arguments', {})}
        result = self.executor.execute(action)
        return {'jsonrpc': '2.0', 'id': message['id'], 'result': result}

def frame(value):
    return json.dumps(value, separators=(',', ':')) + '\n'


def test_bad_lines_never_execute():
    bad_lines = [
        'debug: entering tool handler\n',
        '\u003chtml\u003egateway unavailable\u003c/html\u003e\n',
        '{not json}\n',
        'null\n',
        frame({'jsonrpc': '1.0', 'id': 4, 'method': 'tools/call', 'params': {}}),
        frame({'jsonrpc': '2.0', 'id': 4, 'method': 'tools/call', 'params': 'run'}),
    ]

    for raw_line in bad_lines:
        recorder = Recorder()
        gateway = Gateway(recorder)
        response = gateway.receive_line(raw_line)
        assert response['jsonrpc'] == '2.0'
        assert 'error' in response
        assert gateway.closed
        assert recorder.calls == []

def test_complete_valid_request_executes_once():
    recorder = Recorder()
    gateway = Gateway(recorder)
    request = frame({
        'jsonrpc': '2.0',
        'id': 9,
        'method': 'tools/call',
        'params': {'name': 'safe-test', 'arguments': {'value': 'green'}},
    })

    response = gateway.receive_line(request)
    assert response['result'] == {'ok': True}
    assert recorder.calls == [{'name': 'safe-test', 'arguments': {'value': 'green'}}]
```

यह पूरा MCP implementation नहीं है और इसे ऐसा बनना भी नहीं चाहिए। इसका उद्देश्य non-action property को executable बनाना है। आपका adapter वास्तविक stdin bytes को इसी तरह के test seam में feed कर सकता है और real request validator का उपयोग कर सकता है। Production server में abbreviated method handling को copy न करें।

ध्यान दें कि malformed या invalid inbound line के बाद session बंद करने का विकल्प चुना गया है। Protocol हर implementation को यह session policy अपनाने के लिए मजबूर नहीं करता। जब gateway external side effects नियंत्रित करता है, तब यह sensible default है, क्योंकि टूटी हुई line का अर्थ हो सकता है कि peer ने framing खो दी है। अगर आप साफ़ तौर पर framed invalid request के बाद session खुला रखते हैं, तो उस path की अलग जांच करें और साबित करें कि अगली valid request rejected request से कोई state inherit नहीं कर सकती।

## Runnable harness को bytes और effects दोनों देखने चाहिए

ऊपर का unit test inbound denial check करता है। उस code path के बाहर आने वाले output को पकड़ने के लिए process-level test जोड़ें जिसे आप आम तौर पर exercise करते हैं। नीचे दिया गया helper captured stdout transcript check करता है और client library पर भरोसा नहीं करता कि वह इसे आपके लिए parse करेगी।

```python
import json
import subprocess

def assert_protocol_stdout(data):
    assert data.endswith(b'\n'), 'stdout ended without a complete frame'
    for number, raw_line in enumerate(data.splitlines(), start=1):
        try:
            line = raw_line.decode('utf-8')
            message = json.loads(line)
        except (UnicodeDecodeError, json.JSONDecodeError) as error:
            raise AssertionError(
                f'non-protocol stdout on line {number}: {raw_line!r}'
            ) from error

        assert isinstance(message, dict), f'line {number} is not an object'
        assert message.get('jsonrpc') == '2.0', f'line {number} lacks JSON-RPC 2.0'
        is_notification = isinstance(message.get('method'), str) and 'id' not in message
        is_response = 'id' in message and ('result' in message or 'error' in message)
        assert is_notification or is_response, f'line {number} has no permitted shape'

def run_server(command, stdin_bytes):
    completed = subprocess.run(
        command,
        input=stdin_bytes,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=False,
    )
    assert_protocol_stdout(completed.stdout)
    return completed

def test_release_command_has_clean_stdout():
    initialize = (
        b'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",'
        b'\"params\":{\"protocolVersion\":\"2025-03-26\",'
        b'\"capabilities\":{},\"clientInfo\":{\"name\":\"boundary-test\",\"version\":\"1\"}}}\n'
    )
    completed = run_server(['./gateway-under-test'], initialize)
    assert completed.returncode == 0
```

`./gateway-under-test` की जगह अपना वास्तविक launch command इस्तेमाल करें। अगर उसे runtime, package wrapper या environment variables की जरूरत है, तो production में इस्तेमाल होने वाली वही conditions रखें। Process nonzero status के साथ बंद हो तब भी test को stdout inspect करना चाहिए। विफल process बंद होने से पहले illegal banner लिख सकता है।

Failure में raw byte line दिखाई देनी चाहिए। उपयोगी output इस तरह दिखता है:

```
AssertionError: non-protocol stdout on line 1: b'loading optional extension\n'
```

इस परिणाम से maintainer को पता चलता है कि कहां देखना है। `server did not initialize` जैसे सामान्य message से लोग protocol code में खोज शुरू कर देते हैं, जबकि defect shell script, logging configuration या dependency import में हो सकता है।

Harness को packaged form में चलाएं, केवल source checkout से नहीं। Packaging module resolution, environment variables, executable permissions और error paths बदलती है। यही वे जगहें हैं जहां accidental stdout writes दिखाई देती हैं।

## विनम्र errors नहीं, कठिन inputs test करें

Boundary suite में ऐसे inputs होने चाहिए जो वास्तविक process streams के विफल होने के तरीकों जैसे लगें, केवल हाथ से बनाए गए invalid objects जैसे नहीं। हर case को stdin संभालने वाले उसी entry point से भेजें और assert करें कि executor खाली रहे।

कम से कम इन cases को शामिल करें:

- Valid request से पहले एक साधारण debug line और अगली line पर valid request।
- Valid request के बाद उसी line पर बिना newline separator के banner।
- Truncated JSON object और उसके बाद end of file।
- बिना किसी separator के जुड़े हुए दो पूरे JSON objects।
- Plausible method वाला valid JSON object, लेकिन malformed parameters के साथ।

पहला case ऐसा implementation पकड़ता है जो bad lines को चुपचाप छोड़कर आगे बढ़ जाता है। दूसरा framing bugs पकड़ता है, जिन्हें line reader एक ही corrupted request समझ सकता है। Truncated case उस code को पकड़ता है जो peer के चले जाने के बाद buffer से input सुधारने की कोशिश करता है। Concatenated case ऐसे parser को पकड़ता है जो transport द्वारा एक line में एक frame की अनुमति होने के बावजूद कई top-level JSON values स्वीकार करने के लिए configured है।

इस suite को केवल parser corpus न बनाएं। हर bad input के साथ unique fake action name जोड़ें और recorder में जांचें कि उनमें से कोई दिखाई न दे। Rejected request के बाद valid request तभी शामिल करें जब आपकी documented policy session खुला रखने की अनुमति देती हो। अगर policy session बंद करती है, तो assert करें कि session पहले ही बंद होने के कारण दूसरी request कोई action नहीं करती।

Context में खतरनाक हो सकने वाले valid data की भी जांच करें। `method` को `tools/call` और `params` को string रखने वाली request valid JSON है, लेकिन call नहीं है। Numeric id को आपकी language boolean की तरह treat करती हो, तो आपने अनचाही type boundary पार कर ली है। Unknown arguments वाली request schema द्वारा reject की जा सकती है, लेकिन permissive object merger उन्हें downstream command builder में गलती से भेज सकता है।

## Startup noise के सामान्य कारण होते हैं

अधिकांश stdout contamination सामान्य maintenance work से आती है, किसी protocol को हराने की कोशिश से नहीं। इससे उसे पकड़ने की जरूरत कम नहीं होती।

Command wrapper version notice प्रिंट कर सकता है। Environment बदलने के बाद language runtime deprecation warning लिख सकता है। Developer package import में debug statement छोड़ सकता है, जिसे tests load नहीं करते। Crash handler terminal application के लिए बनाया गया हो और friendly message stdout पर लिख दे। Log collection के हिस्से के रूप में supervisor stderr को stdout के साथ मिला सकता है।

हर source को release test case मानें। Verbose dependency output चालू करने वाले environment variables set करें। Executable को ऐसे directory से चलाएं जहां optional configuration न हो। Recoverable startup failure उत्पन्न करें। Idle startup के बाद पहली request चलाएं। फिर हर बार raw transcript check करें।

Test पास कराने के लिए सभी logs को silence न करें। इससे operational problem केवल दूसरी जगह चली जाएगी। Diagnostics को stderr पर भेजें, operators को उन्हें collect करने का स्पष्ट तरीका दें और process से बाहर जाने से पहले sensitive values redact करें। जब streams अलग रहें, तो protocol correctness और उपयोगी diagnostics साथ-साथ रह सकते हैं।

## Valid JSON सुरक्षित request साबित नहीं करता

Strict JSON parser framing की सुरक्षा करता है। वह यह तय नहीं करता कि request credentials का उपयोग कर सकती है या किसी host तक पहुंच सकती है।

इन decisions को क्रम से रखें। पहले transport एक frame पढ़ता है। फिर parser एक JSON value बनाता है। इसके बाद protocol validator JSON-RPC और MCP method shape तय करता है। Schema validator tool arguments check करता है। इन stages के बाद ही authorization यह तय करे कि requested action चल सकती है या नहीं। Executor को raw method name और parameter dictionary नहीं, बल्कि typed action और वह decision मिलना चाहिए।

यह क्रम एक सूक्ष्म failure रोकता है: validation जारी रहते हुए HTTP request बना देना। अगर बाद का check call reject कर दे, लेकिन library पहले ही host resolve कर चुकी हो, connection खोल चुकी हो या command template expand कर चुकी हो, तो आपका test denial report कर सकता है, जबकि boundary ने काम पहले ही बाहर भेज दिया है। Fake executor test स्पष्ट रूप पकड़ लेता है। Local fake HTTP service या SSH test helper integration tests में accidental network activity पकड़ सकता है।

Rejected request को audit record में रखना उचित हो सकता है, लेकिन audit recording को ऐसा side channel न बनने दें जो action layer को invoke करे। Rejection को reason और ऐसे request fingerprint के साथ rejection की तरह record करें जो secrets उजागर न कर सके। External-action journal को उस attempted call से अलग रखें जो authorization पार ही नहीं कर पाई।

## एक accidental print खतरनाक failure छिपा सकता है

मान लीजिए gateway `deploy-preview` नाम के tool को support करता है। उसका handler कुछ arguments validate करता है, outbound request बनाना शुरू करता है और फिर check करता है कि caller चुने गए credential का उपयोग कर सकता है या नहीं। Refactor के दौरान developer चुने गए target की जांच के लिए stdout print जोड़ देता है।

Strict MCP client print देखता है, उसे JSON-RPC की तरह parse नहीं कर पाता और disconnect हो जाता है। Developer को protocol failure दिखता है और वह print ठीक कर देता है। यह असुविधाजनक है, लेकिन सुरक्षित है।

Permissive client line छोड़ देता है, error response पाता है और operator को बताता है कि authorization ने request अस्वीकार कर दी। इस बीच handler authorization check से पहले assembled request retry करने वाले HTTP helper को दे चुका था। Remote service को usable credential के बिना request मिलती है, शायद वह error लौटाती है और टीम के पास भ्रामक audit trail बचता है: agent को denied दिखाया जाता है, फिर भी service ने activity देखी।

Banner ने authorization ordering bug पैदा नहीं किया। उसने यह दिखाया कि forgiving transport recovery और early action construction खतरनाक जोड़ी क्यों हैं। समाधान कोई smarter skip rule नहीं है। Authorization decision को request construction से पहले ले जाएं, recorder से इसे साबित करें और stdout को इतना strict रखें कि accidental output तुरंत test fail कर दे।

## Wire contract को release gate बनाएं

हर packaged gateway command के पास clean-stdout test रखें और उसे continuous integration में चलाएं। Failure में पहली offending bytes, launch command और stderr को अलग attachment के रूप में शामिल करें। Maintainer को agent conversation फिर से बनाए बिना failure reproduce कर पाना चाहिए।

Malformed inbound frames का छोटा corpus version control में रखें। जब कोई वास्तविक defect सामने आए, तब एक case जोड़ें। केवल इसलिए किसी नई अजीब चीज़ को स्वीकार करने की जल्दी न करें कि किसी client ने उसे भेजा था। अगर implementation malformed traffic भेजता है, तो implementation ठीक करें या versioned compatibility boundary document करें जो actions की अनुमति न देती हो।

Sallyport HTTP और SSH execution को अपने bundled `sp mcp` stdio shim के पीछे रखता है, इसलिए यह harness उसी shim की boundary पर होना चाहिए और assert करना चाहिए कि rejected frame external call जारी नहीं कर सकता। यही अनुशासन हर ऐसे MCP server पर लागू होता है जो केवल text लौटाने से अधिक कर सकता है।

Release criterion सीधा है: stdout में केवल पूरे protocol messages हों और rejected input action recorder में कोई trace न छोड़े। इनमें से कोई भी assertion fail हो, तो build agent traffic संभालने के लिए तैयार नहीं है।
