Overview
Test the Gracenote Video MCP Server locally before building your full application. This guide shows you how to connect using:
Claude Desktop - Chat interface with MCP support
MCP Inspector - Debug and test MCP tools
LM Studio - Local LLM with MCP integration
All three options use the STDIO Proxy (gn-mcp-proxy.py) — a local Python script that bridges desktop AI tools to the Gracenote MCP Server, handling Cognito authentication automatically.
Prerequisites
Python 3.11+ installed
uv package manager installed
Gracenote MCP Server credentials
One of: Claude Desktop, MCP Inspector, or LM Studio
Setup: STDIO Proxy
How the STDIO Proxy Works
Desktop App ←stdio→ gn-mcp-proxy.py ←HTTPS + Cognito→ Gracenote MCP Server
Desktop app launches the proxy via uv run gn-mcp-proxy.py
Proxy authenticates with Cognito using credentials from .env
Desktop app sends MCP requests via STDIN
Proxy forwards requests to Gracenote MCP Server over HTTPS
Proxy returns responses via STDOUT
Token refresh and reconnection are handled automatically
Step 1: Install uv
Mac/Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows:
powershell - ExecutionPolicy ByPass - c "irm https://astral.sh/uv/install.ps1 | iex"
Step 2: Create Project and Install Dependencies
mkdir gn-mcp-proxy
cd gn-mcp-proxy
uv venv
uv pip install mcp boto3 python-dotenv httpx
Step 3: Configure Credentials
Create .env in the gn-mcp-proxy directory:
MCP_SERVER_HOST = gn-video-mcp-server.k8s-p.cloud.gracenote.com
MCP_SERVER_PORT = 443
COGNITO_USER_POOL_REGION = us-west-2
COGNITO_CLIENT_ID = your-client-id
COGNITO_CLIENT_SECRET = your-client-secret
COGNITO_USERNAME = your-username
COGNITO_PASSWORD = your-password
Step 4: Create the Proxy Script
Create gn-mcp-proxy.py:
import os
import sys
import asyncio
import time
import typing
import base64
import hashlib
import hmac
import boto3
import httpx
import mcp.types as types
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from botocore.exceptions import ClientError
from contextlib import AsyncExitStack
from dotenv import load_dotenv
class BearerAuth ( httpx . Auth ):
"""Adds the current Bearer token from the client's headers to each request."""
def __init__ (self, client: "GnRemoteClient" ) -> None :
self ._client = client
def auth_flow (
self, request: httpx.Request
) -> typing.Generator[httpx.Request, httpx.Response, None ]:
token = self ._client.headers.get( "Authorization" )
if token:
request.headers[ "Authorization" ] = token
yield request
class GnRemoteClient :
def compute_secret_hash (self, username: str , client_id: str , client_secret: str ) -> str :
message = username + client_id
dig = hmac.new(
key = client_secret.encode( 'utf-8' ),
msg = message.encode( 'utf-8' ),
digestmod = hashlib.sha256
).digest()
return base64.b64encode(dig).decode()
def get_cognito_id_token (self, region: str , client_id: str , client_secret: str , username: str , password: str ) -> str :
try :
client = boto3.client( 'cognito-idp' , region_name = region)
except Exception as e:
raise RuntimeError ( f "Failed to initialize Cognito client: { e } " )
auth_params = { 'USERNAME' : username, 'PASSWORD' : password}
if client_secret:
auth_params[ 'SECRET_HASH' ] = self .compute_secret_hash(username, client_id, client_secret)
try :
response = client.initiate_auth(
ClientId = client_id,
AuthFlow = 'USER_PASSWORD_AUTH' ,
AuthParameters = auth_params,
)
if "ChallengeName" in response:
challenge = response[ "ChallengeName" ]
raise RuntimeError (
f "Cognito returned unsupported challenge: { challenge } "
"(e.g. NEW_PASSWORD_REQUIRED or MFA). This flow is not supported."
)
auth_result = response[ "AuthenticationResult" ]
self .token_expires_in = int (auth_result.get( "ExpiresIn" , 3600 ))
return auth_result[ "IdToken" ]
except ClientError as e:
error_code = e.response[ 'Error' ][ 'Code' ]
if error_code == 'NotAuthorizedException' :
print ( "ERROR: Invalid credentials - check username/password" , file = sys.stderr)
elif error_code == 'UserNotConfirmedException' :
print ( "ERROR: User must be confirmed before authentication" , file = sys.stderr)
elif error_code == 'UserNotFoundException' :
print ( "ERROR: User does not exist" , file = sys.stderr)
else :
print ( f "ERROR: Authentication failed: { e } " , file = sys.stderr)
raise
def __init__ (self):
self .mcp_host = os.getenv( "MCP_SERVER_HOST" )
self .mcp_port = os.getenv( "MCP_SERVER_PORT" , "443" )
scheme = "https" if str ( self .mcp_port) == "443" else "http"
self .url = f " { scheme } :// {self .mcp_host } : {self .mcp_port } /mcp"
self .cognito_region = os.getenv( "COGNITO_USER_POOL_REGION" , "us-west-2" )
self .cognito_client_id = os.getenv( "COGNITO_CLIENT_ID" )
self .cognito_client_secret = os.getenv( "COGNITO_CLIENT_SECRET" , "" )
self .cognito_username = os.getenv( "COGNITO_USERNAME" )
self .cognito_password = os.getenv( "COGNITO_PASSWORD" )
self .headers = { "Accept" : "text/event-stream" }
self .session = None
self .exit_stack = AsyncExitStack()
self ._token_lock = asyncio.Lock()
self .token_acquired_at = None
self .token_expires_in = 3600
def _should_refresh_token (self) -> bool :
if not self .token_acquired_at:
return True
elapsed = time.time() - self .token_acquired_at
return elapsed > ( self .token_expires_in - 300 )
def _get_token (self) -> str :
return self .get_cognito_id_token(
region = self .cognito_region,
client_id = self .cognito_client_id,
client_secret = self .cognito_client_secret,
username = self .cognito_username,
password = self .cognito_password,
)
async def ensure_valid_token (self):
async with self ._token_lock:
if not self ._should_refresh_token():
return
loop = asyncio.get_event_loop()
token = await loop.run_in_executor( None , self ._get_token)
self .headers[ "Authorization" ] = f "Bearer { token } "
self .token_acquired_at = time.time()
async def connect (self):
await self .ensure_valid_token()
transport = await self .exit_stack.enter_async_context(
streamablehttp_client( self .url, headers = self .headers, auth = BearerAuth( self ))
)
read_stream, write_stream, _ = transport
self .session = await self .exit_stack.enter_async_context(
ClientSession(read_stream, write_stream)
)
await self .session.initialize()
async def reconnect (self) -> None :
"""Close the current transport/session and connect again."""
await self .exit_stack.aclose()
self .exit_stack = AsyncExitStack()
self .session = None
await self .connect()
async def close (self):
await self .exit_stack.aclose()
# Exceptions that indicate a dead connection and warrant reconnect + retry
_CONNECTION_ERRORS = ( ConnectionError , OSError , httpx.TransportError)
load_dotenv()
remote_client = GnRemoteClient()
server = Server( "GN Video MCP Server Local Proxy" )
async def _with_reconnect (coro_factory):
"""Run coro_factory(); on connection-related failure, reconnect and retry once."""
try :
return await coro_factory()
except _CONNECTION_ERRORS as e:
print ( f "Connection lost ( { e } ), reconnecting..." , file = sys.stderr)
await remote_client.reconnect()
return await coro_factory()
@server.list_tools ()
async def handle_list_tools () -> list[types.Tool]:
await remote_client.ensure_valid_token()
async def _call ():
result = await remote_client.session.list_tools()
return result.tools
return await _with_reconnect(_call)
@server.call_tool ()
async def handle_call_tool (name: str , arguments: dict | None ):
await remote_client.ensure_valid_token()
safe_arguments = arguments or {}
async def _call ():
return await remote_client.session.call_tool(name, safe_arguments)
return await _with_reconnect(_call)
@server.list_prompts ()
async def handle_list_prompts () -> list[types.Prompt]:
await remote_client.ensure_valid_token()
async def _call ():
result = await remote_client.session.list_prompts()
return result.prompts
return await _with_reconnect(_call)
@server.get_prompt ()
async def handle_get_prompt (name: str , arguments: dict | None ) -> types.GetPromptResult:
await remote_client.ensure_valid_token()
async def _call ():
return await remote_client.session.get_prompt(name, arguments)
return await _with_reconnect(_call)
async def main ():
required_vars = [ "MCP_SERVER_HOST" , "COGNITO_CLIENT_ID" , "COGNITO_USERNAME" , "COGNITO_PASSWORD" ]
missing = [var for var in required_vars if not os.getenv(var)]
if missing:
print ( f "ERROR: Missing required environment variables: { ', ' .join(missing) } " , file = sys.stderr)
sys.exit( 1 )
# Since stdio is used for MCP communication, all logs go to stderr
try :
await remote_client.connect()
print ( "Connected to Gracenote MCP Server. Starting stdio proxy..." , file = sys.stderr)
async with stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, server.create_initialization_options())
except asyncio.CancelledError:
pass
except Exception as e:
print ( f "ERROR: { e } " , file = sys.stderr)
sys.exit( 1 )
finally :
try :
await remote_client.close()
except ExceptionGroup as eg:
print ( "Connection closed with background task errors:" , file = sys.stderr)
for exc in eg.exceptions:
print ( f " - { exc } " , file = sys.stderr)
except Exception as e:
print ( f "Cleanup error: { e } " , file = sys.stderr)
if __name__ == "__main__" :
try :
asyncio.run(main())
except KeyboardInterrupt :
print ( " \n Proxy shut down." , file = sys.stderr)
Option 1: Claude Desktop
Step 1: Install Claude Desktop
Download from claude.ai/download
Step 2: Configure MCP Server
Edit Claude Desktop's config file (Settings > Developer > Edit Config):
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Add the Gracenote MCP Server:
{
"mcpServers" : {
"GN Video MCP Server" : {
"command" : "/absolute/path/uv" ,
"args" : [
"--directory" ,
"/absolute/path/to/gn-mcp-proxy" ,
"run" ,
"gn-mcp-proxy.py"
]
}
}
}
Important : Replace /absolute/path/to/ with the full path to your respective directories.
Step 3: Restart Claude Desktop
Restart the app completely. Look for the MCP server icon (plug icon) in the chat interface.
Step 4: Test with a Query
Try these queries:
"What is the Gracenote TMSId of The Matrix, and where can I watch it?"
"Find comedies from 2023 and return their Gracenote TMSIds"
"Show me Tom Hanks movies and include their Gracenote TMSIds"
Claude will automatically use the Gracenote MCP tools to answer.
Option 2: MCP Inspector
Run the proxy with MCP Inspector to test tools interactively in your browser (requires Node.js to be installed on your machine):
cd gn-mcp-proxy
npx @modelcontextprotocol/inspector uv run gn-mcp-proxy.py
Opens at http://localhost:5173. You can:
View all available tools
Test tool calls with custom inputs
See raw request/response data
Debug authentication issues
Example test:
Tool: resolve_entities
Input:
{
"request" : [{
"title" : "The Matrix" ,
"entityType" : "Movie" ,
"language" : "en" ,
"countryCode" : "USA"
}]
}
Option 3: LM Studio (Local LLM)
Step 1: Install LM Studio
Download from lmstudio.ai
Step 2: Download a Model
In LM Studio, download a model that supports function calling:
mistral-7b-instruct
llama-3-8b-instruct
phi-3-mini
Step 3: Configure MCP Server
In LM Studio → Developer tab → Add MCP Server :
Name: GN Video MCP Server
Type: STDIO
Command: uv
Arguments: --directory /absolute/path/to/gn-mcp-proxy run gn-mcp-proxy.py
Step 4: Start Chat
Enable MCP tools and test with queries like:
"Find The Matrix and tell me its Gracenote TMSId and where to watch it"
"Search for sci-fi movies from the 1990s and return their Gracenote TMSIds"
Troubleshooting
"Command not found: uv"
Use the absolute path: /Users/yourusername/.cargo/bin/uv (detemine it by running: which uv)
or reinstall with curl -LsSf https://astral.sh/uv/install.sh | sh
Claude Desktop doesn't show the server
Check logs: ~/Library/Logs/Claude/mcp*.log (Mac)
Verify the path in config is absolute (not relative)
Restart Claude Desktop completely (quit and reopen)
"Authentication failed"
Verify credentials in .env
Check for typos or extra spaces
Confirm your account is activated with your Gracenote representative
"Tool call timeout"
Check internet connection
Try with a simpler query first
Once you've tested locally, see Connecting to MCP to build your own integration with direct HTTPS, or jump to the Solutions for use case examples.
Last modified on March 31, 2026