Skip to content

Commit e2e0607

Browse files
committed
feat: add native MCP support
1 parent 138e8ce commit e2e0607

21 files changed

Lines changed: 250 additions & 26 deletions

β€Ž.env.exampleβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ PORT=3000 # Port number for the server
33
HOST=0.0.0.0 # Use 'localhost' to restrict to local access
44
NODE_ENV=development # 'development' | 'production'
55
PUBLIC_URL=http://localhost:3000 # Base URL for the proxy link builder
6+
MCP_ENABLED=false # 'true' | 'false'
67

78
# TMDB Configuration
89
TMDB_API_KEY=your_tmdb_api_key_here

β€ŽREADME.mdβ€Ž

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
This is an extendable multi site scraping framework, which follows the implementation guidelines of the **[OMSS (Open Media Streaming Standard)](https://github.com/omss-spec/omss-spec)**. It demonstrates how to build a compliant streaming media aggregation service that scrapes content from multiple providers and returns standardized responses. It handles most of the logic already for you. You just have to add the scraping logic!
1414

15+
Additionally, this is **the worlds first AI-Enabled Streaming Framework**! With built-in support for the Model Context Protocol (MCP), you can easily integrate LLMs and intelligent agents to find streaming sources using natural language queries, or even automate the management of your streaming backend using AI assistants.
16+
1517
---
1618

1719
## [_πŸš€Check This Template Out To Get Started!πŸš€_](https://github.com/omss-spec/template)
@@ -31,6 +33,8 @@ The `@omss/framework` is the official TypeScript/Node.js implementation framewor
3133
### Key Features
3234

3335
- βœ… **Standardized API**: Consistent response format across all providers
36+
- βœ… **MCP Support**: Optional Model Context Protocol endpoint for LLM integration
37+
- βœ… **Stremio Compatibility**: Designed to work seamlessly with Stremio Addons and also support Stremio Addon SDK
3438
- βœ… **Multi-Provider Support**: Aggregate sources from multiple streaming providers
3539
- βœ… **Built-in Proxy**: Automatic URL proxying with header forwarding
3640
- βœ… **TMDB Integration**: Validation against The Movie Database
@@ -40,14 +44,17 @@ The `@omss/framework` is the official TypeScript/Node.js implementation framewor
4044
- βœ… **Health Checks**: Monitor provider availability
4145
- βœ… **Refresh API**: Force cache invalidation when needed
4246

47+
4348
## πŸ“‹ Table of Contents
4449

4550
- [Installation](#installation)
4651
- [Quick Start](#quick-start)
4752
- [Configuration](#configuration)
4853
- [Creating Custom Providers](#creating-custom-providers)
54+
- [MCP Endpoints](#mcp-endpoints)
4955
- [API Endpoints](#api-endpoints)
5056
- [Environment Variables](#environment-variables)
57+
- [Stremio Compatibility](#stremio-compatibility)
5158
- [Architecture](#architecture)
5259
- [OMSS Compliance](#omss-compliance)
5360
- [License](#license)
@@ -435,6 +442,14 @@ export class MyProvider extends BaseProvider {
435442
436443
See the detailed [Provider Creation Guide](./examples/example-provider.ts) for a complete walkthrough.
437444
445+
## 🧩 MCP Endpoints
446+
447+
The Model Context Protocol (MCP) is an optional JSON-RPC-like API that allows LLMs and other intelligent agents to interact with your OMSS server in a structured way. This can be useful for advanced integrations, such as allowing users to ask an AI assistant to find streaming sources for a movie or TV show.
448+
449+
When enabled, the MCP endpoint is exposed at `/mcp` (configurable) and accepts POST requests with a JSON body containing the method and parameters. The framework currently supports the following MCP method:
450+
451+
- `omss_get_sources`: Fetches streaming sources for a movie or TV episode by TMDB ID. Parameters are the same as the regular API endpoints, but wrapped in an MCP request.
452+
438453
## πŸ“‘ API Endpoints
439454
440455
### GET `/v1/movies/:tmdbId`
@@ -533,6 +548,7 @@ Health check endpoint.
533548
PORT=3000 # Port number for the server
534549
HOST=0.0.0.0 # Use 'localhost' to restrict to local access
535550
NODE_ENV=development # 'development' | 'production'
551+
MCP_ENABLED=false # 'true' | 'false'
536552
537553
# TMDB Configuration
538554
TMDB_API_KEY=your_tmdb_api_key_here
@@ -547,6 +563,14 @@ REDIS_PORT=6379 # default Redis port
547563
REDIS_PASSWORD= # Redis password if required
548564
```
549565
566+
## πŸ“Ί Stremio Compatibility
567+
568+
Although the original OMSS standard was not specifically designed for Stremio, this framework is fully compatible with Stremio. In both ways:
569+
570+
1. You can use this framework to build a Stremio Addon. To enable the Stremio Addon SDK, simply set the `stremioAddon` option to `true` in the server configuration. This will automatically add the required endpoints (`/stremio/manifest.json`) and response formatting to work seamlessly with Stremio.
571+
572+
2. You can bind other Stremio Addon's directly to this framework. Since all Stremio Addons follow a standardized API, you can just pass the manifest URL of any Stremio Addon to the `stremioAddons` configuration option, and the framework will automatically fetch the manifest, extract the sources and bind them to your server. This allows you to easily aggregate sources from existing Stremio Addons alongside your custom providers, and expose them all through a single unified API.
573+
550574
## πŸ—οΈ Architecture
551575

552576
```

β€Žexamples/basic-server.tsβ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ async function main() {
3838
]
3939
},
4040

41+
// MCP (Model Context Protocol) for exposing your providers and the scraping capabilities of your server to LLMs and other intelligent agents, via a simple JSON-RPC-like API. This is an optional feature, but can be useful when you want to integrate your server with LLMs or other intelligent agents (like when you want to be able to "Hey <agent> i want to watch the dark knight, can you find me a source for that?")
42+
mcp: {
43+
enabled: process.env.MCP_ENABLED === 'true', // Whether to enable the MCP controller and endpoints
44+
path: '/mcp', // default: /mcp - The path where the MCP endpoint will be exposed. You can change this if you want to expose it on a different path.
45+
},
46+
4147
// TMDB (required)
4248
tmdb: {
4349
apiKey: process.env.TMDB_API_KEY!,
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import type { FastifyReply, FastifyRequest } from 'fastify'
2+
import type { MCPRequest, MCPResponse } from '../core/types/index.js'
3+
import { SourceService } from '../services/source.service.js'
4+
5+
interface ToolsCallParams {
6+
name: string
7+
arguments: {
8+
tmdbId: string
9+
mediaType: 'movie'
10+
} | {
11+
tmdbId: string
12+
mediaType: 'tv'
13+
season: number
14+
episode: number
15+
}
16+
}
17+
18+
export class MCPController {
19+
constructor(private sourceService: SourceService) {}
20+
21+
async handle(request: FastifyRequest, reply: FastifyReply) {
22+
const body = request.body as MCPRequest | undefined
23+
24+
if (!body || !body.method) {
25+
return reply.code(400).send({
26+
id: body?.id ?? request.id,
27+
error: { code: 400, message: 'Invalid MCP request' },
28+
} satisfies MCPResponse)
29+
}
30+
31+
switch (body.method) {
32+
case 'tools/list':
33+
return this.handleToolsList(body, reply)
34+
case 'tools/call':
35+
return this.handleToolsCall(body, reply)
36+
default:
37+
return reply.code(400).send({
38+
id: body.id ?? request.id,
39+
error: { code: 400, message: `Unsupported method: ${body.method}` },
40+
} satisfies MCPResponse)
41+
}
42+
}
43+
44+
private async handleToolsList(req: MCPRequest, reply: FastifyReply) {
45+
const tools =[
46+
{
47+
name: 'omss_get_sources',
48+
description:
49+
'Fetches streaming sources for a movie or TV episode by TMDB id using the OMSS backend.',
50+
inputSchema: {
51+
type: 'object',
52+
properties: {
53+
tmdbId: { type: 'string', description: 'TMDB id (stringified number).' },
54+
mediaType: {
55+
type: 'string',
56+
enum: ['movie', 'tv'],
57+
description: 'Type of media to fetch sources for.',
58+
},
59+
season: {
60+
type: 'integer',
61+
description: 'Season number (required for tv).',
62+
},
63+
episode: {
64+
type: 'integer',
65+
description: 'Episode number (required for tv).',
66+
},
67+
},
68+
required: ['tmdbId', 'mediaType'],
69+
},
70+
},
71+
]
72+
73+
const res: MCPResponse = {
74+
id: req.id,
75+
result: { tools },
76+
}
77+
return reply.send(res)
78+
}
79+
80+
private async handleToolsCall(req: MCPRequest, reply: FastifyReply) {
81+
const params = req.params as ToolsCallParams | undefined
82+
if (!params || !params.name) {
83+
return reply.code(400).send({
84+
id: req.id,
85+
error: { code: 400, message: 'Missing tool name in params' },
86+
} satisfies MCPResponse)
87+
}
88+
89+
if (params.name !== 'omss_get_sources') {
90+
return reply.code(400).send({
91+
id: req.id,
92+
error: { code: 400, message: `Unknown tool: ${params.name}` },
93+
} satisfies MCPResponse)
94+
}
95+
96+
const args = params.arguments as ToolsCallParams['arguments']
97+
if (!args?.tmdbId || !args?.mediaType) {
98+
return reply.code(400).send({
99+
id: req.id,
100+
error: {
101+
code: 400,
102+
message: 'tmdbId and mediaType are required',
103+
},
104+
} satisfies MCPResponse)
105+
}
106+
107+
try {
108+
let result
109+
if (args.mediaType === 'movie') {
110+
// Reuse the movie path logic
111+
result = await this.sourceService.getMovieSources(args.tmdbId)
112+
} else {
113+
// mediaType === 'tv'
114+
if (
115+
typeof args.season !== 'number' ||
116+
typeof args.episode !== 'number'
117+
) {
118+
return reply.code(400).send({
119+
id: req.id,
120+
error: {
121+
code: 400,
122+
message:
123+
'season and episode are required for mediaType=tv',
124+
},
125+
} satisfies MCPResponse)
126+
}
127+
128+
result = await this.sourceService.getTVSources(
129+
args.tmdbId,
130+
args.season,
131+
args.episode,
132+
)
133+
}
134+
135+
const res: MCPResponse = {
136+
id: req.id,
137+
result, // this is the OMSS SourceResponse
138+
}
139+
return reply.send(res)
140+
} catch (err: any) {
141+
const res: MCPResponse = {
142+
id: req.id,
143+
error: {
144+
code: 500,
145+
message: 'Failed to fetch sources from OMSS',
146+
data: { message: err?.message ?? String(err) },
147+
},
148+
}
149+
return reply.code(500).send(res)
150+
}
151+
}
152+
}

β€Žsrc/controllers/proxy.controller.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FastifyRequest, FastifyReply } from 'fastify'
22
import { ProxyService, isStreamingResponse } from '../services/proxy.service.js'
3-
import { ProxyData } from '../core/types.js'
3+
import { ProxyData } from '../core/types/index.js'
44

55
interface ProxyQuery {
66
data: string

β€Žsrc/controllers/stremio.controller.tsβ€Ž

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { FastifyReply, FastifyRequest } from 'fastify'
22
import { SourceService } from '../services/source.service.js'
3-
import { OMSSConfig, SourceResponse, Source } from '../core/types.js'
3+
import { OMSSConfig, SourceResponse, Source } from '../core/types/index.js'
44
import { TMDBService } from '../services/tmdb.service.js'
5+
import { safeId } from '../utils/string.js'
56

67
interface StreamParams {
78
type: string
@@ -45,11 +46,7 @@ export class StremioController {
4546
* GET /stremio/manifest.json
4647
*/
4748
async getManifest(_req: FastifyRequest, reply: FastifyReply) {
48-
const safeName = this.config.name
49-
.toLowerCase()
50-
.replace(/[^a-z\s]/g, '')
51-
.trim()
52-
.replace(/\s+/g, '.')
49+
const safeName = safeId(this.config.name) || 'omss-addon'
5350

5451
const manifest: StremioManifest = {
5552
id: `omss.${safeName}`,

β€Žsrc/core/cache.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CacheConfig } from '../core/types.js'
1+
import { CacheConfig } from '../core/types/index.js'
22
import { Redis as RedisClient } from 'ioredis'
33

44
export interface CacheService {

β€Žsrc/core/errors.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ErrorCode } from './types.js'
1+
import { ErrorCode } from './types/index.js'
22
import { v4 as uuidv4 } from 'uuid'
33

44
export class OMSSError extends Error {

β€Žsrc/core/server.tsβ€Ž

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Fastify, { FastifyInstance } from 'fastify'
22
import cors, { FastifyCorsOptions } from '@fastify/cors'
3-
import { OMSSConfig } from './types.js'
3+
import { OMSSConfig } from './types/index.js'
44
import { ProviderRegistry } from '../providers/provider-registry.js'
55
import { createCacheService, CacheService } from './cache.js'
66
import { SourceService } from '../services/source.service.js'
@@ -16,6 +16,7 @@ import { validateContentType } from '../middleware/validation.js'
1616
import { TMDBService } from '../services/tmdb.service.js'
1717
import { v4 as uuidv4 } from 'uuid'
1818
import { StremioController } from '../controllers/stremio.controller.js'
19+
import { MCPController } from 'src/controllers/mcp.controller.js'
1920

2021
export class OMSSServer {
2122
private app: FastifyInstance
@@ -35,6 +36,7 @@ export class OMSSServer {
3536
private proxyController: ProxyController
3637
private healthController: HealthController
3738
private stremioController?: StremioController
39+
private mcpController?: MCPController
3840

3941
constructor(config: OMSSConfig, registry?: ProviderRegistry) {
4042
this.config = config
@@ -100,6 +102,10 @@ export class OMSSServer {
100102
this.stremioController = new StremioController(this.sourceService, config, this.tmdbService)
101103
}
102104

105+
if (config.mcp?.enabled) {
106+
this.mcpController = new MCPController(this.sourceService)
107+
}
108+
103109
// Setup middleware and routes
104110
this.setupMiddleware(config.cors)
105111
this.setupRoutes()
@@ -155,6 +161,12 @@ export class OMSSServer {
155161
this.app.get('/stremio/stream/:type/:id', this.stremioController.getStream.bind(this.stremioController))
156162
}
157163

164+
if (this.mcpController && this.config.mcp?.enabled) {
165+
const path = this.config.mcp.path || '/mcp'
166+
this.app.post(path, this.mcpController.handle.bind(this.mcpController))
167+
}
168+
169+
158170
// 404 handler
159171
this.app.setNotFoundHandler((request, reply) => {
160172
reply.code(404).send({
@@ -190,7 +202,7 @@ export class OMSSServer {
190202

191203
const addons = this.config.stremio?.stremioAddons || []
192204
const enabledAddons = addons.filter((a) => a.enabled !== false).length
193-
205+
const mcpStatus = this.config.mcp?.enabled ? 'Enabled' : 'Disabled'
194206
const stremioStatus = this.config.stremio?.enableNativeAddon ? `Enabled` : 'Disabled'
195207

196208
console.log(`
@@ -203,6 +215,7 @@ export class OMSSServer {
203215
β•‘ Providers: ${this.registry ? this.registry['providers'].size.toString().padEnd(42) : '0'.padEnd(42)}β•‘
204216
β•‘ Cache: ${(this.config.cache?.type || 'memory').padEnd(42)}β•‘
205217
β•‘ Stremio: ${stremioStatus.padEnd(42)}β•‘
218+
β•‘ MCP: ${mcpStatus.padEnd(42)}β•‘
206219
β•‘ Addons: ${`${enabledAddons} enabled`.padEnd(42)}β•‘
207220
╠════════════════════════════════════════════════════════╣
208221
β•‘ Endpoints: β•‘

β€Žsrc/core/types/index.tsβ€Ž

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './main.js'
2+
export * from './mcp-types.js'

0 commit comments

Comments
Β (0)