import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express, { Request, Response } from "express";
import cors from "cors";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Reference content loaded from files
const REFERENCES_DIR = path.join(__dirname, "..", "references");
function loadReference(filename: string): string {
const filepath = path.join(REFERENCES_DIR, filename);
try {
return fs.readFileSync(filepath, "utf-8");
} catch (error) {
return `Error loading ${filename}: File not found`;
}
}
// Quick reference summary
const QUICK_REFERENCE = `# Android Development Quick Reference
Based on Google's official architecture guidance as demonstrated in NowInAndroid.
## Core Principles
1. **Offline-first**: Local database is source of truth, sync with remote
2. **Unidirectional data flow**: Events flow down, data flows up
3. **Reactive streams**: Use Kotlin Flow for all data exposure
4. **Modular by feature**: Each feature is self-contained with clear boundaries
5. **Testable by design**: Use interfaces and test doubles, no mocking libraries
## Architecture Layers
\`\`\`
┌─────────────────────────────────────────┐
│ UI Layer │
│ (Compose Screens + ViewModels) │
├─────────────────────────────────────────┤
│ Domain Layer │
│ (Use Cases - optional, for reuse) │
├─────────────────────────────────────────┤
│ Data Layer │
│ (Repositories + DataSources) │
└─────────────────────────────────────────┘
\`\`\`
## Module Types
\`\`\`
app/ # App module - navigation, scaffolding
feature/
├── featurename/
│ ├── api/ # Navigation keys (public)
│ └── impl/ # Screen, ViewModel, DI (internal)
core/
├── data/ # Repositories
├── database/ # Room DAOs, entities
├── network/ # Retrofit, API models
├── model/ # Domain models (pure Kotlin)
├── common/ # Shared utilities
├── ui/ # Reusable Compose components
├── designsystem/ # Theme, icons, base components
├── datastore/ # Preferences storage
└── testing/ # Test utilities
\`\`\`
## Available Tools
- get_architecture_reference: Full architecture documentation
- get_compose_patterns: Jetpack Compose UI patterns
- get_modularization_guide: Module structure and dependencies
- get_gradle_setup: Build configuration and convention plugins
- get_testing_patterns: Testing approach with test doubles
- generate_feature_module: Generate feature module boilerplate
- generate_viewmodel: Generate ViewModel with UiState
- generate_repository: Generate repository with offline-first pattern
- search_documentation: Search across all documentation
`;
// Feature module generator
function generateFeatureModule(
featureName: string,
packageName: string
): string {
const pascalCase = featureName
.split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
const camelCase = pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
const cleanName = featureName.replace(/[-_]/g, "");
return `# Generated Feature Module: ${featureName}
## Directory Structure
\`\`\`
feature/${cleanName}/
├── api/
│ ├── build.gradle.kts
│ └── src/main/kotlin/${packageName.replace(/\./g, "/")}/${cleanName}/api/
│ └── ${pascalCase}Navigation.kt
└── impl/
├── build.gradle.kts
└── src/main/kotlin/${packageName.replace(/\./g, "/")}/${cleanName}/impl/
├── ${pascalCase}Screen.kt
├── ${pascalCase}ViewModel.kt
├── ${pascalCase}UiState.kt
└── ${pascalCase}Navigation.kt
\`\`\`
## api/build.gradle.kts
\`\`\`kotlin
plugins {
alias(libs.plugins.nowinandroid.android.library)
alias(libs.plugins.kotlin.serialization)
}
android {
namespace = "${packageName}.feature.${cleanName}.api"
}
dependencies {
api(projects.core.model)
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.navigation.compose)
}
\`\`\`
## api/${pascalCase}Navigation.kt
\`\`\`kotlin
package ${packageName}.feature.${cleanName}.api
import androidx.navigation.NavController
import kotlinx.serialization.Serializable
@Serializable
data class ${pascalCase}Route(val id: String? = null)
fun NavController.navigateTo${pascalCase}(id: String? = null) {
navigate(${pascalCase}Route(id))
}
\`\`\`
## impl/build.gradle.kts
\`\`\`kotlin
plugins {
alias(libs.plugins.nowinandroid.android.feature)
alias(libs.plugins.nowinandroid.android.library.compose)
}
android {
namespace = "${packageName}.feature.${cleanName}.impl"
}
dependencies {
api(projects.feature.${cleanName}.api)
implementation(projects.core.data)
implementation(projects.core.ui)
implementation(projects.core.designsystem)
}
\`\`\`
## impl/${pascalCase}UiState.kt
\`\`\`kotlin
package ${packageName}.feature.${cleanName}.impl
sealed interface ${pascalCase}UiState {
data object Loading : ${pascalCase}UiState
data class Success(
val data: List<String> = emptyList(),
) : ${pascalCase}UiState
data class Error(
val message: String,
) : ${pascalCase}UiState
}
\`\`\`
## impl/${pascalCase}ViewModel.kt
\`\`\`kotlin
package ${packageName}.feature.${cleanName}.impl
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
@HiltViewModel
class ${pascalCase}ViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
// TODO: Inject repositories here
) : ViewModel() {
val uiState: StateFlow<${pascalCase}UiState> = flow {
// TODO: Replace with actual data flow
emit(${pascalCase}UiState.Success(data = listOf("Item 1", "Item 2")))
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ${pascalCase}UiState.Loading,
)
fun onAction(action: ${pascalCase}Action) {
when (action) {
is ${pascalCase}Action.ItemClicked -> handleItemClick(action.id)
}
}
private fun handleItemClick(id: String) {
// TODO: Handle item click
}
}
sealed interface ${pascalCase}Action {
data class ItemClicked(val id: String) : ${pascalCase}Action
}
\`\`\`
## impl/${pascalCase}Screen.kt
\`\`\`kotlin
package ${packageName}.feature.${cleanName}.impl
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@Composable
internal fun ${pascalCase}Route(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
viewModel: ${pascalCase}ViewModel = hiltViewModel(),
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
${pascalCase}Screen(
uiState = uiState,
onAction = viewModel::onAction,
onBackClick = onBackClick,
modifier = modifier,
)
}
@Composable
internal fun ${pascalCase}Screen(
uiState: ${pascalCase}UiState,
onAction: (${pascalCase}Action) -> Unit,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (uiState) {
is ${pascalCase}UiState.Loading -> {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator()
}
}
is ${pascalCase}UiState.Success -> {
LazyColumn(
modifier = modifier.fillMaxSize().padding(16.dp),
) {
items(uiState.data) { item ->
Text(
text = item,
modifier = Modifier.padding(vertical = 8.dp),
)
}
}
}
is ${pascalCase}UiState.Error -> {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = uiState.message,
color = MaterialTheme.colorScheme.error,
)
}
}
}
}
@Preview
@Composable
private fun ${pascalCase}ScreenPreview() {
${pascalCase}Screen(
uiState = ${pascalCase}UiState.Success(
data = listOf("Preview Item 1", "Preview Item 2"),
),
onAction = {},
onBackClick = {},
)
}
\`\`\`
## impl/${pascalCase}Navigation.kt
\`\`\`kotlin
package ${packageName}.feature.${cleanName}.impl
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import ${packageName}.feature.${cleanName}.api.${pascalCase}Route
fun NavGraphBuilder.${camelCase}Screen(
onBackClick: () -> Unit,
) {
composable<${pascalCase}Route> {
${pascalCase}Route(
onBackClick = onBackClick,
)
}
}
\`\`\`
## Next Steps
1. Add to settings.gradle.kts:
\`\`\`kotlin
include(":feature:${cleanName}:api")
include(":feature:${cleanName}:impl")
\`\`\`
2. Add dependency in app/build.gradle.kts:
\`\`\`kotlin
implementation(projects.feature.${cleanName}.impl)
\`\`\`
3. Add navigation in NiaNavHost:
\`\`\`kotlin
${camelCase}Screen(
onBackClick = navController::popBackStack,
)
\`\`\`
`;
}
// ViewModel generator
function generateViewModel(
name: string,
packageName: string,
repositoryName?: string
): string {
const pascalCase = name
.split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
const camelCase = pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
const repoParam = repositoryName
? `private val ${camelCase}Repository: ${repositoryName},`
: "// TODO: Inject repositories here";
const dataFlow = repositoryName
? `${camelCase}Repository.getData()
.map { data -> ${pascalCase}UiState.Success(data) }`
: `flow {
// TODO: Replace with actual data flow
emit(${pascalCase}UiState.Success(data = emptyList()))
}`;
return `# Generated ViewModel: ${pascalCase}ViewModel
## ${pascalCase}UiState.kt
\`\`\`kotlin
package ${packageName}
sealed interface ${pascalCase}UiState {
data object Loading : ${pascalCase}UiState
data class Success(
val data: List<Any> = emptyList(),
val isRefreshing: Boolean = false,
) : ${pascalCase}UiState
data class Error(
val message: String,
) : ${pascalCase}UiState
}
sealed interface ${pascalCase}Action {
data class ItemClicked(val id: String) : ${pascalCase}Action
data object Refresh : ${pascalCase}Action
}
\`\`\`
## ${pascalCase}ViewModel.kt
\`\`\`kotlin
package ${packageName}
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ${pascalCase}ViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
${repoParam}
) : ViewModel() {
val uiState: StateFlow<${pascalCase}UiState> = ${dataFlow}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ${pascalCase}UiState.Loading,
)
fun onAction(action: ${pascalCase}Action) {
when (action) {
is ${pascalCase}Action.ItemClicked -> handleItemClick(action.id)
is ${pascalCase}Action.Refresh -> refresh()
}
}
private fun handleItemClick(id: String) {
viewModelScope.launch {
// TODO: Handle item click
}
}
private fun refresh() {
viewModelScope.launch {
// TODO: Implement refresh
}
}
}
\`\`\`
## Key Patterns Used
1. **StateFlow for UI state**: Single source of truth for screen state
2. **SharingStarted.WhileSubscribed(5_000)**: Keeps flow active for 5 seconds after last subscriber (handles configuration changes)
3. **Sealed interface for actions**: Type-safe event handling
4. **SavedStateHandle**: For process death restoration
5. **viewModelScope.launch**: For one-shot operations
`;
}
// Repository generator
function generateRepository(
entityName: string,
packageName: string
): string {
const pascalCase = entityName
.split(/[-_]/)
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
.join("");
const camelCase = pascalCase.charAt(0).toLowerCase() + pascalCase.slice(1);
const pluralName = pascalCase + "s";
return `# Generated Repository: ${pascalCase}Repository
## ${pascalCase}Repository.kt (Interface)
\`\`\`kotlin
package ${packageName}.repository
import kotlinx.coroutines.flow.Flow
import ${packageName}.model.${pascalCase}
interface ${pascalCase}Repository {
fun get${pluralName}(): Flow<List<${pascalCase}>>
fun get${pascalCase}(id: String): Flow<${pascalCase}>
suspend fun update${pascalCase}(${camelCase}: ${pascalCase})
suspend fun delete${pascalCase}(id: String)
suspend fun syncWith(synchronizer: Synchronizer): Boolean
}
\`\`\`
## OfflineFirst${pascalCase}Repository.kt (Implementation)
\`\`\`kotlin
package ${packageName}.repository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ${packageName}.database.${pascalCase}Dao
import ${packageName}.database.${pascalCase}Entity
import ${packageName}.database.asEntity
import ${packageName}.database.asExternalModel
import ${packageName}.model.${pascalCase}
import ${packageName}.network.${pascalCase}NetworkDataSource
import ${packageName}.network.Network${pascalCase}
import ${packageName}.network.asEntity
import javax.inject.Inject
internal class OfflineFirst${pascalCase}Repository @Inject constructor(
private val ${camelCase}Dao: ${pascalCase}Dao,
private val networkDataSource: ${pascalCase}NetworkDataSource,
) : ${pascalCase}Repository {
override fun get${pluralName}(): Flow<List<${pascalCase}>> =
${camelCase}Dao.get${pascalCase}Entities()
.map { entities -> entities.map(${pascalCase}Entity::asExternalModel) }
override fun get${pascalCase}(id: String): Flow<${pascalCase}> =
${camelCase}Dao.get${pascalCase}Entity(id)
.map(${pascalCase}Entity::asExternalModel)
override suspend fun update${pascalCase}(${camelCase}: ${pascalCase}) {
${camelCase}Dao.upsert(${camelCase}.asEntity())
}
override suspend fun delete${pascalCase}(id: String) {
${camelCase}Dao.delete(id)
}
override suspend fun syncWith(synchronizer: Synchronizer): Boolean =
synchronizer.changeListSync(
versionReader = ChangeListVersions::${camelCase}Version,
changeListFetcher = { networkDataSource.get${pascalCase}ChangeList(after = it) },
versionUpdater = { latestVersion ->
copy(${camelCase}Version = latestVersion)
},
modelDeleter = ${camelCase}Dao::delete${pluralName},
modelUpdater = { changedIds ->
val network${pluralName} = networkDataSource.get${pluralName}(ids = changedIds)
${camelCase}Dao.upsert${pluralName}(network${pluralName}.map(Network${pascalCase}::asEntity))
},
)
}
\`\`\`
## ${pascalCase}Dao.kt
\`\`\`kotlin
package ${packageName}.database
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Upsert
import kotlinx.coroutines.flow.Flow
@Dao
interface ${pascalCase}Dao {
@Query("SELECT * FROM ${camelCase}s")
fun get${pascalCase}Entities(): Flow<List<${pascalCase}Entity>>
@Query("SELECT * FROM ${camelCase}s WHERE id = :id")
fun get${pascalCase}Entity(id: String): Flow<${pascalCase}Entity>
@Upsert
suspend fun upsert(entity: ${pascalCase}Entity)
@Upsert
suspend fun upsert${pluralName}(entities: List<${pascalCase}Entity>)
@Query("DELETE FROM ${camelCase}s WHERE id = :id")
suspend fun delete(id: String)
@Query("DELETE FROM ${camelCase}s WHERE id IN (:ids)")
suspend fun delete${pluralName}(ids: List<String>)
}
\`\`\`
## ${pascalCase}Entity.kt
\`\`\`kotlin
package ${packageName}.database
import androidx.room.Entity
import androidx.room.PrimaryKey
import ${packageName}.model.${pascalCase}
@Entity(tableName = "${camelCase}s")
data class ${pascalCase}Entity(
@PrimaryKey
val id: String,
val name: String,
val description: String,
// Add more fields as needed
)
fun ${pascalCase}Entity.asExternalModel() = ${pascalCase}(
id = id,
name = name,
description = description,
)
fun ${pascalCase}.asEntity() = ${pascalCase}Entity(
id = id,
name = name,
description = description,
)
\`\`\`
## DI Module
\`\`\`kotlin
package ${packageName}.di
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ${packageName}.repository.${pascalCase}Repository
import ${packageName}.repository.OfflineFirst${pascalCase}Repository
@Module
@InstallIn(SingletonComponent::class)
internal abstract class ${pascalCase}DataModule {
@Binds
abstract fun binds${pascalCase}Repository(
impl: OfflineFirst${pascalCase}Repository,
): ${pascalCase}Repository
}
\`\`\`
## Key Patterns Used
1. **Offline-first**: Local database is the source of truth
2. **Interface + Internal implementation**: Repository interface is public, implementation is internal
3. **Flow for reactive data**: All data exposed as Flow<T>
4. **Model mapping**: Entity ↔ Domain model conversion functions
5. **Hilt DI**: Bindings in a module for dependency injection
`;
}
// Search function
function searchDocumentation(query: string): string {
const files = [
{ name: "architecture.md", content: loadReference("architecture.md") },
{ name: "compose-patterns.md", content: loadReference("compose-patterns.md") },
{ name: "modularization.md", content: loadReference("modularization.md") },
{ name: "gradle-setup.md", content: loadReference("gradle-setup.md") },
{ name: "testing.md", content: loadReference("testing.md") },
];
const queryLower = query.toLowerCase();
const results: string[] = [];
for (const file of files) {
const lines = file.content.split("\n");
const matchingLines: { line: number; content: string }[] = [];
lines.forEach((line, index) => {
if (line.toLowerCase().includes(queryLower)) {
matchingLines.push({ line: index + 1, content: line.trim() });
}
});
if (matchingLines.length > 0) {
results.push(`\n## ${file.name}\n`);
matchingLines.slice(0, 10).forEach((match) => {
results.push(`- Line ${match.line}: ${match.content}`);
});
if (matchingLines.length > 10) {
results.push(`... and ${matchingLines.length - 10} more matches`);
}
}
}
if (results.length === 0) {
return `No results found for "${query}". Try searching for: viewmodel, repository, compose, navigation, hilt, room, testing, module`;
}
return `# Search Results for "${query}"\n${results.join("\n")}`;
}
// Create MCP server
function createServer(): McpServer {
const server = new McpServer({
name: "android-dev-mcp",
version: "1.0.0",
});
// Register tools
server.tool(
"get_quick_reference",
"Get a quick overview of Android development patterns and available tools",
{},
async () => ({
content: [{ type: "text", text: QUICK_REFERENCE }],
})
);
server.tool(
"get_architecture_reference",
"Get detailed architecture documentation (Data, Domain, UI layers, Repository pattern, etc.)",
{},
async () => ({
content: [{ type: "text", text: loadReference("architecture.md") }],
})
);
server.tool(
"get_compose_patterns",
"Get Jetpack Compose UI patterns (Screen architecture, state management, navigation, theming)",
{},
async () => ({
content: [{ type: "text", text: loadReference("compose-patterns.md") }],
})
);
server.tool(
"get_modularization_guide",
"Get module structure documentation (feature modules, core modules, dependencies)",
{},
async () => ({
content: [{ type: "text", text: loadReference("modularization.md") }],
})
);
server.tool(
"get_gradle_setup",
"Get Gradle and build configuration (version catalog, convention plugins, build variants)",
{},
async () => ({
content: [{ type: "text", text: loadReference("gradle-setup.md") }],
})
);
server.tool(
"get_testing_patterns",
"Get testing documentation (test doubles, ViewModel tests, Repository tests, UI tests)",
{},
async () => ({
content: [{ type: "text", text: loadReference("testing.md") }],
})
);
server.tool(
"generate_feature_module",
"Generate a complete feature module with Screen, ViewModel, UiState, and Navigation",
{
feature_name: {
type: "string",
description:
"Feature name in kebab-case (e.g., 'user-profile', 'settings')",
},
package_name: {
type: "string",
description: "Base package name (e.g., 'com.example.app')",
},
},
async ({ feature_name, package_name }) => ({
content: [
{
type: "text",
text: generateFeatureModule(
feature_name as string,
package_name as string
),
},
],
})
);
server.tool(
"generate_viewmodel",
"Generate a ViewModel with UiState following NowInAndroid patterns",
{
name: {
type: "string",
description: "ViewModel name (e.g., 'topic', 'settings')",
},
package_name: {
type: "string",
description: "Package name for the ViewModel",
},
repository_name: {
type: "string",
description:
"Optional repository interface name to inject (e.g., 'TopicsRepository')",
},
},
async ({ name, package_name, repository_name }) => ({
content: [
{
type: "text",
text: generateViewModel(
name as string,
package_name as string,
repository_name as string | undefined
),
},
],
})
);
server.tool(
"generate_repository",
"Generate an offline-first repository with DAO and Entity",
{
entity_name: {
type: "string",
description:
"Entity name in PascalCase (e.g., 'Topic', 'NewsResource')",
},
package_name: {
type: "string",
description: "Base package name for the data layer",
},
},
async ({ entity_name, package_name }) => ({
content: [
{
type: "text",
text: generateRepository(
entity_name as string,
package_name as string
),
},
],
})
);
server.tool(
"search_documentation",
"Search across all documentation for a keyword or phrase",
{
query: {
type: "string",
description:
"Search query (e.g., 'StateFlow', 'navigation', 'Room')",
},
},
async ({ query }) => ({
content: [
{ type: "text", text: searchDocumentation(query as string) },
],
})
);
return server;
}
// Main entry point
async function main() {
const args = process.argv.slice(2);
const useSSE = args.includes("--sse") || process.env.MCP_TRANSPORT === "sse";
const port = parseInt(process.env.PORT || "3000", 10);
const server = createServer();
if (useSSE) {
// HTTP/SSE mode for Docker
const app = express();
app.use(cors());
app.use(express.json());
const transports = new Map<string, SSEServerTransport>();
// Health check endpoint
app.get("/health", (_req: Request, res: Response) => {
res.json({ status: "ok", name: "android-dev-mcp", version: "1.0.0" });
});
// SSE endpoint
app.get("/sse", async (req: Request, res: Response) => {
console.log("New SSE connection");
const transport = new SSEServerTransport("/messages", res);
const sessionId = Date.now().toString();
transports.set(sessionId, transport);
res.on("close", () => {
console.log("SSE connection closed");
transports.delete(sessionId);
});
await server.connect(transport);
});
// Message endpoint
app.post("/messages", async (req: Request, res: Response) => {
// Get the most recent transport
const transport = Array.from(transports.values()).pop();
if (transport) {
// The SSE transport handles messages internally
res.status(202).json({ status: "accepted" });
} else {
res.status(503).json({ error: "No active SSE connection" });
}
});
app.listen(port, () => {
console.log(`🚀 Android Dev MCP Server running on http://localhost:${port}`);
console.log(` SSE endpoint: http://localhost:${port}/sse`);
console.log(` Health check: http://localhost:${port}/health`);
});
} else {
// stdio mode for local/CLI usage
console.error("Starting Android Dev MCP Server (stdio mode)...");
const transport = new StdioServerTransport();
await server.connect(transport);
}
}
main().catch(console.error);